diff --git a/.gitignore b/.gitignore index 08f1c3b25..d28292a8e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ dist/ dist2/ dist2.7/ dist3/ +dist-react/ allDist/ packages/**/runtime coverage/ @@ -14,6 +15,11 @@ coverage/ /packages/vue/mobile-first.ts /packages/vue/app.ts +/packages/react/index.ts +/packages/react/pc.ts +/packages/react/mobile.ts +/packages/react/app.ts + /examples/**/playwright-report vite.config.ts.timestamp* vitest.config.ts.timestamp* @@ -47,5 +53,3 @@ packages/theme/scripts/theme-result.txt packages/theme/scripts/themeExcel.xlsx packages/theme/src/theme/*-theme/component.js - - diff --git a/examples/docs/newsrc/uses/useMonaco.js b/examples/docs/newsrc/uses/useMonaco.js new file mode 100644 index 000000000..fb376012d --- /dev/null +++ b/examples/docs/newsrc/uses/useMonaco.js @@ -0,0 +1,31 @@ +import * as monaco from 'monaco-editor' +import { hooks } from '@opentiny/vue-common' +// monaco ESM模块集成说明 : https://github.com/microsoft/monaco-editor/blob/main/docs/integrate-esm.md#using-vite +// https://github.com/vitejs/vite/discussions/1791#discussioncomment-321046 +import HtmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker' +self.MonacoEnvironment = { + getWorker: () => new HtmlWorker() +} + +export function useMonaco(selector) { + const state = { + editor: null, + getCode: () => state.editor && state.editor.getValue(), + setCode: (code) => state.editor && state.editor.setValue(code), + hotKey: (key, fn) => state.editor && state.editor.addCommand(key, fn), + format: () => state.editor && state.editor.trigger('anyString', 'editor.action.formatDocument'), + scrollTop: () => state.editor && state.editor.setScrollTop(0) + } + + hooks.onMounted(() => { + state.editor = monaco.editor.create(document.querySelector(selector), { + value: '', + language: 'html', + theme: 'vs-dark', + tabSize: 2, + automaticLayout: true + }) + }) + hooks.onUnmounted(() => (state.editor = null)) + return state +} diff --git a/examples/react-docs/index.html b/examples/react-docs/index.html new file mode 100644 index 000000000..6a8a3c0a3 --- /dev/null +++ b/examples/react-docs/index.html @@ -0,0 +1,13 @@ + + + + + + + Opentiny React 组件调试 + + +
+ + + diff --git a/examples/react-docs/package.json b/examples/react-docs/package.json new file mode 100644 index 000000000..478db6c64 --- /dev/null +++ b/examples/react-docs/package.json @@ -0,0 +1,33 @@ +{ + "name": "@opentiny/react-docs", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "@opentiny/react": "workspace:~", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.14", + "@types/react-dom": "^18.2.6", + "@typescript-eslint/eslint-plugin": "^5.61.0", + "@typescript-eslint/parser": "^5.61.0", + "@vitejs/plugin-react": "^4.0.1", + "autoprefixer": "^10.4.12", + "eslint": "^8.44.0", + "eslint-plugin-react-hooks": "^4.6.0", + "postcss": "^8.4.16", + "tailwindcss": "^3.3.3", + "typescript": "^5.0.2", + "vite": "^4.4.0", + "vite-plugin-react": "^4.0.1", + "vite-plugin-svgr": "^3.2.0" + } +} diff --git a/examples/react-docs/postcss.config.mjs b/examples/react-docs/postcss.config.mjs new file mode 100644 index 000000000..cb9b0aaa5 --- /dev/null +++ b/examples/react-docs/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + } +} diff --git a/examples/react-docs/public/vite.svg b/examples/react-docs/public/vite.svg new file mode 100644 index 000000000..e7b8dfb1b --- /dev/null +++ b/examples/react-docs/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/react-docs/src/App.tsx b/examples/react-docs/src/App.tsx new file mode 100644 index 000000000..101e93316 --- /dev/null +++ b/examples/react-docs/src/App.tsx @@ -0,0 +1,16 @@ +import { Alert } from '@opentiny/react' + +// 在这里导入组件,进行 api 调试 +function App() { + return ( +
+ +
+ ) +} + +export default App diff --git a/examples/react-docs/src/main.css b/examples/react-docs/src/main.css new file mode 100644 index 000000000..9be80073a --- /dev/null +++ b/examples/react-docs/src/main.css @@ -0,0 +1,8 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +.app { + margin: 10px; + width: 500px; +} \ No newline at end of file diff --git a/examples/react-docs/src/main.tsx b/examples/react-docs/src/main.tsx new file mode 100644 index 000000000..dce122516 --- /dev/null +++ b/examples/react-docs/src/main.tsx @@ -0,0 +1,8 @@ +// import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.tsx' +import './main.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + +) diff --git a/examples/react-docs/tailwind.config.cjs b/examples/react-docs/tailwind.config.cjs new file mode 100644 index 000000000..b802834cb --- /dev/null +++ b/examples/react-docs/tailwind.config.cjs @@ -0,0 +1,11 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + '../../packages/react/src/**/*.{css,less,vue,js,jsx,ts,tsx}', + '!../../packages/react/src/**/node_modules', + ], + theme: { + extend: {}, + }, + plugins: [], +} \ No newline at end of file diff --git a/examples/react-docs/tsconfig.json b/examples/react-docs/tsconfig.json new file mode 100644 index 000000000..a7fc6fbf2 --- /dev/null +++ b/examples/react-docs/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/examples/react-docs/tsconfig.node.json b/examples/react-docs/tsconfig.node.json new file mode 100644 index 000000000..42872c59f --- /dev/null +++ b/examples/react-docs/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/examples/react-docs/vite.config.ts b/examples/react-docs/vite.config.ts new file mode 100644 index 000000000..4ec11151b --- /dev/null +++ b/examples/react-docs/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import svgr from "vite-plugin-svgr"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + react({ include: /\.(mdx|js|jsx|ts|tsx)$/ }), svgr() + ], +}) diff --git a/examples/react-site/.prettierrc.js b/examples/react-site/.prettierrc.js new file mode 100644 index 000000000..1be6b5a02 --- /dev/null +++ b/examples/react-site/.prettierrc.js @@ -0,0 +1,15 @@ +module.exports = { + printWidth: 160, // 一行120字符数,如果超过会进行换行 + tabWidth: 2, // tab等2个空格 + useTabs: false, // 用空格缩进行 + semi: true, // 行尾使用分号 + singleQuote: true, // 字符串使用单引号 + quoteProps: 'as-needed', // 仅在需要时在对象属性添加引号 + jsxSingleQuote: false, // 在JSX中使用双引号 + trailingComma: 'es5', // 使用尾逗号(对象、数组等) + bracketSpacing: true, // 对象的括号间增加空格 + jsxBracketSameLine: false, // 将多行JSX元素的>放在最后一行的末尾 + arrowParens: 'avoid', // 在唯一的arrow函数参数周围省略括号 + vueIndentScriptAndStyle: false, // 不缩进Vue文件中的 diff --git a/examples/react-site/demos/app/alert/center.jsx b/examples/react-site/demos/app/alert/center.jsx new file mode 100644 index 000000000..47037f9d9 --- /dev/null +++ b/examples/react-site/demos/app/alert/center.jsx @@ -0,0 +1,17 @@ +import { Alert as TinyAlert } from '@pe-3/react' +import ReactDOM from 'react-dom/client' +function App(props) { + return (
+ {props.children} +
) +} + +export default class extends HTMLElement { + connectedCallback() { + ReactDOM.createRoot(this).render( + + + + ) + } +} \ No newline at end of file diff --git a/examples/react-site/demos/app/alert/center.vue b/examples/react-site/demos/app/alert/center.vue new file mode 100644 index 000000000..138a5be43 --- /dev/null +++ b/examples/react-site/demos/app/alert/center.vue @@ -0,0 +1,9 @@ + + + diff --git a/examples/react-site/demos/app/alert/size.jsx b/examples/react-site/demos/app/alert/size.jsx new file mode 100644 index 000000000..a14fc76a2 --- /dev/null +++ b/examples/react-site/demos/app/alert/size.jsx @@ -0,0 +1,18 @@ +import { Alert as TinyAlert } from '@pe-3/react' +import ReactDOM from 'react-dom/client' +function App(props) { + return (
+ {props.children} +
) +} + +export default class extends HTMLElement { + connectedCallback() { + ReactDOM.createRoot(this).render( + + + + + ) + } +} \ No newline at end of file diff --git a/examples/react-site/demos/app/alert/size.vue b/examples/react-site/demos/app/alert/size.vue new file mode 100644 index 000000000..39684a6ff --- /dev/null +++ b/examples/react-site/demos/app/alert/size.vue @@ -0,0 +1,11 @@ + + + diff --git a/examples/react-site/demos/app/alert/title.jsx b/examples/react-site/demos/app/alert/title.jsx new file mode 100644 index 000000000..61d7149fe --- /dev/null +++ b/examples/react-site/demos/app/alert/title.jsx @@ -0,0 +1,25 @@ +import { Alert as TinyAlert } from '@pe-3/react' +import ReactDOM from 'react-dom/client' +function App(props) { + return (
+ {props.children} +
) +} + +export default class extends HTMLElement { + connectedCallback() { + ReactDOM.createRoot(this).render( + + +
+ '通过 slot 设置自定义 title' + }} + > + +
+ ) + } +} \ No newline at end of file diff --git a/examples/react-site/demos/app/alert/title.vue b/examples/react-site/demos/app/alert/title.vue new file mode 100644 index 000000000..da9a0c421 --- /dev/null +++ b/examples/react-site/demos/app/alert/title.vue @@ -0,0 +1,11 @@ + + + diff --git a/examples/react-site/demos/app/alert/type.jsx b/examples/react-site/demos/app/alert/type.jsx new file mode 100644 index 000000000..e2b22d074 --- /dev/null +++ b/examples/react-site/demos/app/alert/type.jsx @@ -0,0 +1,21 @@ +import { Alert as TinyAlert } from '@pe-3/react' +import ReactDOM from 'react-dom/client' +function App(props) { + return (
+ {props.children} +
) +} + +export default class extends HTMLElement { + connectedCallback() { + ReactDOM.createRoot(this).render( + + + + + + + + ) + } +} \ No newline at end of file diff --git a/examples/react-site/demos/app/alert/type.vue b/examples/react-site/demos/app/alert/type.vue new file mode 100644 index 000000000..f8f288a4b --- /dev/null +++ b/examples/react-site/demos/app/alert/type.vue @@ -0,0 +1,11 @@ + + + diff --git a/examples/react-site/demos/app/alert/webdoc/alert.cn.md b/examples/react-site/demos/app/alert/webdoc/alert.cn.md new file mode 100644 index 000000000..819b6c660 --- /dev/null +++ b/examples/react-site/demos/app/alert/webdoc/alert.cn.md @@ -0,0 +1,7 @@ +--- +title: Alert 警告 +--- + +# Alert 警告 + +
Alert 警告,提供 warning、error、info、success 四种类型显示不同类别的信息。
diff --git a/examples/react-site/demos/app/alert/webdoc/alert.en.md b/examples/react-site/demos/app/alert/webdoc/alert.en.md new file mode 100644 index 000000000..08cd41e21 --- /dev/null +++ b/examples/react-site/demos/app/alert/webdoc/alert.en.md @@ -0,0 +1,7 @@ +--- +title: Alert +--- + +# Alert + +
Alert alarms, including warning, error, info, and success.
diff --git a/examples/react-site/demos/app/alert/webdoc/alert.json b/examples/react-site/demos/app/alert/webdoc/alert.json new file mode 100644 index 000000000..6db2fad42 --- /dev/null +++ b/examples/react-site/demos/app/alert/webdoc/alert.json @@ -0,0 +1,228 @@ +{ + "column": "2", + "owner": "", + "demos": [ + { + "demoId": "base", + "name": { + "zh-CN": "基本用法", + "en-US": "Basic Usage" + }, + "desc": { + "zh-CN": "详细用法参考如下示例", + "en-US": "For details, see the following example." + }, + "codeFiles": [ + "base.vue" + ] + }, + { + "demoId": "type", + "name": { + "zh-CN": "类型", + "en-US": "Type" + }, + "desc": { + "zh-CN": "

通过 type 设置不同的类型。可选值:success、warning、info、error,默认值:success 。

\n", + "en-US": "

Set different types through type. The options are success, warning, info, and error. The default value is success.

\n" + }, + "codeFiles": [ + "type.vue" + ] + }, + { + "demoId": "size", + "name": { + "zh-CN": "大尺寸", + "en-US": "Large Size" + }, + "desc": { + "zh-CN": "

通过 size 属性设置不同的尺寸,可选值:nomal、large,默认值:nomal 。

\n", + "en-US": "

Set different sizes through the size attribute. The options are nomal and large. The default value is nomal.

\n" + }, + "codeFiles": [ + "size.vue" + ] + }, + { + "demoId": "title", + "name": { + "zh-CN": "自定义标题", + "en-US": "Custom Title" + }, + "desc": { + "zh-CN": "

size 为 large 时显示标题,可设置 titleslot 自定义标题。默认标题根据设置的 type 显示。

\n", + "en-US": "

When size is set to large, the title is displayed. You can set title or slot to customize the title. The default title is displayed according to the set type.

\n" + }, + "codeFiles": [ + "title.vue" + ] + }, + { + "demoId": "center", + "name": { + "zh-CN": "文字居中", + "en-US": "Center text" + }, + "desc": { + "zh-CN": "

通过 center 属性可使文字显示居中。

\n", + "en-US": "

You can use the center property to center the text.

\n" + }, + "codeFiles": [ + "center.vue" + ] + } + ], + "apis": [ + { + "name": "alert", + "type": "component", + "properties": [ + { + "name": "closable", + "type": "Boolean", + "defaultValue": "该属性的默认值为 true", + "desc": { + "zh-CN": "设置警告是否可以关闭", + "en-US": "Set whether alarms can be disabled." + }, + "demoId": "closable" + }, + { + "name": "icon", + "type": "String , Object", + "defaultValue": "", + "desc": { + "zh-CN": "设置警告的图标,默认会根据 type 值自动使用对应图标", + "en-US": "Set the alarm icon. By default, the corresponding icon is automatically used based on the value of type." + }, + "demoId": "icon" + }, + { + "name": "size", + "type": "String", + "defaultValue": "该属性的默认值为 normal", + "desc": { + "zh-CN": "设置警告的大小 nomal/large, 缺省为 nomal。;该属性的可选值为 nomal / large", + "en-US": "Set the warning size to nomal or large. The default value is nomal. ;The value of this attribute can be nomal or large" + }, + "demoId": "size" + }, + { + "name": "title", + "type": "String", + "defaultValue": "", + "desc": { + "zh-CN": "设置警告的标题,在 size 为 large 时有效,默认根据 type 自动设置", + "en-US": "Set the warning title. This parameter is valid only when size is set to large. By default, the alarm title is automatically set based on type." + }, + "demoId": "title" + }, + { + "name": "type", + "type": "String", + "defaultValue": "该属性的默认值为 success", + "desc": { + "zh-CN": "设置警告的类型;该属性的可选值为 success/warning/info/error", + "en-US": "Set the alarm type. The value of this attribute can be success / warning / info / error" + }, + "demoId": "type" + }, + { + "name": "description", + "type": "String", + "defaultValue": "", + "desc": { + "zh-CN": "设置警告的提示内容,默认为空;", + "en-US": "Set the warning prompt content. The default value is null." + }, + "demoId": "custom-description" + }, + { + "name": "center", + "type": "Boolean", + "defaultValue": "该属性的默认值为 false", + "desc": { + "zh-CN": "文字是否居中", + "en-US": "Whether the text is centered" + }, + "demoId": "center" + }, + { + "name": "close-text", + "type": "String", + "defaultValue": "", + "desc": { + "zh-CN": "关闭按钮自定义文本", + "en-US": "Customized text of the close button" + }, + "demoId": "close-text" + }, + { + "name": "show-icon", + "type": "Boolean", + "defaultValue": "该属性的默认值为 true", + "desc": { + "zh-CN": "是否显示图标", + "en-US": "Display icon" + }, + "demoId": "show-icon" + } + ], + "events": [ + { + "name": "close", + "type": "", + "defaultValue": "", + "desc": { + "zh-CN": "关闭 alert 时触发的事件", + "en-US": "Event triggered when the alert function is disabled" + }, + "demoId": "close-events" + } + ], + "slots": [ + { + "name": "default", + "type": "", + "defaultValue": "", + "desc": { + "zh-CN": "组件默认插槽", + "en-US": "Default slot of the component" + }, + "demoId": "slot-default" + }, + { + "name": "title", + "type": "", + "defaultValue": "", + "desc": { + "zh-CN": "标题的内容", + "en-US": "Title content" + }, + "demoId": "title" + }, + { + "name": "description", + "type": "", + "defaultValue": "", + "desc": { + "zh-CN": "提示内容", + "en-US": "Prompt Content" + }, + "demoId": "custom-description" + }, + { + "name": "close", + "type": "", + "defaultValue": "", + "desc": { + "zh-CN": "自定义关闭按钮,当 closable 属性为 false 时有效", + "en-US": "" + }, + "demoId": "custom-close" + } + ] + } + ] +} \ No newline at end of file diff --git a/examples/react-site/demos/app/button/button-click-webcomp.jsx b/examples/react-site/demos/app/button/button-click-webcomp.jsx new file mode 100644 index 000000000..17c36ec39 --- /dev/null +++ b/examples/react-site/demos/app/button/button-click-webcomp.jsx @@ -0,0 +1,17 @@ +import { Button as TinyButton} from '@pe-3/react' +import ReactDOM from 'react-dom/client' +function App(props) { + return (
+ {props.children} +
) +} + +export default class extends HTMLElement { + connectedCallback() { + ReactDOM.createRoot(this).render( + + 默认按钮 + + ) + } +} \ No newline at end of file diff --git a/examples/react-site/demos/app/button/button-click.vue b/examples/react-site/demos/app/button/button-click.vue new file mode 100644 index 000000000..96ac564b2 --- /dev/null +++ b/examples/react-site/demos/app/button/button-click.vue @@ -0,0 +1,9 @@ + + + diff --git a/examples/react-site/demos/app/button/button-round-webcomp.jsx b/examples/react-site/demos/app/button/button-round-webcomp.jsx new file mode 100644 index 000000000..9b26fb019 --- /dev/null +++ b/examples/react-site/demos/app/button/button-round-webcomp.jsx @@ -0,0 +1,18 @@ +import { Button as TinyButton} from '@pe-3/react' +import ReactDOM from 'react-dom/client' + +function App(props) { + return (
+ {props.children} +
) +} + +export default class extends HTMLElement { + connectedCallback() { + ReactDOM.createRoot(this).render( + + 主要按钮 + + ) + } +} \ No newline at end of file diff --git a/examples/react-site/demos/app/button/button-round.vue b/examples/react-site/demos/app/button/button-round.vue new file mode 100644 index 000000000..b747bd02c --- /dev/null +++ b/examples/react-site/demos/app/button/button-round.vue @@ -0,0 +1,8 @@ + + diff --git a/examples/react-site/demos/app/button/button-type-webcomp.jsx b/examples/react-site/demos/app/button/button-type-webcomp.jsx new file mode 100644 index 000000000..d43d7b339 --- /dev/null +++ b/examples/react-site/demos/app/button/button-type-webcomp.jsx @@ -0,0 +1,18 @@ +import { Button as TinyButton} from '@pe-3/react' +import ReactDOM from 'react-dom/client' + +function App(props) { + return (
+ {props.children} +
) +} + +export default class extends HTMLElement { + connectedCallback() { + ReactDOM.createRoot(this).render( + + 成功按钮 + + ) + } +} \ No newline at end of file diff --git a/examples/react-site/demos/app/button/button-type.vue b/examples/react-site/demos/app/button/button-type.vue new file mode 100644 index 000000000..58b20d4be --- /dev/null +++ b/examples/react-site/demos/app/button/button-type.vue @@ -0,0 +1,8 @@ + + diff --git a/examples/react-site/demos/app/button/webdoc/button.cn.md b/examples/react-site/demos/app/button/webdoc/button.cn.md new file mode 100644 index 000000000..62ea794f5 --- /dev/null +++ b/examples/react-site/demos/app/button/webdoc/button.cn.md @@ -0,0 +1,16 @@ +--- +title: Button 按钮 +--- + +# Button 按钮 + +
+ +按钮组件一般用于触发一些操作。 + +```typescript +import { Button } from '@opentiny/vue'; +``` + +
+ diff --git a/examples/react-site/demos/app/button/webdoc/button.en.md b/examples/react-site/demos/app/button/webdoc/button.en.md new file mode 100644 index 000000000..8a64f8880 --- /dev/null +++ b/examples/react-site/demos/app/button/webdoc/button.en.md @@ -0,0 +1,8 @@ +--- +title: Button 按钮 +--- + +# Button 按钮 + +
按钮组件一般用于触发一些操作。
+ diff --git a/examples/react-site/demos/app/button/webdoc/button.json b/examples/react-site/demos/app/button/webdoc/button.json new file mode 100644 index 000000000..434c8da1f --- /dev/null +++ b/examples/react-site/demos/app/button/webdoc/button.json @@ -0,0 +1,71 @@ +{ + "column": "2", + "demos": [ + { + "demoId": "button-type", + "name": { + "zh-CN": "按钮类型", + "en-US": "button type" + }, + "desc": { + "zh-CN": "

通过属性type配置按钮类型,包含successinfowarningdanger四种类型。", + "en-US": "

button type

" + }, + "codeFiles": ["button-type.vue"] + }, + { + "demoId": "button-round", + "name": { + "zh-CN": "圆角按钮", + "en-US": "button round" + }, + "desc": { + "zh-CN": "

通过round属性设置按钮是否圆角", + "en-US": "

button round

" + }, + "codeFiles": ["button-round.vue"] + }, + { + "demoId": "button-click", + "name": { + "zh-CN": "事件", + "en-US": "events" + }, + "desc": { + "zh-CN": "

按钮点击事件。", + "en-US": "

bbutton click

" + }, + "codeFiles": ["button-click.vue"] + } + ], + "apis": [ + { + "name": "Button", + "type": "component", + "properties": [ + { + "name": "type", + "type": "primary | success | warning", + "defaultValue": "", + "desc": { + "zh-CN": "

展示按钮不同的状态

", + "en-US": "display different button" + }, + "demoId": "button-type" + } + ], + "events": [ + { + "name": "click", + "type": "", + "defaultValue": "", + "desc": { + "zh-CN": "

点击按钮时触发的回调

", + "en-US": "Click" + }, + "demoId": "button-click" + } + ] + } + ] +} diff --git a/examples/react-site/demos/config.js b/examples/react-site/demos/config.js new file mode 100644 index 000000000..dabcc1730 --- /dev/null +++ b/examples/react-site/demos/config.js @@ -0,0 +1,4 @@ +export default { + isMobile: false, + initApp: (app) => { } +} \ No newline at end of file diff --git a/examples/react-site/demos/overviewimage/button.svg b/examples/react-site/demos/overviewimage/button.svg new file mode 100644 index 000000000..7e443c451 --- /dev/null +++ b/examples/react-site/demos/overviewimage/button.svg @@ -0,0 +1,106 @@ + + + Button按钮 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/react-site/demos/overviewimage/buttongroup.svg b/examples/react-site/demos/overviewimage/buttongroup.svg new file mode 100644 index 000000000..2dd081880 --- /dev/null +++ b/examples/react-site/demos/overviewimage/buttongroup.svg @@ -0,0 +1,138 @@ + + + Buttongroup选块组 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + 2 + + + + + + + + 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/react-site/demos/overviewimage/dev.svg b/examples/react-site/demos/overviewimage/dev.svg new file mode 100644 index 000000000..fa4b5e668 --- /dev/null +++ b/examples/react-site/demos/overviewimage/dev.svg @@ -0,0 +1,76 @@ + + + Modal 弹出框 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 开发中 + + + + + + \ No newline at end of file diff --git a/examples/react-site/demos/webdoc/introduce.md b/examples/react-site/demos/webdoc/introduce.md new file mode 100644 index 000000000..4162f22a2 --- /dev/null +++ b/examples/react-site/demos/webdoc/introduce.md @@ -0,0 +1,7 @@ +--- +title: 介绍 | TinyReact +--- + +# 介绍 + +TinyReact 简介 md文档 \ No newline at end of file diff --git a/examples/react-site/demos/webdoc/menus.js b/examples/react-site/demos/webdoc/menus.js new file mode 100644 index 000000000..483aacb06 --- /dev/null +++ b/examples/react-site/demos/webdoc/menus.js @@ -0,0 +1,43 @@ +// 注意,删除了useFor属性 +// title,label增加英文版,以应对将来的国际化功能 +export const standaloneMenus = [ + { + label: '组件总览', + key: 'overview', + }, +]; + +export const docMenus = [ + { + label: '使用指南', + labelEn: 'Guide', //*********** + key: 'doc_use', + children: [ + { + title: '背景简介', + titleEn: 'Introduce', + key: 'introduce', + }, + ], + }, +]; + +//------------------------------------------------------------------- +export const cmpMenus = [ + { + label: '表单选择', + labelEn: 'Form Selection', + key: 'cmp_formselect', + children: [ + { name: 'Button', nameCn: '按钮', key: 'button' } + ] + }, + { + 'label': '提示组件', + 'labelEn': 'Tips Components', + 'key': 'cmp_tips_components', + 'children': [ + { 'nameCn': '警告', 'name': 'Alert', 'key': 'alert' } + ] + } +]; diff --git a/examples/react-site/env/.env b/examples/react-site/env/.env new file mode 100644 index 000000000..6594d266b --- /dev/null +++ b/examples/react-site/env/.env @@ -0,0 +1,2 @@ +# 1、声明一个变量 +VITE_CONTEXT=/tiny-react/ diff --git a/examples/react-site/index.html b/examples/react-site/index.html new file mode 100644 index 000000000..75a3a9eb0 --- /dev/null +++ b/examples/react-site/index.html @@ -0,0 +1,28 @@ + + + + + + + + + OpenTiny - TinyVue + + + + + + + + + +
+ + + + + + diff --git a/examples/react-site/md.extend.config.js b/examples/react-site/md.extend.config.js new file mode 100644 index 000000000..a3b38a513 --- /dev/null +++ b/examples/react-site/md.extend.config.js @@ -0,0 +1,41 @@ +import md_prism from 'markdown-it-prism'; // 高亮 +import md_emoji from 'markdown-it-emoji'; // 表情 +import md_sub from 'markdown-it-sub'; // 下标 ~ ~ +import md_sup from 'markdown-it-sup'; // 上标 ^ ^ +import md_mark from 'markdown-it-mark'; // 高亮文字 == == +import md_container from 'markdown-it-container'; // 提示块 +import md_anchor from 'markdown-it-anchor'; +export const MdExt = [md_emoji, md_sub, md_sup, md_mark]; + +// 自定义container +function createContainer(klass) { + return [ + md_container, + klass, + { + render(tokens, idx) { + const token = tokens[idx]; + const info = token.info.trim().slice(klass.length).trim() || ''; + if (token.nesting === 1) { + return `

${info}

\n`; + } else { + return `
\n`; + } + }, + }, + ]; +} + +export function mdInstall(md) { + md.use(md_prism, { plugins: ['line-highlight'] }) + .use(...createContainer('tip')) + .use(...createContainer('info')) + .use(...createContainer('warning')) + .use(...createContainer('danger')) + .use(md_anchor, { + permalink: true, + permalinkBefore: true, + permalinkSymbol: '', + slugify: s => encodeURIComponent(s), + }); +} diff --git a/examples/react-site/package.json b/examples/react-site/package.json new file mode 100644 index 000000000..158adbca1 --- /dev/null +++ b/examples/react-site/package.json @@ -0,0 +1,67 @@ +{ + "name": "@opentiny/react-site", + "private": true, + "version": "0.1.0", + "scripts": { + "start": "node ./scripts/copy.js && node ./scripts/build-react.mjs && vite", + "build:react": "node ./scripts/build-react.mjs", + "build": "node ./scripts/copy.js && node ./scripts/build-react.mjs && vite build", + "prettier": "npx prettier --write ./**/*.{ts,tsx,css,less,scss}", + "stylelint": "npx stylelint ./src/**/*.scss ./src/**/*.less ./src/**/*.css --fix" + }, + "dependencies": { + "@babel/preset-env": "^7.22.20", + "@opentiny/vue": "^3.10.1", + "@pe-3/react": "^1.0.15", + "@rollup/plugin-babel": "^6.0.3", + "@unocss/reset": "0.38.2", + "@vitejs/plugin-react": "^4.0.4", + "@vueuse/head": "0.7.13", + "dompurify": "^3.0.1", + "github-markdown-css": "^5.1.0", + "highlight.js": "^11.5.1", + "marked": "^4.3.0", + "prismjs": "^1.28.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "vite4": "npm:vite@4", + "vue": "^3.2.37", + "vue-i18n": "^9.1.10", + "vue-router": "4.1.5" + }, + "devDependencies": { + "@types/markdown-it": "^12.2.3", + "@types/node": "^17.0.45", + "@unocss/preset-icons": "^0.38.2", + "@vitejs/plugin-vue": "^2.3.3", + "@vitejs/plugin-vue-jsx": "^1.3.10", + "@vue/compiler-sfc": "^3.2.37", + "chalk": "4.1.2", + "cross-spawn": "^7.0.3", + "fast-glob": "^3.2.12", + "fs-extra": "^10.1.0", + "less": "^4.1.3", + "markdown-it": "^13.0.1", + "markdown-it-anchor": "^8.6.4", + "markdown-it-container": "^3.0.0", + "markdown-it-emoji": "^2.0.2", + "markdown-it-mark": "^3.0.1", + "markdown-it-prism": "^2.2.4", + "markdown-it-sub": "^1.0.0", + "markdown-it-sup": "^1.0.0", + "markdown-it-table-of-contents": "^0.6.0", + "markdown-it-toc-done-right": "^4.2.0", + "naive-ui": "^2.30.6", + "prettier": "^2.7.1", + "stylelint": "^14.9.1", + "stylelint-config-standard": "^26.0.0", + "unocss": "^0.39.3", + "unplugin-auto-import": "0.8.7", + "unplugin-vue-components": "^0.19.9", + "uslug": "^1.0.4", + "vite": "^2.9.12", + "vite-plugin-html": "^2.0.0", + "vite-plugin-inspect": "^0.5.0", + "vite-plugin-md": "0.13.1" + } +} diff --git a/examples/react-site/public/@demos/app/alert/base.vue b/examples/react-site/public/@demos/app/alert/base.vue new file mode 100644 index 000000000..cdac59438 --- /dev/null +++ b/examples/react-site/public/@demos/app/alert/base.vue @@ -0,0 +1,15 @@ + + + diff --git a/examples/react-site/public/@demos/app/alert/center.vue b/examples/react-site/public/@demos/app/alert/center.vue new file mode 100644 index 000000000..138a5be43 --- /dev/null +++ b/examples/react-site/public/@demos/app/alert/center.vue @@ -0,0 +1,9 @@ + + + diff --git a/examples/react-site/public/@demos/app/alert/size.vue b/examples/react-site/public/@demos/app/alert/size.vue new file mode 100644 index 000000000..39684a6ff --- /dev/null +++ b/examples/react-site/public/@demos/app/alert/size.vue @@ -0,0 +1,11 @@ + + + diff --git a/examples/react-site/public/@demos/app/alert/title.vue b/examples/react-site/public/@demos/app/alert/title.vue new file mode 100644 index 000000000..da9a0c421 --- /dev/null +++ b/examples/react-site/public/@demos/app/alert/title.vue @@ -0,0 +1,11 @@ + + + diff --git a/examples/react-site/public/@demos/app/alert/type.vue b/examples/react-site/public/@demos/app/alert/type.vue new file mode 100644 index 000000000..f8f288a4b --- /dev/null +++ b/examples/react-site/public/@demos/app/alert/type.vue @@ -0,0 +1,11 @@ + + + diff --git a/examples/react-site/public/@demos/app/alert/webdoc/alert.cn.md b/examples/react-site/public/@demos/app/alert/webdoc/alert.cn.md new file mode 100644 index 000000000..819b6c660 --- /dev/null +++ b/examples/react-site/public/@demos/app/alert/webdoc/alert.cn.md @@ -0,0 +1,7 @@ +--- +title: Alert 警告 +--- + +# Alert 警告 + +
Alert 警告,提供 warning、error、info、success 四种类型显示不同类别的信息。
diff --git a/examples/react-site/public/@demos/app/alert/webdoc/alert.en.md b/examples/react-site/public/@demos/app/alert/webdoc/alert.en.md new file mode 100644 index 000000000..08cd41e21 --- /dev/null +++ b/examples/react-site/public/@demos/app/alert/webdoc/alert.en.md @@ -0,0 +1,7 @@ +--- +title: Alert +--- + +# Alert + +
Alert alarms, including warning, error, info, and success.
diff --git a/examples/react-site/public/@demos/app/alert/webdoc/alert.json b/examples/react-site/public/@demos/app/alert/webdoc/alert.json new file mode 100644 index 000000000..6db2fad42 --- /dev/null +++ b/examples/react-site/public/@demos/app/alert/webdoc/alert.json @@ -0,0 +1,228 @@ +{ + "column": "2", + "owner": "", + "demos": [ + { + "demoId": "base", + "name": { + "zh-CN": "基本用法", + "en-US": "Basic Usage" + }, + "desc": { + "zh-CN": "详细用法参考如下示例", + "en-US": "For details, see the following example." + }, + "codeFiles": [ + "base.vue" + ] + }, + { + "demoId": "type", + "name": { + "zh-CN": "类型", + "en-US": "Type" + }, + "desc": { + "zh-CN": "

通过 type 设置不同的类型。可选值:success、warning、info、error,默认值:success 。

\n", + "en-US": "

Set different types through type. The options are success, warning, info, and error. The default value is success.

\n" + }, + "codeFiles": [ + "type.vue" + ] + }, + { + "demoId": "size", + "name": { + "zh-CN": "大尺寸", + "en-US": "Large Size" + }, + "desc": { + "zh-CN": "

通过 size 属性设置不同的尺寸,可选值:nomal、large,默认值:nomal 。

\n", + "en-US": "

Set different sizes through the size attribute. The options are nomal and large. The default value is nomal.

\n" + }, + "codeFiles": [ + "size.vue" + ] + }, + { + "demoId": "title", + "name": { + "zh-CN": "自定义标题", + "en-US": "Custom Title" + }, + "desc": { + "zh-CN": "

size 为 large 时显示标题,可设置 titleslot 自定义标题。默认标题根据设置的 type 显示。

\n", + "en-US": "

When size is set to large, the title is displayed. You can set title or slot to customize the title. The default title is displayed according to the set type.

\n" + }, + "codeFiles": [ + "title.vue" + ] + }, + { + "demoId": "center", + "name": { + "zh-CN": "文字居中", + "en-US": "Center text" + }, + "desc": { + "zh-CN": "

通过 center 属性可使文字显示居中。

\n", + "en-US": "

You can use the center property to center the text.

\n" + }, + "codeFiles": [ + "center.vue" + ] + } + ], + "apis": [ + { + "name": "alert", + "type": "component", + "properties": [ + { + "name": "closable", + "type": "Boolean", + "defaultValue": "该属性的默认值为 true", + "desc": { + "zh-CN": "设置警告是否可以关闭", + "en-US": "Set whether alarms can be disabled." + }, + "demoId": "closable" + }, + { + "name": "icon", + "type": "String , Object", + "defaultValue": "", + "desc": { + "zh-CN": "设置警告的图标,默认会根据 type 值自动使用对应图标", + "en-US": "Set the alarm icon. By default, the corresponding icon is automatically used based on the value of type." + }, + "demoId": "icon" + }, + { + "name": "size", + "type": "String", + "defaultValue": "该属性的默认值为 normal", + "desc": { + "zh-CN": "设置警告的大小 nomal/large, 缺省为 nomal。;该属性的可选值为 nomal / large", + "en-US": "Set the warning size to nomal or large. The default value is nomal. ;The value of this attribute can be nomal or large" + }, + "demoId": "size" + }, + { + "name": "title", + "type": "String", + "defaultValue": "", + "desc": { + "zh-CN": "设置警告的标题,在 size 为 large 时有效,默认根据 type 自动设置", + "en-US": "Set the warning title. This parameter is valid only when size is set to large. By default, the alarm title is automatically set based on type." + }, + "demoId": "title" + }, + { + "name": "type", + "type": "String", + "defaultValue": "该属性的默认值为 success", + "desc": { + "zh-CN": "设置警告的类型;该属性的可选值为 success/warning/info/error", + "en-US": "Set the alarm type. The value of this attribute can be success / warning / info / error" + }, + "demoId": "type" + }, + { + "name": "description", + "type": "String", + "defaultValue": "", + "desc": { + "zh-CN": "设置警告的提示内容,默认为空;", + "en-US": "Set the warning prompt content. The default value is null." + }, + "demoId": "custom-description" + }, + { + "name": "center", + "type": "Boolean", + "defaultValue": "该属性的默认值为 false", + "desc": { + "zh-CN": "文字是否居中", + "en-US": "Whether the text is centered" + }, + "demoId": "center" + }, + { + "name": "close-text", + "type": "String", + "defaultValue": "", + "desc": { + "zh-CN": "关闭按钮自定义文本", + "en-US": "Customized text of the close button" + }, + "demoId": "close-text" + }, + { + "name": "show-icon", + "type": "Boolean", + "defaultValue": "该属性的默认值为 true", + "desc": { + "zh-CN": "是否显示图标", + "en-US": "Display icon" + }, + "demoId": "show-icon" + } + ], + "events": [ + { + "name": "close", + "type": "", + "defaultValue": "", + "desc": { + "zh-CN": "关闭 alert 时触发的事件", + "en-US": "Event triggered when the alert function is disabled" + }, + "demoId": "close-events" + } + ], + "slots": [ + { + "name": "default", + "type": "", + "defaultValue": "", + "desc": { + "zh-CN": "组件默认插槽", + "en-US": "Default slot of the component" + }, + "demoId": "slot-default" + }, + { + "name": "title", + "type": "", + "defaultValue": "", + "desc": { + "zh-CN": "标题的内容", + "en-US": "Title content" + }, + "demoId": "title" + }, + { + "name": "description", + "type": "", + "defaultValue": "", + "desc": { + "zh-CN": "提示内容", + "en-US": "Prompt Content" + }, + "demoId": "custom-description" + }, + { + "name": "close", + "type": "", + "defaultValue": "", + "desc": { + "zh-CN": "自定义关闭按钮,当 closable 属性为 false 时有效", + "en-US": "" + }, + "demoId": "custom-close" + } + ] + } + ] +} \ No newline at end of file diff --git a/examples/react-site/public/@demos/app/button/button-click.vue b/examples/react-site/public/@demos/app/button/button-click.vue new file mode 100644 index 000000000..96ac564b2 --- /dev/null +++ b/examples/react-site/public/@demos/app/button/button-click.vue @@ -0,0 +1,9 @@ + + + diff --git a/examples/react-site/public/@demos/app/button/button-round.vue b/examples/react-site/public/@demos/app/button/button-round.vue new file mode 100644 index 000000000..b747bd02c --- /dev/null +++ b/examples/react-site/public/@demos/app/button/button-round.vue @@ -0,0 +1,8 @@ + + diff --git a/examples/react-site/public/@demos/app/button/button-type.vue b/examples/react-site/public/@demos/app/button/button-type.vue new file mode 100644 index 000000000..58b20d4be --- /dev/null +++ b/examples/react-site/public/@demos/app/button/button-type.vue @@ -0,0 +1,8 @@ + + diff --git a/examples/react-site/public/@demos/app/button/webdoc/button.cn.md b/examples/react-site/public/@demos/app/button/webdoc/button.cn.md new file mode 100644 index 000000000..62ea794f5 --- /dev/null +++ b/examples/react-site/public/@demos/app/button/webdoc/button.cn.md @@ -0,0 +1,16 @@ +--- +title: Button 按钮 +--- + +# Button 按钮 + +
+ +按钮组件一般用于触发一些操作。 + +```typescript +import { Button } from '@opentiny/vue'; +``` + +
+ diff --git a/examples/react-site/public/@demos/app/button/webdoc/button.en.md b/examples/react-site/public/@demos/app/button/webdoc/button.en.md new file mode 100644 index 000000000..8a64f8880 --- /dev/null +++ b/examples/react-site/public/@demos/app/button/webdoc/button.en.md @@ -0,0 +1,8 @@ +--- +title: Button 按钮 +--- + +# Button 按钮 + +
按钮组件一般用于触发一些操作。
+ diff --git a/examples/react-site/public/@demos/app/button/webdoc/button.json b/examples/react-site/public/@demos/app/button/webdoc/button.json new file mode 100644 index 000000000..434c8da1f --- /dev/null +++ b/examples/react-site/public/@demos/app/button/webdoc/button.json @@ -0,0 +1,71 @@ +{ + "column": "2", + "demos": [ + { + "demoId": "button-type", + "name": { + "zh-CN": "按钮类型", + "en-US": "button type" + }, + "desc": { + "zh-CN": "

通过属性type配置按钮类型,包含successinfowarningdanger四种类型。", + "en-US": "

button type

" + }, + "codeFiles": ["button-type.vue"] + }, + { + "demoId": "button-round", + "name": { + "zh-CN": "圆角按钮", + "en-US": "button round" + }, + "desc": { + "zh-CN": "

通过round属性设置按钮是否圆角", + "en-US": "

button round

" + }, + "codeFiles": ["button-round.vue"] + }, + { + "demoId": "button-click", + "name": { + "zh-CN": "事件", + "en-US": "events" + }, + "desc": { + "zh-CN": "

按钮点击事件。", + "en-US": "

bbutton click

" + }, + "codeFiles": ["button-click.vue"] + } + ], + "apis": [ + { + "name": "Button", + "type": "component", + "properties": [ + { + "name": "type", + "type": "primary | success | warning", + "defaultValue": "", + "desc": { + "zh-CN": "

展示按钮不同的状态

", + "en-US": "display different button" + }, + "demoId": "button-type" + } + ], + "events": [ + { + "name": "click", + "type": "", + "defaultValue": "", + "desc": { + "zh-CN": "

点击按钮时触发的回调

", + "en-US": "Click" + }, + "demoId": "button-click" + } + ] + } + ] +} diff --git a/examples/react-site/public/@demos/config.js b/examples/react-site/public/@demos/config.js new file mode 100644 index 000000000..dabcc1730 --- /dev/null +++ b/examples/react-site/public/@demos/config.js @@ -0,0 +1,4 @@ +export default { + isMobile: false, + initApp: (app) => { } +} \ No newline at end of file diff --git a/examples/react-site/public/@demos/overviewimage/button.svg b/examples/react-site/public/@demos/overviewimage/button.svg new file mode 100644 index 000000000..7e443c451 --- /dev/null +++ b/examples/react-site/public/@demos/overviewimage/button.svg @@ -0,0 +1,106 @@ + + + Button按钮 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/react-site/public/@demos/overviewimage/buttongroup.svg b/examples/react-site/public/@demos/overviewimage/buttongroup.svg new file mode 100644 index 000000000..2dd081880 --- /dev/null +++ b/examples/react-site/public/@demos/overviewimage/buttongroup.svg @@ -0,0 +1,138 @@ + + + Buttongroup选块组 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + 2 + + + + + + + + 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/react-site/public/@demos/overviewimage/dev.svg b/examples/react-site/public/@demos/overviewimage/dev.svg new file mode 100644 index 000000000..fa4b5e668 --- /dev/null +++ b/examples/react-site/public/@demos/overviewimage/dev.svg @@ -0,0 +1,76 @@ + + + Modal 弹出框 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 开发中 + + + + + + \ No newline at end of file diff --git a/examples/react-site/public/@demos/webdoc/introduce.md b/examples/react-site/public/@demos/webdoc/introduce.md new file mode 100644 index 000000000..4162f22a2 --- /dev/null +++ b/examples/react-site/public/@demos/webdoc/introduce.md @@ -0,0 +1,7 @@ +--- +title: 介绍 | TinyReact +--- + +# 介绍 + +TinyReact 简介 md文档 \ No newline at end of file diff --git a/examples/react-site/public/@demos/webdoc/menus.js b/examples/react-site/public/@demos/webdoc/menus.js new file mode 100644 index 000000000..483aacb06 --- /dev/null +++ b/examples/react-site/public/@demos/webdoc/menus.js @@ -0,0 +1,43 @@ +// 注意,删除了useFor属性 +// title,label增加英文版,以应对将来的国际化功能 +export const standaloneMenus = [ + { + label: '组件总览', + key: 'overview', + }, +]; + +export const docMenus = [ + { + label: '使用指南', + labelEn: 'Guide', //*********** + key: 'doc_use', + children: [ + { + title: '背景简介', + titleEn: 'Introduce', + key: 'introduce', + }, + ], + }, +]; + +//------------------------------------------------------------------- +export const cmpMenus = [ + { + label: '表单选择', + labelEn: 'Form Selection', + key: 'cmp_formselect', + children: [ + { name: 'Button', nameCn: '按钮', key: 'button' } + ] + }, + { + 'label': '提示组件', + 'labelEn': 'Tips Components', + 'key': 'cmp_tips_components', + 'children': [ + { 'nameCn': '警告', 'name': 'Alert', 'key': 'alert' } + ] + } +]; diff --git a/examples/react-site/public/favicon.ico b/examples/react-site/public/favicon.ico new file mode 100644 index 000000000..826d98acb Binary files /dev/null and b/examples/react-site/public/favicon.ico differ diff --git a/examples/react-site/scripts/build-react.mjs b/examples/react-site/scripts/build-react.mjs new file mode 100755 index 000000000..a9ccdef6f --- /dev/null +++ b/examples/react-site/scripts/build-react.mjs @@ -0,0 +1,75 @@ +import { build, defineConfig } from 'vite4' +import fg from 'fast-glob' +import { getBabelOutputPlugin } from '@rollup/plugin-babel' + +function createEntry() { + const entries = fg.sync( + ['demos/**/*.jsx'] + ) + return entries.reduce((pre, item) => { + pre[ + item + .replace('demos/app/', '') + .replace('.jsx', '') + ] = item + return pre + }, ({})) +} + +const entries = createEntry() + +function prependPlugin(options) { + return { + name: 'prepend-plugin', + generateBundle(_, bundle) { + for (const fileName in bundle) { + const chunk = bundle[fileName]; + if (chunk.isEntry) { + chunk.code = `${options.code}\n${chunk.code}`; + } + } + }, + }; +} + +async function buildReact() { + await build({ + ...defineConfig({ + publicDir: false, + extensions: ['.js', '.ts', '.tsx', '.jsx'], + plugins: [] + }), + configFile: false, + build: { + outDir: './src/webcomps', + emptyOutDir: true, + minify: false, + rollupOptions: { + plugins: [ + getBabelOutputPlugin({ + presets: [['@babel/preset-env', { loose: true, modules: false }]] + }), + prependPlugin({ + code: `import React from 'react'` + }) + ], + output: { + strict: false, + manualChunks: {} + }, + external: [ + /^@pe-3/, + /^@opentiny/, + /^react/ + ] + }, + lib: { + entry: entries, + formats: ['es'], + fileName: (format, entryName) => `${entryName}.js` + } + } + }) +} + +buildReact() \ No newline at end of file diff --git a/examples/react-site/scripts/copy.js b/examples/react-site/scripts/copy.js new file mode 100644 index 000000000..c56f760c9 --- /dev/null +++ b/examples/react-site/scripts/copy.js @@ -0,0 +1,32 @@ +const fs = require('fs-extra'); +const path = require('path'); +const chalk = require('chalk'); + +const baseDir = process.cwd(); +const toRemovefiles = [path.resolve(baseDir, './public/@demos')]; + +toRemovefiles.forEach(file => { + if (fs.pathExistsSync(file)) { + fs.removeSync(file); + } +}); + +const copyfiles = [ + { + // 组件示例源码、组件描述markdown和组件示例配置 + source: 'demos', + target: './public/@demos', + }, +]; + +// 根据传入的参数,同步拷贝相应的文件 +copyfiles.forEach(path => { + try { + fs.copySync(path.source, path.target, (src) => { + return !/.jsx$/.test(src); + }); + console.log(chalk.green(path.source + ' 拷贝完成!')); + } catch (err) { + console.log(chalk.red(err)); + } +}); diff --git a/examples/react-site/src/App.vue b/examples/react-site/src/App.vue new file mode 100644 index 000000000..4972a7575 --- /dev/null +++ b/examples/react-site/src/App.vue @@ -0,0 +1,52 @@ + + diff --git a/examples/react-site/src/assets/custom-block.less b/examples/react-site/src/assets/custom-block.less new file mode 100644 index 000000000..a47355709 --- /dev/null +++ b/examples/react-site/src/assets/custom-block.less @@ -0,0 +1,144 @@ +.markdown-body ul, +.markdown-body ol, +.markdown-body li { + list-style: circle; +} +// 切换md中,这2个类的显示隐藏. used-config used-tiny 类 +.md-config .markdown-body { + .used-tiny { + display: none; + } +} +.md-tiny .markdown-body { + .used-config { + display: none; + } +} +// 表格组件demo区会将其父容器(.n-layout-scroll-container)宽度撑开超出中间内容区,设置此样式限制demo区容器的宽度 +.md-tiny .n-layout-scroll-container { + width: 100%; + scroll-behavior: smooth; +} +.n-code pre { + line-height: 1.5; +} +.markdown-body { + font-size: 15px; + h1, h2, h3 { + border-bottom: none; + } + p { + font-size: 14px; + } + code { + margin: 0 4px; + padding: 0.2em 0.4em; + background: rgba(0, 0, 0, 0.04); + border: 1px solid rgba(5, 5, 5, 0.06); + border-radius: 3px; + } + pre { + background-color: rgba(0, 0, 0, 0.04); + } + a { + color: #5073e5; + &:hover { + color: #69b1ff; + text-decoration: none; + } + } +} + +// 以下是提示块的样式 +.theme-light { + .markdown-body { + --color-tip-bg: #f3f5f7; + --color-tip-fg: #24292f; + --color-tip-bd: #3eaf7c; + --color-tip-title: #24292f; + + --color-info-bg: #f3f5f7; + --color-info-fg: #24292f; + --color-info-bd: #476582; + --color-info-title: #24292f; + + --color-warning-bg: #ffe5644d; + --color-warning-fg: #6b5900; + --color-warning-bd: #e7c000; + --color-warning-title: #b29400; + + --color-danger-bg: #ffe6e6; + --color-danger-fg: #4d0000; + --color-danger-bd: #c00; + --color-danger-title: #900; + } +} +.theme-dark { + .markdown-body { + --color-tip-fg: #d3d5d6; + --color-tip-bg: #24292f; + --color-tip-bd: #3eaf7c; + --color-tip-title: #f3f5f7; + + --color-info-fg: #d3d5d6; + --color-info-bg: #24292f; + --color-info-bd: #476582; + --color-info-title: #f3f5f7; + + --color-warning-fg: #ffe564; + --color-warning-bg: #6b59004d; + --color-warning-bd: #e7c000; + --color-warning-title: #b89e1d; + + --color-danger-fg: #f17070; + --color-danger-bg: #6b0b0b; + --color-danger-bd: #c00; + --color-danger-title: #df8686; + } +} +.markdown-body .custom-block.tip, +.custom-block.info, +.custom-block.warning, +.custom-block.danger { + margin: 1rem 0; + border-left: 0.5rem solid; + padding: 0.1rem 1.5rem; + overflow-x: auto; +} +.markdown-body { + .custom-block.tip { + background-color: var(--color-tip-bg); + color: var(--color-tip-fg); + border-color: var(--color-tip-bd); + .custom-block-title { + color: var(--color-tip-title); + } + } + + .custom-block.info { + background-color: var(--color-info-bg); + color: var(--color-info-fg); + border-color: var(--color-info-bd); + .custom-block-title { + color: var(--color-info-title); + } + } + + .custom-block.warning { + background-color: var(--color-warning-bg); + color: var(--color-warning-fg); + border-color: var(--color-warning-bd); + .custom-block-title { + color: var(--color-warning-title); + } + } + + .custom-block.danger { + background-color: var(--color-danger-bg); + color: var(--color-danger-fg); + border-color: var(--color-danger-bd); + .custom-block-title { + color: var(--color-danger-title); + } + } +} diff --git a/examples/react-site/src/assets/custom-markdown.css b/examples/react-site/src/assets/custom-markdown.css new file mode 100644 index 000000000..abca6acc1 --- /dev/null +++ b/examples/react-site/src/assets/custom-markdown.css @@ -0,0 +1,94 @@ +.markdown-body hr, +.markdown-body hr + h2 { + display: none; +} +.theme-dark .markdown-body { + color-scheme: dark; + --color-prettylights-syntax-comment: #8b949e; + --color-prettylights-syntax-constant: #79c0ff; + --color-prettylights-syntax-entity: #d2a8ff; + --color-prettylights-syntax-storage-modifier-import: #c9d1d9; + --color-prettylights-syntax-entity-tag: #7ee787; + --color-prettylights-syntax-keyword: #ff7b72; + --color-prettylights-syntax-string: #a5d6ff; + --color-prettylights-syntax-variable: #ffa657; + --color-prettylights-syntax-brackethighlighter-unmatched: #f85149; + --color-prettylights-syntax-invalid-illegal-text: #f0f6fc; + --color-prettylights-syntax-invalid-illegal-bg: #8e1519; + --color-prettylights-syntax-carriage-return-text: #f0f6fc; + --color-prettylights-syntax-carriage-return-bg: #b62324; + --color-prettylights-syntax-string-regexp: #7ee787; + --color-prettylights-syntax-markup-list: #f2cc60; + --color-prettylights-syntax-markup-heading: #1f6feb; + --color-prettylights-syntax-markup-italic: #c9d1d9; + --color-prettylights-syntax-markup-bold: #c9d1d9; + --color-prettylights-syntax-markup-deleted-text: #ffdcd7; + --color-prettylights-syntax-markup-deleted-bg: #67060c; + --color-prettylights-syntax-markup-inserted-text: #aff5b4; + --color-prettylights-syntax-markup-inserted-bg: #033a16; + --color-prettylights-syntax-markup-changed-text: #ffdfb6; + --color-prettylights-syntax-markup-changed-bg: #5a1e02; + --color-prettylights-syntax-markup-ignored-text: #c9d1d9; + --color-prettylights-syntax-markup-ignored-bg: #1158c7; + --color-prettylights-syntax-meta-diff-range: #d2a8ff; + --color-prettylights-syntax-brackethighlighter-angle: #8b949e; + --color-prettylights-syntax-sublimelinter-gutter-mark: #484f58; + --color-prettylights-syntax-constant-other-reference-link: #a5d6ff; + --color-fg-default: #c9d1d9; + --color-fg-muted: #8b949e; + --color-fg-subtle: #484f58; + --color-canvas-default: #0d1117; + --color-canvas-subtle: #161b22; + --color-border-default: #30363d; + --color-border-muted: #21262d; + --color-neutral-muted: rgba(110, 118, 129, 0.4); + --color-accent-fg: #58a6ff; + --color-accent-emphasis: #1f6feb; + --color-attention-subtle: rgba(187, 128, 9, 0.15); + --color-danger-fg: #f85149; +} +.theme-light .markdown-body { + color-scheme: light; + --color-prettylights-syntax-comment: #6e7781; + --color-prettylights-syntax-constant: #0550ae; + --color-prettylights-syntax-entity: #8250df; + --color-prettylights-syntax-storage-modifier-import: #24292f; + --color-prettylights-syntax-entity-tag: #116329; + --color-prettylights-syntax-keyword: #cf222e; + --color-prettylights-syntax-string: #0a3069; + --color-prettylights-syntax-variable: #953800; + --color-prettylights-syntax-brackethighlighter-unmatched: #82071e; + --color-prettylights-syntax-invalid-illegal-text: #f6f8fa; + --color-prettylights-syntax-invalid-illegal-bg: #82071e; + --color-prettylights-syntax-carriage-return-text: #f6f8fa; + --color-prettylights-syntax-carriage-return-bg: #cf222e; + --color-prettylights-syntax-string-regexp: #116329; + --color-prettylights-syntax-markup-list: #3b2300; + --color-prettylights-syntax-markup-heading: #0550ae; + --color-prettylights-syntax-markup-italic: #24292f; + --color-prettylights-syntax-markup-bold: #24292f; + --color-prettylights-syntax-markup-deleted-text: #82071e; + --color-prettylights-syntax-markup-deleted-bg: #ffebe9; + --color-prettylights-syntax-markup-inserted-text: #116329; + --color-prettylights-syntax-markup-inserted-bg: #dafbe1; + --color-prettylights-syntax-markup-changed-text: #953800; + --color-prettylights-syntax-markup-changed-bg: #ffd8b5; + --color-prettylights-syntax-markup-ignored-text: #eaeef2; + --color-prettylights-syntax-markup-ignored-bg: #0550ae; + --color-prettylights-syntax-meta-diff-range: #8250df; + --color-prettylights-syntax-brackethighlighter-angle: #57606a; + --color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f; + --color-prettylights-syntax-constant-other-reference-link: #0a3069; + --color-fg-default: #24292f; + --color-fg-muted: #57606a; + --color-fg-subtle: #6e7781; + --color-canvas-default: #ffffff; + --color-canvas-subtle: #f6f8fa; + --color-border-default: #d0d7de; + --color-border-muted: hsla(210, 18%, 87%, 1); + --color-neutral-muted: rgba(175, 184, 193, 0.2); + --color-accent-fg: #0969da; + --color-accent-emphasis: #0969da; + --color-attention-subtle: #fff8c5; + --color-danger-fg: #cf222e; +} diff --git a/examples/react-site/src/assets/images/no-data.svg b/examples/react-site/src/assets/images/no-data.svg new file mode 100644 index 000000000..7f9ddfb71 --- /dev/null +++ b/examples/react-site/src/assets/images/no-data.svg @@ -0,0 +1,141 @@ + + + + 插图/无搜索结果/h150px + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/react-site/src/assets/images/search.svg b/examples/react-site/src/assets/images/search.svg new file mode 100644 index 000000000..ce5d77f61 --- /dev/null +++ b/examples/react-site/src/assets/images/search.svg @@ -0,0 +1,16 @@ + + + \ No newline at end of file diff --git a/examples/react-site/src/assets/index.less b/examples/react-site/src/assets/index.less new file mode 100644 index 000000000..a42bf25c5 --- /dev/null +++ b/examples/react-site/src/assets/index.less @@ -0,0 +1,67 @@ +// 重置代码 +html, +body { + height: 100%; + width: 100%; + scroll-behavior: smooth; + font-size: 12px; +} +* { + box-sizing: border-box; +} +:root { + --white: #fff; + --black: #000; + --main: #2b292d; // 主字体色 + --mainless: #2c2e30; + --second: #7e8085; // 次级字色 + --secondless: lighten(#7e8085, 20%); // 次级字色 + + --light: #f6f8f9; // 非常浅的背景色 + --lightless: #f1f2f3; // 非常浅的 + + --primary: #2f5bea; // 四个主色 + --success: #5073e5; + --warning: #ffbe0e; + --error: #ee343f; + --primaryless: lighten(#2f5bea, 20%); // 四个主色的浅色,比如鼠标移上时 + --successless: lighten(#5073e5, 20%); + --warningless: lighten(#ffbe0e, 20%); + --errorless: lighten(#ee343f, 20%); + + --border-style: solid; + --border-color: #ddd; + --border-width: 1px; + --radius-sm: 5px; + --radius-lg: 30px; + --shadow-sm: 0px 1px 3px 3px RGBA(0, 0, 0, 0.1); + --shadow-lg: 0px 6px 18px 18px RGBA(0, 0, 0, 0.1); + + --text-shadow-sm: rgb(0, 0, 0, 0.4) 0px 1px 3px; + --text-shadow-lg: rgb(0, 0, 0, 0.4) 0px 6px 3px; + + --mask: fade(#000, 45%); + --trans-time: 0.4s; + --trans-time-slow: 1s; + + // echarts 的9种颜色 + --c0: #efefef; + --c1: #5470c6; + --c2: #91cc75; + --c3: #fac858; + --c4: #ee6666; + --c5: #73c0de; + --c6: #3ba272; + --c7: #fc8452; + --c8: #9a60b4; + --c9: #ea7ccc; +} + +.theme-dark { + --lightless: #3d3d3d; // 非常浅的 + --border-color: #6b6a6a; +} + +[draggable='true'] { + user-select: none; +} diff --git a/examples/react-site/src/i18n/en.json b/examples/react-site/src/i18n/en.json new file mode 100644 index 000000000..27970fff9 --- /dev/null +++ b/examples/react-site/src/i18n/en.json @@ -0,0 +1,23 @@ +{ + "dark": "Dark", + "light": "Light", + "searchPlaceholder": "Search", + "home": "Home", + "doc": "Docs", + "component": "Components", + "common": "Common", + "apiPreference": "Framework", + "apiTiny": "Vue", + "yan-shi": "Demo", + "name": "Name", + "propType": "Type", + "defValue": "Default", + "typeValue": "Option Value", + "desc": "Description", + "showCode": "Show Code", + "hideCode": "Hide Code", + "copyCode": "Copy Code", + "doc-owner": "Owner", + "copyCodeOk": "Copy Success", + "frameAngular": "Angular" +} diff --git a/examples/react-site/src/i18n/index.js b/examples/react-site/src/i18n/index.js new file mode 100644 index 000000000..2d26be4c9 --- /dev/null +++ b/examples/react-site/src/i18n/index.js @@ -0,0 +1,16 @@ +import { createI18n } from 'vue-i18n'; + +import { $local } from '../tools'; +import zh from './zh.json'; +import en from './en.json'; +const messages = { enUS: en, zhCN: zh }; +$local._lang = $local._lang !== 'zhCN' && $local._lang !== 'enUS' ? 'zhCN' : $local._lang; +const i18n = createI18n({ + locale: $local._lang, // set locale + fallbackLocale: 'zhCN', // set fallback locale + messages, // set locale messages +}); +const $t = i18n.global.t; +const $t2 = (cn, en) => (i18n.global.locale === 'zhCN' ? cn : en); + +export { i18n, $t, $t2 }; diff --git a/examples/react-site/src/i18n/zh.json b/examples/react-site/src/i18n/zh.json new file mode 100644 index 000000000..e3a443359 --- /dev/null +++ b/examples/react-site/src/i18n/zh.json @@ -0,0 +1,23 @@ +{ + "dark": "深色", + "light": "浅色", + "searchPlaceholder": "搜索", + "home": "首页", + "doc": "文档", + "component": "组件", + "common": "常规", + "apiPreference": "框架选择", + "apiTiny": "Vue", + "yan-shi": "演示", + "name": "名称", + "propType": "类型", + "defValue": "默认值", + "typeValue": "可选值", + "desc": "说明", + "hideCode": "收起代码", + "showCode": "显示代码", + "copyCode": "复制代码", + "doc-owner": "负责人", + "copyCodeOk": "复制成功", + "frameAngular": "Angular" +} diff --git a/examples/react-site/src/main.js b/examples/react-site/src/main.js new file mode 100644 index 000000000..992d1fa8c --- /dev/null +++ b/examples/react-site/src/main.js @@ -0,0 +1,32 @@ +import { createHead } from '@vueuse/head'; +import { createApp } from 'vue'; +import '@unocss/reset/eric-meyer.css'; +// markdown文件内代码高亮 +import 'prismjs/themes/prism.css'; +import 'uno.css'; +// markdown样式引用的是github-markdown.css +import 'github-markdown-css/github-markdown.css'; +import './assets/index.less'; +// 覆盖默认的github markdown样式 +import './assets/custom-markdown.css'; +import './assets/custom-block.less'; +import { i18n } from './i18n/index'; +import { router } from './router'; +import App from './App.vue'; +import { $t, $t2 } from './i18n'; +import { $pub } from './tools'; +import demoConfig from '@demos/config.js'; + +let app = createApp(App); +app.config.performance = true; +app + .use(router) + .use(i18n) + .use(createHead()) // 支持md修改title + .mixin({ methods: { $t, $t2, $pub } }); + +if (typeof demoConfig.initApp === 'function') { + demoConfig.initApp(app); +} + +app.mount('#app'); diff --git a/examples/react-site/src/menus.jsx b/examples/react-site/src/menus.jsx new file mode 100644 index 000000000..4486d36a6 --- /dev/null +++ b/examples/react-site/src/menus.jsx @@ -0,0 +1,54 @@ +import { docMenus, cmpMenus } from '@demos/webdoc/menus.js'; + +import { appData, $t2 } from './tools'; + +/** + * 聚合doc / cmp 两个页面的所有菜单. + * 根据menus,生成总的menuOptions, doc的路由指向docs/:docId, 组件的路由指向components/:cmpId + * 1、docId 必须匹配doc_md中的文件名 , cmpId必须匹配 cmp_md 中的文件名 + */ +const getTo = (route, key) => `${import.meta.env.VITE_CONTEXT}${route}${key}`; +const getTitle = page => `${page.name} ${$t2(page.nameCn, '')}`; + +// 生成所有的菜单 +function genMenus() { + const standaloneOptions = [ + { + key: 'overview', + label: () => ( + + 组件总览 + + ), + }, + ]; + + const docOptions = docMenus.map(menu => ({ + ...menu, + children: menu.children.map(page => ({ + key: page.key, + label: () => ( + + {page.title} + + ), + })), + })); + const cmpOptions = cmpMenus.map(menu => ({ + ...menu, + children: menu.children.map(page => ({ + key: page.key, + label: () => ( + + + {page.name} + {appData.lang === 'zhCN' && {page.nameCn} } + + + ), + })), + })); + return [...standaloneOptions, ...docOptions, ...cmpOptions]; +} + +export { genMenus }; diff --git a/examples/react-site/src/router.js b/examples/react-site/src/router.js new file mode 100644 index 000000000..e45b88226 --- /dev/null +++ b/examples/react-site/src/router.js @@ -0,0 +1,53 @@ +import { createRouter, createWebHistory } from 'vue-router'; +import Layout from '@/views/layout/layout.vue'; +import Components from '@/views/components/components.vue' +import DemoPage from '@/views/components/demoPage.vue' +import Docs from '@/views/docs/docs.vue' +import Overview from '@/views/overview.vue' + +let routes = [ + // 组件总览 + { + path: import.meta.env.VITE_CONTEXT + 'overview', + component: Layout, + name: 'overview', + children: [{ path: '', component: Overview, meta: { title: '组件总览 | TinyVue' } }], + }, + // 文档 + { + path: import.meta.env.VITE_CONTEXT + 'docs/:docId', + component: Layout, + name: 'docs', + children: [{ path: '', component: Docs }], + }, + // 组件 + { + path: import.meta.env.VITE_CONTEXT + 'components/:cmpId', + component: Layout, + name: 'components', + children: [{ path: '', component: Components }], + }, + //单组件示例 + { + path: import.meta.env.VITE_CONTEXT + 'demoPage/:cmpId/:demoId', + component: DemoPage, + name: 'demoPage', + }, + // 未匹配到目标地址时,进行路由重定向 + { + path: '/:pathMatch(.*)*', + redirect: { path: import.meta.env.VITE_CONTEXT + 'overview' }, + }, +]; +const router = createRouter({ + history: createWebHistory(), + routes, +}); + +// 为浏览器添加title +router.afterEach((to, from) => { + if (to.meta.title) { + document.title = to.meta.title; + } +}); +export { router }; diff --git a/examples/react-site/src/tools/appData.js b/examples/react-site/src/tools/appData.js new file mode 100644 index 000000000..3c71a674b --- /dev/null +++ b/examples/react-site/src/tools/appData.js @@ -0,0 +1,29 @@ +import { reactive, computed, watchEffect } from 'vue'; + +import { useAutoStore } from './storage'; +import { useMediaQuery } from './useMediaQuery'; + +const appData = reactive({ + lang: useAutoStore('local', '_lang', 'zhCN'), + theme: useAutoStore('local', '_theme', 'light'), + configMode: false, + configType: computed(() => 'tiny'), + bpState: useMediaQuery([640, 1024, 1280]).matches, // 3点4区间, bp0,bp1,bp2,bp3 +}); +let appFn = { + toggleLang() { + appData.lang = appData.lang === 'zhCN' ? 'enUS' : 'zhCN'; + location.reload(); + }, + toggleTheme() { + appData.theme = appData.theme === 'light' ? 'dark' : 'light'; + }, +}; +// 减少页面处理 +watchEffect(() => { + document.body.classList.remove('theme-light', 'theme-dark'); + document.body.classList.add('theme-' + appData.theme); +}); +// 为了和tiny-vue共享同一个响应变量 +window.appData = appData; +export { appData, appFn }; diff --git a/examples/react-site/src/tools/index.js b/examples/react-site/src/tools/index.js new file mode 100644 index 000000000..b7e62a92f --- /dev/null +++ b/examples/react-site/src/tools/index.js @@ -0,0 +1,4 @@ +export * from './storage.js'; +export * from './utils.js'; +export * from '@/i18n/index'; +export * from './appData.js'; diff --git a/examples/react-site/src/tools/storage.js b/examples/react-site/src/tools/storage.js new file mode 100644 index 000000000..0020a97d6 --- /dev/null +++ b/examples/react-site/src/tools/storage.js @@ -0,0 +1,69 @@ +import { ref, watch } from 'vue'; + +function parse(str) { + if (str === null) return undefined; + const type = str[0]; + const strVal = str.slice(1); + // 对象时,有可能是Date, 否则反解析后统一是对象 + if (type === 'o' || type === 'b') { + let val = JSON.parse(strVal); + return typeof val === 'string' ? new Date(val) : val; + } + if (type === 'n') return +Number(strVal); + if (type === 's') return strVal; +} + +// 带前缀的保存值 +function save(store, k, v) { + const type = typeof v; + store.setItem(k, type === 'object' ? JSON.stringify(v) : v); +} + +/** + * 快速的保存值到 sessionStorage, localStorage. + * 支持基本类型,时间,数组,对象,null,不存在的键值返回undefined 。 + * 不支持:Map,Set, 以及多级串联赋值,比如:$session.obj.name="abcd" + */ +function handler(storage) { + return { + get: function (target, propKey, receiver) { + return storage.getItem(propKey); + }, + set: function (target, propKey, value) { + save(storage, propKey, value); + return true; + }, + }; +} + +/** * 快速读写sessionStorage 示例: $session.abc="shen" */ +const $session = new Proxy({}, handler(sessionStorage)); + +/** * 快速读写 localStorage 示例: $local.abc="shen" */ +const $local = new Proxy({}, handler(localStorage)); + +/** * 全局共享值,刷新即丢失! 示例: $cache.abc="shen" */ +const $cache = {}; + +const typeMatcher = { session: $session, local: $local, api: null }; + +/** + * 用于记录用户行为,并保存到session,local 或api接口(api保存的功能还未实现) + * 示例:useAutoStore("session","key1") + * useAutoStore("session","key2",100) + * useAutoStore("session","key2",$session.key2 || 100) + * @param type 自动存储到的目标 + * @param key 存储时的key + * @param defaultValue 默认值。 + * @returns 响应式ref + */ +const useAutoStore = (type, key, defaultValue) => { + let refVar = ref(typeMatcher[type][key]); + watch(refVar, (curr, prev) => { + typeMatcher[type][key] = curr; + }); + if (typeof refVar.value === 'undefined') refVar.value = defaultValue; + return refVar; +}; + +export { $session, $local, $cache, useAutoStore }; diff --git a/examples/react-site/src/tools/useMediaQuery.js b/examples/react-site/src/tools/useMediaQuery.js new file mode 100644 index 000000000..b2522017d --- /dev/null +++ b/examples/react-site/src/tools/useMediaQuery.js @@ -0,0 +1,29 @@ +import { reactive, nextTick } from 'vue'; + +export function useMediaQuery(breakpoints, onChange) { + let matches = reactive({}); // 格式为: { bp0:false, bp1:true,bp2:false} + // 生成 query 表达式 + let start = 1; + let querys = []; + breakpoints.forEach(bp => { + querys.push(`(min-width:${start}px) and (max-width:${bp - 1}px)`); + start = bp; + }); + querys.push(`(min-width:${start}px)`); + let mqlList = querys.map(q => window.matchMedia(q)); + // 添加所有监听, 通过Idx追踪位置 + mqlList.forEach((mql, idx) => { + matches['bp' + idx] = mql.matches; + matches['range-bp' + idx] = querys[idx]; + mql.fn = ev => { + matches['bp' + idx] = ev.matches; + ev.matches && nextTick(onChange); + }; + mql.addEventListener('change', mql.fn); + }); + function destory() { + mqlList.forEach(mql => mql.removeEventListener('change', mql.fn)); + mqlList = null; + } + return { matches, destory }; +} diff --git a/examples/react-site/src/tools/utils.js b/examples/react-site/src/tools/utils.js new file mode 100644 index 000000000..1bb0016f8 --- /dev/null +++ b/examples/react-site/src/tools/utils.js @@ -0,0 +1,55 @@ +const baseUrl = import.meta.env.BASE_URL; + +/** + * json clone, 会丢失函数等。 + * @param obj 普通对象或reactive对象 + */ +const $clone = target => JSON.parse(JSON.stringify(target)); + +/** + * 将目标字段分隔后,取相应位置的值 + * @example $split("/project/home","/",-1) //取出home + * @param target 目标字符串 + * @param splitor 分隔符 + * @param pos 取数位置,可为-1,-2 + */ +const $split = (target, splitor = '/', pos = 0) => target.split(splitor).slice(pos)[0]; + +/** + * 延时函数 + * @example $delay(300).then(()=>{ }) + */ +const $delay = time => new Promise(resolve => setTimeout(resolve, time)); + +/** + * 空闲函数 + * @example $idle().then(()=>{ }) + */ +const $idle = () => new Promise(resolve => (window.requestIdleCallback || window.requestAnimationFrame)(resolve)); + +const $pub = url => { + // return baseUrl + url; + return `${baseUrl}/${url}`; +}; + +/** + * fetch组件库示例静态文件,包括markdown、示例源码和示例配置 + * @param {string} path + * @returns + */ +const fetchDemosFile = path => { + return fetch(baseUrl + '/' + path, { + method: 'GET', + headers: { + 'Content-Type': 'text/plain;charset=UTF-8', + }, + }).then(res => { + if (res.ok) { + return res.text(); + } else { + throw new Error(res.statusText); + } + }); +}; + +export { $clone, $split, $delay, $idle, $pub, fetchDemosFile }; diff --git a/examples/react-site/src/views/components/cmpConfig.js b/examples/react-site/src/views/components/cmpConfig.js new file mode 100644 index 000000000..06dfef56a --- /dev/null +++ b/examples/react-site/src/views/components/cmpConfig.js @@ -0,0 +1,40 @@ +import { $split } from '@/tools'; + +// 批量导入vue组件示例文件, 进行vue组件示例的渲染 +const vueFiles = import.meta.globEager('@demos/app/**/*.vue'); +const vueComponents = Object.create(null); +for (const path in vueFiles) { + if (Object.prototype.hasOwnProperty.call(vueFiles, path)) { + const cmpId = $split(path, '/', -2); + const key = $split(path, '/', -1); + vueComponents[`${cmpId}/${key}`] = vueFiles[path].default; + } +} + +const languageMap = { + js: 'javascript', + ts: 'javascript', + html: 'html', + vue: 'html', + css: 'css', + less: 'css', + scss: 'css', + sass: 'css', +}; + +const textColor = '#5073e5'; +const borderColor = '#d9dbdd'; +const themeOverrides = { + Tabs: { + tabTextColorActiveLine: textColor, + tabTextColorHoverLine: textColor, + barColor: textColor, + tabBorderColor: borderColor, + }, +}; + +// 只有select组件需要select.faq.cn.md +const faqMdConfig = { + select: true +}; +export { languageMap, themeOverrides, faqMdConfig, vueComponents }; diff --git a/examples/react-site/src/views/components/components.vue b/examples/react-site/src/views/components/components.vue new file mode 100644 index 000000000..7931da2d0 --- /dev/null +++ b/examples/react-site/src/views/components/components.vue @@ -0,0 +1,42 @@ + + + diff --git a/examples/react-site/src/views/components/componentsDetail.vue b/examples/react-site/src/views/components/componentsDetail.vue new file mode 100644 index 000000000..2e21e4260 --- /dev/null +++ b/examples/react-site/src/views/components/componentsDetail.vue @@ -0,0 +1,361 @@ + + + diff --git a/examples/react-site/src/views/components/componentsDetailTab.vue b/examples/react-site/src/views/components/componentsDetailTab.vue new file mode 100644 index 000000000..dda432d04 --- /dev/null +++ b/examples/react-site/src/views/components/componentsDetailTab.vue @@ -0,0 +1,402 @@ + + + diff --git a/examples/react-site/src/views/components/demo.vue b/examples/react-site/src/views/components/demo.vue new file mode 100644 index 000000000..3a5b46999 --- /dev/null +++ b/examples/react-site/src/views/components/demo.vue @@ -0,0 +1,211 @@ + + + diff --git a/examples/react-site/src/views/components/demoPage.vue b/examples/react-site/src/views/components/demoPage.vue new file mode 100644 index 000000000..e9d8ec301 --- /dev/null +++ b/examples/react-site/src/views/components/demoPage.vue @@ -0,0 +1,207 @@ + + + diff --git a/examples/react-site/src/views/docs/docConfig.js b/examples/react-site/src/views/docs/docConfig.js new file mode 100644 index 000000000..385a76cdf --- /dev/null +++ b/examples/react-site/src/views/docs/docConfig.js @@ -0,0 +1,13 @@ +import { $split } from '@/tools/utils'; +// 从npm 仓库的位置,引入所有md. 统一用文件名做key +const docMDs = {}; +const mds = import.meta.globEager('@demos/webdoc/**.md'); + +for (const path in mds) { + if (Object.prototype.hasOwnProperty.call(mds, path)) { + const key = $split(path, '/', -1); + docMDs[key] = mds[path].default; + } +} + +export default docMDs; diff --git a/examples/react-site/src/views/docs/docs.vue b/examples/react-site/src/views/docs/docs.vue new file mode 100644 index 000000000..0bcf6f84b --- /dev/null +++ b/examples/react-site/src/views/docs/docs.vue @@ -0,0 +1,77 @@ + + + diff --git a/examples/react-site/src/views/layout/layout.vue b/examples/react-site/src/views/layout/layout.vue new file mode 100644 index 000000000..c98b0356d --- /dev/null +++ b/examples/react-site/src/views/layout/layout.vue @@ -0,0 +1,122 @@ + + + diff --git a/examples/react-site/src/views/layout/layoutData.js b/examples/react-site/src/views/layout/layoutData.js new file mode 100644 index 000000000..c559bf74b --- /dev/null +++ b/examples/react-site/src/views/layout/layoutData.js @@ -0,0 +1,43 @@ +export function hiddenPanel() { + // 解决滚动页面时,下拉类组件面板与目标元素分离问题。为容器添加scroll事件,滚动页面,关闭下拉面板。 + const demoContainerEle = document.getElementById('doc-layout').getElementsByClassName('n-layout-scroll-container')[0]; + demoContainerEle?.addEventListener('scroll', () => { + const event = new CustomEvent('tiScroll', { bubbles: false, cancelable: true }); + document.dispatchEvent(event); + }); +} + +// 主题色更新 +const backgroundColor = '#e9edfa'; +const textColor = '#5073e5'; +const borderFocus = '1px solid #5073e5'; +const boxShadowFocus = '0 0 0 2px rgba(80,115,229,0.2)'; +export const themeOverrides = { + Menu: { + itemTextColorActive: textColor, + itemColorActive: backgroundColor, + itemTextColorActiveHover: textColor, + itemColorHover: backgroundColor, + itemColorActiveHover: backgroundColor, + itemTextColorChildActive: textColor, + itemTextColorChildActiveHover: textColor, + arrowColorActive: textColor, + arrowColorActiveHover: textColor, + arrowColorChildActive: textColor, + arrowColorChildActiveHover: textColor, + }, + Anchor: { + linkColor: backgroundColor, + linkTextColorHover: textColor, + linkTextColorActive: textColor, + linkTextColorPressed: textColor, + railColorActive: textColor, + }, + Input: { + caretColor: textColor, + borderHover: borderFocus, + borderFocus: borderFocus, + loadingColor: textColor, + boxShadowFocus: boxShadowFocus, + }, +}; diff --git a/examples/react-site/src/views/overview.vue b/examples/react-site/src/views/overview.vue new file mode 100644 index 000000000..52ceb5c75 --- /dev/null +++ b/examples/react-site/src/views/overview.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/examples/react-site/src/webcomps/alert/base.js b/examples/react-site/src/webcomps/alert/base.js new file mode 100644 index 000000000..166c5f437 --- /dev/null +++ b/examples/react-site/src/webcomps/alert/base.js @@ -0,0 +1,104 @@ +import React from 'react' +function _inheritsLoose(subClass, superClass) { + subClass.prototype = Object.create(superClass.prototype); + subClass.prototype.constructor = subClass; + _setPrototypeOf(subClass, superClass); +} +function _wrapNativeSuper(Class) { + var _cache = typeof Map === "function" ? /* @__PURE__ */ new Map() : void 0; + _wrapNativeSuper = function _wrapNativeSuper2(Class2) { + if (Class2 === null || !_isNativeFunction(Class2)) + return Class2; + if (typeof Class2 !== "function") { + throw new TypeError("Super expression must either be null or a function"); + } + if (typeof _cache !== "undefined") { + if (_cache.has(Class2)) + return _cache.get(Class2); + _cache.set(Class2, Wrapper); + } + function Wrapper() { + return _construct(Class2, arguments, _getPrototypeOf(this).constructor); + } + Wrapper.prototype = Object.create(Class2.prototype, { constructor: { value: Wrapper, enumerable: false, writable: true, configurable: true } }); + return _setPrototypeOf(Wrapper, Class2); + }; + return _wrapNativeSuper(Class); +} +function _construct(Parent, args, Class) { + if (_isNativeReflectConstruct()) { + _construct = Reflect.construct.bind(); + } else { + _construct = function _construct2(Parent2, args2, Class2) { + var a = [null]; + a.push.apply(a, args2); + var Constructor = Function.bind.apply(Parent2, a); + var instance = new Constructor(); + if (Class2) + _setPrototypeOf(instance, Class2.prototype); + return instance; + }; + } + return _construct.apply(null, arguments); +} +function _isNativeReflectConstruct() { + if (typeof Reflect === "undefined" || !Reflect.construct) + return false; + if (Reflect.construct.sham) + return false; + if (typeof Proxy === "function") + return true; + try { + Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function() { + })); + return true; + } catch (e) { + return false; + } +} +function _isNativeFunction(fn) { + return Function.toString.call(fn).indexOf("[native code]") !== -1; +} +function _setPrototypeOf(o, p) { + _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf2(o2, p2) { + o2.__proto__ = p2; + return o2; + }; + return _setPrototypeOf(o, p); +} +function _getPrototypeOf(o) { + _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf2(o2) { + return o2.__proto__ || Object.getPrototypeOf(o2); + }; + return _getPrototypeOf(o); +} +import { Alert } from "@pe-3/react"; +import ReactDOM from "react-dom/client"; +function App(props) { + return /* @__PURE__ */ React.createElement("div", null, props.children); +} +var base = /* @__PURE__ */ function(_HTMLElement) { + _inheritsLoose(base2, _HTMLElement); + function base2() { + return _HTMLElement.apply(this, arguments) || this; + } + var _proto = base2.prototype; + _proto.connectedCallback = function connectedCallback() { + ReactDOM.createRoot(this).render(/* @__PURE__ */ React.createElement(App, null, /* @__PURE__ */ React.createElement(Alert, { + description: "type 为默认值 success" + }), /* @__PURE__ */ React.createElement(Alert, { + type: "error", + description: "type 为 error" + }), /* @__PURE__ */ React.createElement(Alert, { + type: "info", + description: "type 为 info" + }), /* @__PURE__ */ React.createElement(Alert, { + type: "warning", + description: "type 为 warning" + }))); + }; + return base2; +}(/* @__PURE__ */ _wrapNativeSuper(HTMLElement)); +export { + base as default +}; diff --git a/examples/react-site/src/webcomps/alert/center.js b/examples/react-site/src/webcomps/alert/center.js new file mode 100644 index 000000000..3028aa2f5 --- /dev/null +++ b/examples/react-site/src/webcomps/alert/center.js @@ -0,0 +1,96 @@ +import React from 'react' +function _inheritsLoose(subClass, superClass) { + subClass.prototype = Object.create(superClass.prototype); + subClass.prototype.constructor = subClass; + _setPrototypeOf(subClass, superClass); +} +function _wrapNativeSuper(Class) { + var _cache = typeof Map === "function" ? /* @__PURE__ */ new Map() : void 0; + _wrapNativeSuper = function _wrapNativeSuper2(Class2) { + if (Class2 === null || !_isNativeFunction(Class2)) + return Class2; + if (typeof Class2 !== "function") { + throw new TypeError("Super expression must either be null or a function"); + } + if (typeof _cache !== "undefined") { + if (_cache.has(Class2)) + return _cache.get(Class2); + _cache.set(Class2, Wrapper); + } + function Wrapper() { + return _construct(Class2, arguments, _getPrototypeOf(this).constructor); + } + Wrapper.prototype = Object.create(Class2.prototype, { constructor: { value: Wrapper, enumerable: false, writable: true, configurable: true } }); + return _setPrototypeOf(Wrapper, Class2); + }; + return _wrapNativeSuper(Class); +} +function _construct(Parent, args, Class) { + if (_isNativeReflectConstruct()) { + _construct = Reflect.construct.bind(); + } else { + _construct = function _construct2(Parent2, args2, Class2) { + var a = [null]; + a.push.apply(a, args2); + var Constructor = Function.bind.apply(Parent2, a); + var instance = new Constructor(); + if (Class2) + _setPrototypeOf(instance, Class2.prototype); + return instance; + }; + } + return _construct.apply(null, arguments); +} +function _isNativeReflectConstruct() { + if (typeof Reflect === "undefined" || !Reflect.construct) + return false; + if (Reflect.construct.sham) + return false; + if (typeof Proxy === "function") + return true; + try { + Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function() { + })); + return true; + } catch (e) { + return false; + } +} +function _isNativeFunction(fn) { + return Function.toString.call(fn).indexOf("[native code]") !== -1; +} +function _setPrototypeOf(o, p) { + _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf2(o2, p2) { + o2.__proto__ = p2; + return o2; + }; + return _setPrototypeOf(o, p); +} +function _getPrototypeOf(o) { + _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf2(o2) { + return o2.__proto__ || Object.getPrototypeOf(o2); + }; + return _getPrototypeOf(o); +} +import { Alert } from "@pe-3/react"; +import ReactDOM from "react-dom/client"; +function App(props) { + return /* @__PURE__ */ React.createElement("div", null, props.children); +} +var center = /* @__PURE__ */ function(_HTMLElement) { + _inheritsLoose(center2, _HTMLElement); + function center2() { + return _HTMLElement.apply(this, arguments) || this; + } + var _proto = center2.prototype; + _proto.connectedCallback = function connectedCallback() { + ReactDOM.createRoot(this).render(/* @__PURE__ */ React.createElement(App, null, /* @__PURE__ */ React.createElement(Alert, { + center: true, + description: "文字居中" + }))); + }; + return center2; +}(/* @__PURE__ */ _wrapNativeSuper(HTMLElement)); +export { + center as default +}; diff --git a/examples/react-site/src/webcomps/alert/size.js b/examples/react-site/src/webcomps/alert/size.js new file mode 100644 index 000000000..be5a123d9 --- /dev/null +++ b/examples/react-site/src/webcomps/alert/size.js @@ -0,0 +1,99 @@ +import React from 'react' +function _inheritsLoose(subClass, superClass) { + subClass.prototype = Object.create(superClass.prototype); + subClass.prototype.constructor = subClass; + _setPrototypeOf(subClass, superClass); +} +function _wrapNativeSuper(Class) { + var _cache = typeof Map === "function" ? /* @__PURE__ */ new Map() : void 0; + _wrapNativeSuper = function _wrapNativeSuper2(Class2) { + if (Class2 === null || !_isNativeFunction(Class2)) + return Class2; + if (typeof Class2 !== "function") { + throw new TypeError("Super expression must either be null or a function"); + } + if (typeof _cache !== "undefined") { + if (_cache.has(Class2)) + return _cache.get(Class2); + _cache.set(Class2, Wrapper); + } + function Wrapper() { + return _construct(Class2, arguments, _getPrototypeOf(this).constructor); + } + Wrapper.prototype = Object.create(Class2.prototype, { constructor: { value: Wrapper, enumerable: false, writable: true, configurable: true } }); + return _setPrototypeOf(Wrapper, Class2); + }; + return _wrapNativeSuper(Class); +} +function _construct(Parent, args, Class) { + if (_isNativeReflectConstruct()) { + _construct = Reflect.construct.bind(); + } else { + _construct = function _construct2(Parent2, args2, Class2) { + var a = [null]; + a.push.apply(a, args2); + var Constructor = Function.bind.apply(Parent2, a); + var instance = new Constructor(); + if (Class2) + _setPrototypeOf(instance, Class2.prototype); + return instance; + }; + } + return _construct.apply(null, arguments); +} +function _isNativeReflectConstruct() { + if (typeof Reflect === "undefined" || !Reflect.construct) + return false; + if (Reflect.construct.sham) + return false; + if (typeof Proxy === "function") + return true; + try { + Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function() { + })); + return true; + } catch (e) { + return false; + } +} +function _isNativeFunction(fn) { + return Function.toString.call(fn).indexOf("[native code]") !== -1; +} +function _setPrototypeOf(o, p) { + _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf2(o2, p2) { + o2.__proto__ = p2; + return o2; + }; + return _setPrototypeOf(o, p); +} +function _getPrototypeOf(o) { + _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf2(o2) { + return o2.__proto__ || Object.getPrototypeOf(o2); + }; + return _getPrototypeOf(o); +} +import { Alert } from "@pe-3/react"; +import ReactDOM from "react-dom/client"; +function App(props) { + return /* @__PURE__ */ React.createElement("div", null, props.children); +} +var size = /* @__PURE__ */ function(_HTMLElement) { + _inheritsLoose(size2, _HTMLElement); + function size2() { + return _HTMLElement.apply(this, arguments) || this; + } + var _proto = size2.prototype; + _proto.connectedCallback = function connectedCallback() { + ReactDOM.createRoot(this).render(/* @__PURE__ */ React.createElement(App, null, /* @__PURE__ */ React.createElement(Alert, { + size: "normal", + description: "size 为 normal" + }), /* @__PURE__ */ React.createElement(Alert, { + size: "large", + title: "size 为 large" + }))); + }; + return size2; +}(/* @__PURE__ */ _wrapNativeSuper(HTMLElement)); +export { + size as default +}; diff --git a/examples/react-site/src/webcomps/alert/title.js b/examples/react-site/src/webcomps/alert/title.js new file mode 100644 index 000000000..728a14675 --- /dev/null +++ b/examples/react-site/src/webcomps/alert/title.js @@ -0,0 +1,103 @@ +import React from 'react' +function _inheritsLoose(subClass, superClass) { + subClass.prototype = Object.create(superClass.prototype); + subClass.prototype.constructor = subClass; + _setPrototypeOf(subClass, superClass); +} +function _wrapNativeSuper(Class) { + var _cache = typeof Map === "function" ? /* @__PURE__ */ new Map() : void 0; + _wrapNativeSuper = function _wrapNativeSuper2(Class2) { + if (Class2 === null || !_isNativeFunction(Class2)) + return Class2; + if (typeof Class2 !== "function") { + throw new TypeError("Super expression must either be null or a function"); + } + if (typeof _cache !== "undefined") { + if (_cache.has(Class2)) + return _cache.get(Class2); + _cache.set(Class2, Wrapper); + } + function Wrapper() { + return _construct(Class2, arguments, _getPrototypeOf(this).constructor); + } + Wrapper.prototype = Object.create(Class2.prototype, { constructor: { value: Wrapper, enumerable: false, writable: true, configurable: true } }); + return _setPrototypeOf(Wrapper, Class2); + }; + return _wrapNativeSuper(Class); +} +function _construct(Parent, args, Class) { + if (_isNativeReflectConstruct()) { + _construct = Reflect.construct.bind(); + } else { + _construct = function _construct2(Parent2, args2, Class2) { + var a = [null]; + a.push.apply(a, args2); + var Constructor = Function.bind.apply(Parent2, a); + var instance = new Constructor(); + if (Class2) + _setPrototypeOf(instance, Class2.prototype); + return instance; + }; + } + return _construct.apply(null, arguments); +} +function _isNativeReflectConstruct() { + if (typeof Reflect === "undefined" || !Reflect.construct) + return false; + if (Reflect.construct.sham) + return false; + if (typeof Proxy === "function") + return true; + try { + Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function() { + })); + return true; + } catch (e) { + return false; + } +} +function _isNativeFunction(fn) { + return Function.toString.call(fn).indexOf("[native code]") !== -1; +} +function _setPrototypeOf(o, p) { + _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf2(o2, p2) { + o2.__proto__ = p2; + return o2; + }; + return _setPrototypeOf(o, p); +} +function _getPrototypeOf(o) { + _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf2(o2) { + return o2.__proto__ || Object.getPrototypeOf(o2); + }; + return _getPrototypeOf(o); +} +import { Alert } from "@pe-3/react"; +import ReactDOM from "react-dom/client"; +function App(props) { + return /* @__PURE__ */ React.createElement("div", null, props.children); +} +var title = /* @__PURE__ */ function(_HTMLElement) { + _inheritsLoose(title2, _HTMLElement); + function title2() { + return _HTMLElement.apply(this, arguments) || this; + } + var _proto = title2.prototype; + _proto.connectedCallback = function connectedCallback() { + ReactDOM.createRoot(this).render(/* @__PURE__ */ React.createElement(App, null, /* @__PURE__ */ React.createElement(Alert, { + size: "large", + title: "通过属性设置自定义 title" + }), /* @__PURE__ */ React.createElement("br", null), /* @__PURE__ */ React.createElement(Alert, { + size: "large", + slots: { + title: function title3() { + return "通过 slot 设置自定义 title"; + } + } + }))); + }; + return title2; +}(/* @__PURE__ */ _wrapNativeSuper(HTMLElement)); +export { + title as default +}; diff --git a/examples/react-site/src/webcomps/alert/type.js b/examples/react-site/src/webcomps/alert/type.js new file mode 100644 index 000000000..43257e1fd --- /dev/null +++ b/examples/react-site/src/webcomps/alert/type.js @@ -0,0 +1,107 @@ +import React from 'react' +function _inheritsLoose(subClass, superClass) { + subClass.prototype = Object.create(superClass.prototype); + subClass.prototype.constructor = subClass; + _setPrototypeOf(subClass, superClass); +} +function _wrapNativeSuper(Class) { + var _cache = typeof Map === "function" ? /* @__PURE__ */ new Map() : void 0; + _wrapNativeSuper = function _wrapNativeSuper2(Class2) { + if (Class2 === null || !_isNativeFunction(Class2)) + return Class2; + if (typeof Class2 !== "function") { + throw new TypeError("Super expression must either be null or a function"); + } + if (typeof _cache !== "undefined") { + if (_cache.has(Class2)) + return _cache.get(Class2); + _cache.set(Class2, Wrapper); + } + function Wrapper() { + return _construct(Class2, arguments, _getPrototypeOf(this).constructor); + } + Wrapper.prototype = Object.create(Class2.prototype, { constructor: { value: Wrapper, enumerable: false, writable: true, configurable: true } }); + return _setPrototypeOf(Wrapper, Class2); + }; + return _wrapNativeSuper(Class); +} +function _construct(Parent, args, Class) { + if (_isNativeReflectConstruct()) { + _construct = Reflect.construct.bind(); + } else { + _construct = function _construct2(Parent2, args2, Class2) { + var a = [null]; + a.push.apply(a, args2); + var Constructor = Function.bind.apply(Parent2, a); + var instance = new Constructor(); + if (Class2) + _setPrototypeOf(instance, Class2.prototype); + return instance; + }; + } + return _construct.apply(null, arguments); +} +function _isNativeReflectConstruct() { + if (typeof Reflect === "undefined" || !Reflect.construct) + return false; + if (Reflect.construct.sham) + return false; + if (typeof Proxy === "function") + return true; + try { + Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function() { + })); + return true; + } catch (e) { + return false; + } +} +function _isNativeFunction(fn) { + return Function.toString.call(fn).indexOf("[native code]") !== -1; +} +function _setPrototypeOf(o, p) { + _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf2(o2, p2) { + o2.__proto__ = p2; + return o2; + }; + return _setPrototypeOf(o, p); +} +function _getPrototypeOf(o) { + _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf2(o2) { + return o2.__proto__ || Object.getPrototypeOf(o2); + }; + return _getPrototypeOf(o); +} +import { Alert } from "@pe-3/react"; +import ReactDOM from "react-dom/client"; +function App(props) { + return /* @__PURE__ */ React.createElement("div", null, props.children); +} +var type = /* @__PURE__ */ function(_HTMLElement) { + _inheritsLoose(type2, _HTMLElement); + function type2() { + return _HTMLElement.apply(this, arguments) || this; + } + var _proto = type2.prototype; + _proto.connectedCallback = function connectedCallback() { + ReactDOM.createRoot(this).render(/* @__PURE__ */ React.createElement(App, null, /* @__PURE__ */ React.createElement(Alert, { + description: "type 为默认值 success" + }), /* @__PURE__ */ React.createElement(Alert, { + type: "info", + description: "type 为 info" + }), /* @__PURE__ */ React.createElement(Alert, { + type: "error", + description: "type 为 error" + }), /* @__PURE__ */ React.createElement(Alert, { + type: "success", + description: "type 为 success" + }), /* @__PURE__ */ React.createElement(Alert, { + type: "warning", + description: "type 为 warning" + }))); + }; + return type2; +}(/* @__PURE__ */ _wrapNativeSuper(HTMLElement)); +export { + type as default +}; diff --git a/examples/react-site/src/webcomps/button/button-click-webcomp.js b/examples/react-site/src/webcomps/button/button-click-webcomp.js new file mode 100644 index 000000000..b095c94b9 --- /dev/null +++ b/examples/react-site/src/webcomps/button/button-click-webcomp.js @@ -0,0 +1,93 @@ +import React from 'react' +function _inheritsLoose(subClass, superClass) { + subClass.prototype = Object.create(superClass.prototype); + subClass.prototype.constructor = subClass; + _setPrototypeOf(subClass, superClass); +} +function _wrapNativeSuper(Class) { + var _cache = typeof Map === "function" ? /* @__PURE__ */ new Map() : void 0; + _wrapNativeSuper = function _wrapNativeSuper2(Class2) { + if (Class2 === null || !_isNativeFunction(Class2)) + return Class2; + if (typeof Class2 !== "function") { + throw new TypeError("Super expression must either be null or a function"); + } + if (typeof _cache !== "undefined") { + if (_cache.has(Class2)) + return _cache.get(Class2); + _cache.set(Class2, Wrapper); + } + function Wrapper() { + return _construct(Class2, arguments, _getPrototypeOf(this).constructor); + } + Wrapper.prototype = Object.create(Class2.prototype, { constructor: { value: Wrapper, enumerable: false, writable: true, configurable: true } }); + return _setPrototypeOf(Wrapper, Class2); + }; + return _wrapNativeSuper(Class); +} +function _construct(Parent, args, Class) { + if (_isNativeReflectConstruct()) { + _construct = Reflect.construct.bind(); + } else { + _construct = function _construct2(Parent2, args2, Class2) { + var a = [null]; + a.push.apply(a, args2); + var Constructor = Function.bind.apply(Parent2, a); + var instance = new Constructor(); + if (Class2) + _setPrototypeOf(instance, Class2.prototype); + return instance; + }; + } + return _construct.apply(null, arguments); +} +function _isNativeReflectConstruct() { + if (typeof Reflect === "undefined" || !Reflect.construct) + return false; + if (Reflect.construct.sham) + return false; + if (typeof Proxy === "function") + return true; + try { + Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function() { + })); + return true; + } catch (e) { + return false; + } +} +function _isNativeFunction(fn) { + return Function.toString.call(fn).indexOf("[native code]") !== -1; +} +function _setPrototypeOf(o, p) { + _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf2(o2, p2) { + o2.__proto__ = p2; + return o2; + }; + return _setPrototypeOf(o, p); +} +function _getPrototypeOf(o) { + _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf2(o2) { + return o2.__proto__ || Object.getPrototypeOf(o2); + }; + return _getPrototypeOf(o); +} +import { Button } from "@pe-3/react"; +import ReactDOM from "react-dom/client"; +function App(props) { + return /* @__PURE__ */ React.createElement("div", null, props.children); +} +var buttonClickWebcomp = /* @__PURE__ */ function(_HTMLElement) { + _inheritsLoose(buttonClickWebcomp2, _HTMLElement); + function buttonClickWebcomp2() { + return _HTMLElement.apply(this, arguments) || this; + } + var _proto = buttonClickWebcomp2.prototype; + _proto.connectedCallback = function connectedCallback() { + ReactDOM.createRoot(this).render(/* @__PURE__ */ React.createElement(App, null, /* @__PURE__ */ React.createElement(Button, null, "默认按钮"))); + }; + return buttonClickWebcomp2; +}(/* @__PURE__ */ _wrapNativeSuper(HTMLElement)); +export { + buttonClickWebcomp as default +}; diff --git a/examples/react-site/src/webcomps/button/button-round-webcomp.js b/examples/react-site/src/webcomps/button/button-round-webcomp.js new file mode 100644 index 000000000..d29fcfe2e --- /dev/null +++ b/examples/react-site/src/webcomps/button/button-round-webcomp.js @@ -0,0 +1,96 @@ +import React from 'react' +function _inheritsLoose(subClass, superClass) { + subClass.prototype = Object.create(superClass.prototype); + subClass.prototype.constructor = subClass; + _setPrototypeOf(subClass, superClass); +} +function _wrapNativeSuper(Class) { + var _cache = typeof Map === "function" ? /* @__PURE__ */ new Map() : void 0; + _wrapNativeSuper = function _wrapNativeSuper2(Class2) { + if (Class2 === null || !_isNativeFunction(Class2)) + return Class2; + if (typeof Class2 !== "function") { + throw new TypeError("Super expression must either be null or a function"); + } + if (typeof _cache !== "undefined") { + if (_cache.has(Class2)) + return _cache.get(Class2); + _cache.set(Class2, Wrapper); + } + function Wrapper() { + return _construct(Class2, arguments, _getPrototypeOf(this).constructor); + } + Wrapper.prototype = Object.create(Class2.prototype, { constructor: { value: Wrapper, enumerable: false, writable: true, configurable: true } }); + return _setPrototypeOf(Wrapper, Class2); + }; + return _wrapNativeSuper(Class); +} +function _construct(Parent, args, Class) { + if (_isNativeReflectConstruct()) { + _construct = Reflect.construct.bind(); + } else { + _construct = function _construct2(Parent2, args2, Class2) { + var a = [null]; + a.push.apply(a, args2); + var Constructor = Function.bind.apply(Parent2, a); + var instance = new Constructor(); + if (Class2) + _setPrototypeOf(instance, Class2.prototype); + return instance; + }; + } + return _construct.apply(null, arguments); +} +function _isNativeReflectConstruct() { + if (typeof Reflect === "undefined" || !Reflect.construct) + return false; + if (Reflect.construct.sham) + return false; + if (typeof Proxy === "function") + return true; + try { + Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function() { + })); + return true; + } catch (e) { + return false; + } +} +function _isNativeFunction(fn) { + return Function.toString.call(fn).indexOf("[native code]") !== -1; +} +function _setPrototypeOf(o, p) { + _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf2(o2, p2) { + o2.__proto__ = p2; + return o2; + }; + return _setPrototypeOf(o, p); +} +function _getPrototypeOf(o) { + _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf2(o2) { + return o2.__proto__ || Object.getPrototypeOf(o2); + }; + return _getPrototypeOf(o); +} +import { Button } from "@pe-3/react"; +import ReactDOM from "react-dom/client"; +function App(props) { + return /* @__PURE__ */ React.createElement("div", null, props.children); +} +var buttonRoundWebcomp = /* @__PURE__ */ function(_HTMLElement) { + _inheritsLoose(buttonRoundWebcomp2, _HTMLElement); + function buttonRoundWebcomp2() { + return _HTMLElement.apply(this, arguments) || this; + } + var _proto = buttonRoundWebcomp2.prototype; + _proto.connectedCallback = function connectedCallback() { + ReactDOM.createRoot(this).render(/* @__PURE__ */ React.createElement(App, null, /* @__PURE__ */ React.createElement(Button, { + round: true, + type: "primary" + }, "主要按钮"))); + }; + return buttonRoundWebcomp2; +}(/* @__PURE__ */ _wrapNativeSuper(HTMLElement)); +export { + buttonRoundWebcomp as default +}; diff --git a/examples/react-site/src/webcomps/button/button-type-webcomp.js b/examples/react-site/src/webcomps/button/button-type-webcomp.js new file mode 100644 index 000000000..ffa971a04 --- /dev/null +++ b/examples/react-site/src/webcomps/button/button-type-webcomp.js @@ -0,0 +1,95 @@ +import React from 'react' +function _inheritsLoose(subClass, superClass) { + subClass.prototype = Object.create(superClass.prototype); + subClass.prototype.constructor = subClass; + _setPrototypeOf(subClass, superClass); +} +function _wrapNativeSuper(Class) { + var _cache = typeof Map === "function" ? /* @__PURE__ */ new Map() : void 0; + _wrapNativeSuper = function _wrapNativeSuper2(Class2) { + if (Class2 === null || !_isNativeFunction(Class2)) + return Class2; + if (typeof Class2 !== "function") { + throw new TypeError("Super expression must either be null or a function"); + } + if (typeof _cache !== "undefined") { + if (_cache.has(Class2)) + return _cache.get(Class2); + _cache.set(Class2, Wrapper); + } + function Wrapper() { + return _construct(Class2, arguments, _getPrototypeOf(this).constructor); + } + Wrapper.prototype = Object.create(Class2.prototype, { constructor: { value: Wrapper, enumerable: false, writable: true, configurable: true } }); + return _setPrototypeOf(Wrapper, Class2); + }; + return _wrapNativeSuper(Class); +} +function _construct(Parent, args, Class) { + if (_isNativeReflectConstruct()) { + _construct = Reflect.construct.bind(); + } else { + _construct = function _construct2(Parent2, args2, Class2) { + var a = [null]; + a.push.apply(a, args2); + var Constructor = Function.bind.apply(Parent2, a); + var instance = new Constructor(); + if (Class2) + _setPrototypeOf(instance, Class2.prototype); + return instance; + }; + } + return _construct.apply(null, arguments); +} +function _isNativeReflectConstruct() { + if (typeof Reflect === "undefined" || !Reflect.construct) + return false; + if (Reflect.construct.sham) + return false; + if (typeof Proxy === "function") + return true; + try { + Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function() { + })); + return true; + } catch (e) { + return false; + } +} +function _isNativeFunction(fn) { + return Function.toString.call(fn).indexOf("[native code]") !== -1; +} +function _setPrototypeOf(o, p) { + _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf2(o2, p2) { + o2.__proto__ = p2; + return o2; + }; + return _setPrototypeOf(o, p); +} +function _getPrototypeOf(o) { + _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf2(o2) { + return o2.__proto__ || Object.getPrototypeOf(o2); + }; + return _getPrototypeOf(o); +} +import { Button } from "@pe-3/react"; +import ReactDOM from "react-dom/client"; +function App(props) { + return /* @__PURE__ */ React.createElement("div", null, props.children); +} +var buttonTypeWebcomp = /* @__PURE__ */ function(_HTMLElement) { + _inheritsLoose(buttonTypeWebcomp2, _HTMLElement); + function buttonTypeWebcomp2() { + return _HTMLElement.apply(this, arguments) || this; + } + var _proto = buttonTypeWebcomp2.prototype; + _proto.connectedCallback = function connectedCallback() { + ReactDOM.createRoot(this).render(/* @__PURE__ */ React.createElement(App, null, /* @__PURE__ */ React.createElement(Button, { + type: "success" + }, "成功按钮"))); + }; + return buttonTypeWebcomp2; +}(/* @__PURE__ */ _wrapNativeSuper(HTMLElement)); +export { + buttonTypeWebcomp as default +}; diff --git a/examples/react-site/tiny-uno/index.js b/examples/react-site/tiny-uno/index.js new file mode 100644 index 000000000..4df7dab19 --- /dev/null +++ b/examples/react-site/tiny-uno/index.js @@ -0,0 +1,56 @@ +// 定制脚本的参数 +const defaultOption = { + prefix: '', + isRem: false, + breakpoints: { + xs: '0px', + sm: '640px', + md: '1024px', + lg: '1280px', + }, +}; +// rules +import border from './rules/border'; +import color from './rules/color'; +import font from './rules/font'; +import layout from './rules/layout'; +import size from './rules/size'; +import transform from './rules/transform'; +import utils from './rules/utils'; +import animate from './rules/animate'; + +// variants +import child from './variants/child'; +import hover from './variants/hover'; +import important from './variants/important'; +import mediaquery from './variants/mediaquery'; +import range from './variants/range'; +import select from './variants/select'; +import prefixBuilder from './variants/prefix'; + +// preflights +import preflights from './preflights'; + +// 每一项 opt={rules:[],shortcuts:[]} +function merge(options, ...rules) { + let ret = { rules: [], shortcuts: [] }; + rules.forEach(ruler => { + let rule = ruler(options); + ret.rules = ret.rules.concat(rule.rules); + ret.shortcuts = ret.shortcuts.concat(rule.shortcuts); + }); + return ret; +} +export default options => { + const tempOptions = { ...defaultOption, ...options }; + let prefix = tempOptions.prefix; + return { + name: 'preset-tinyuno', + ...merge(options, border, color, font, layout, size, transform, utils, animate), + variants: [prefixBuilder(prefix), child, hover, important, mediaquery, range, select], + theme: { + breakpoints: tempOptions.breakpoints, + }, + preflights, + }; +}; diff --git a/examples/react-site/tiny-uno/preflights.js b/examples/react-site/tiny-uno/preflights.js new file mode 100644 index 000000000..5aca1ba5b --- /dev/null +++ b/examples/react-site/tiny-uno/preflights.js @@ -0,0 +1,7 @@ +export default [ + { + getCSS: () => `:root { + + }`, + }, +]; diff --git a/examples/react-site/tiny-uno/rules/animate.js b/examples/react-site/tiny-uno/rules/animate.js new file mode 100644 index 000000000..895cd0293 --- /dev/null +++ b/examples/react-site/tiny-uno/rules/animate.js @@ -0,0 +1,61 @@ +// 所有的动画效果 +import { toEscapedSelector as e } from 'unocss'; +const _s = { + run: 'running', + pause: 'paused', // + + none: 'none', + both: 'both', + stay: 'forwards', + start: 'backwards', + + ease: 'ease', + in: 'ease-in', + out: 'ease-out', + inout: 'ease-in-out', + linear: 'linear', +}; +import * as keyframes from './keyframes/index'; +export default function builder(option) { + return { + rules: [ + // 动画效果及动画影响属性 ani-bounce-width#0px#-100px + // 操作的属性只能是纯数值+单位或无单位。只能操作1个属性 + [ + /^ani-(line|bounce|shake)-(\w+)#(-?\w+)#(-?\w+)$/, + ([, name, prop, from, to], { rawSelector, currentSelector, variantHandlers, theme }) => { + return ` + ${e(currentSelector)}{ + animation-name:${name + prop}; + } + @keyframes ${name + prop} { + ${keyframes[name](prop, from, to)} + } + `; + }, + ], + // 时长 last ani-l#3s ani-last#3s 必填项 + [/^ani-(l|last)#(\d+\.?\d*)s$/, ([, tag, time]) => ({ 'animation-duration': `${time}s` })], + // 延迟 delay ani-d#3s ani-d#-3s ani-delay#-3s 默认0 + [/^ani-(d|delay)#(-?\d+\.?\d*)s$/, ([, tag, time]) => ({ 'animation-delay': `${time}s` })], + // 次数 count ani-c#3 ani-count#3.5 ani-c#infinite ani-c#keep 默认1 小数表示执行一半就结束 + [/^ani-(c|count)#(\d+\.?\d*)$/, ([, tag, time]) => ({ 'animation-iteration-count': `${time}` })], + [/^ani-(c|count)#(infinite|keep)$/, ([, tag]) => ({ 'animation-iteration-count': 'infinite' })], + + // 是否循环往返 ani-round 添加则往返。 默认为: ani-normal 。不添加则每次循环,即每个周期都从头开始 + ['ani-round', { 'animation-direction': 'alternate' }], + ['ani-normal', { 'animation-direction': 'normal' }], + + // 结束时停住位置, ani-stop-stay ani-stop-none 默认为: 结束时,清除动画属性. 有delay时间时, stop-start表示,未开始时,位置于0% + [/^ani-stop-(stay|none|both|start)$/, ([, pos]) => ({ 'animation-fill-mode': `${_s[pos]}` })], + + // 动画函数 ani-fun-ease 默认值 ease + [/^ani-(f|fun|function)-(ease|in|out|inout|linear)$/, ([, tag, pos]) => ({ 'animation-timing-function': `${_s[pos]}` })], + // ani-fun-step4 + [/^ani-(f|fun|function)-step(\d)$/, ([, tag, num]) => ({ 'animation-timing-function': `steps(${num},start)` })], + // 动画开关, ani-run ani-pause 不添加则结束时,清除动画属性 【ani-stop-start还不行】 + [/^ani-(run|pause)$/, ([, pos]) => ({ 'animation-play-state': `${_s[pos]}` })], + ], + shortcuts: [], + }; +} diff --git a/examples/react-site/tiny-uno/rules/border.js b/examples/react-site/tiny-uno/rules/border.js new file mode 100644 index 000000000..2738705f3 --- /dev/null +++ b/examples/react-site/tiny-uno/rules/border.js @@ -0,0 +1,35 @@ +// shortkey的 字典变量 +const _s = { + a: '', // all + r: '-right', + l: '-left', + b: '-bottom', + t: '-top', + + ws: 'var(--border-width) var(--border-style) ', // width & style +}; + +export default function builder(option) { + const isRem = !!option.isRem; + const $t = num => (isRem ? `${num}rem` : `${num}px`); + return { + rules: [ + // 边框 b-a b-b b-r b-t b-l b-a-primary b-a#ff0000 + [/^b-([arlbt])$/, ([, pos]) => ({ [`border${_s[pos]}`]: _s.ws + 'var(--border-color)' })], + [/^b-([arlbt])-(\w+)$/, ([, pos, color]) => ({ [`border${_s[pos]}`]: _s.ws + `var(--${color})` })], + [/^b-([arlbt])#(\w+)$/, ([, pos, color]) => ({ [`border${_s[pos]}`]: _s.ws + `#${color}` })], + // 边框样式 bs-dotted bs-double + [/^bs-(none|dotted|dashed|solid|double|groove|ridge|inset|outset)$/, ([, style]) => ({ 'border-style': style })], + [/^br-(\d+)$/, ([, val]) => ({ 'border-radius': $t(val) })], + // 无边框 nb-a nb-b nb-r nb-t nb-l + [/^nb-([arlbt])$/, ([, pos]) => ({ [`border${_s[pos]}`]: 'none' })], + + ['br-sm', { 'border-radius': 'var(--radius-sm)' }], + ['br-lg', { 'border-radius': 'var(--radius-lg)' }], + ['br-circle', { 'border-radius': '50%' }], + ['bs-sm', { 'box-shadow': 'var(--shadow-sm)' }], + ['bs-lg', { 'box-shadow': 'var(--shadow-lg)' }], + ], + shortcuts: [], + }; +} diff --git a/examples/react-site/tiny-uno/rules/color.js b/examples/react-site/tiny-uno/rules/color.js new file mode 100644 index 000000000..2597ab017 --- /dev/null +++ b/examples/react-site/tiny-uno/rules/color.js @@ -0,0 +1,12 @@ +// shortkey的 字典变量 +const _s = { c: 'color', bg: 'background-color' }; +export default function builder(option) { + return { + rules: [ + // 颜色 #不建议使用 c-black bg-primary c#123456 + [/^(c|bg)-(\w+)$/, ([, attr, color]) => ({ [`${_s[attr]}`]: `var(--${color})` })], + [/^(c|bg)#(\w+)$/, ([, attr, color]) => ({ [`${_s[attr]}`]: `#${color}` })], + ], + shortcuts: [], + }; +} diff --git a/examples/react-site/tiny-uno/rules/font.js b/examples/react-site/tiny-uno/rules/font.js new file mode 100644 index 000000000..962cc7c37 --- /dev/null +++ b/examples/react-site/tiny-uno/rules/font.js @@ -0,0 +1,39 @@ +// shortkey的 字典变量 +const _s = { + f: 'font-size', + lh: 'line-height', + + bold: 'bolder', + thin: 'lighter', + normal: 'normal', + + underline: 'underline', + overline: 'overline', + through: 'line-through', +}; + +export default function builder(option) { + const isRem = !!option.isRem; + const $t = (num) => (isRem ? `${num}rem` : `${num}px`); + return { + rules: [ + // 字体与行高 f12 lh20 + [/^(f|lh)(\d+)$/, ([, attr, num]) => ({ [`${_s[attr]}`]: $t(num) })], + // 字体粗细 fw-bold fw-700 + [/^fw-(bold|thin|normal)$/, ([, dir]) => ({ 'font-weight': `${_s[dir]}` })], + [/^fw-(\d+)$/, ([, val]) => ({ 'font-weight': val })], + // 文字对齐 text-right text-underline text-overline + [/^text-(right|left|center)$/, ([, dir]) => ({ 'text-align': dir })], + // 文字上下划线 solid|double|dotted|dashed|wavy + // text-underline text-overlinewavy text-overline- + [ + /^text-(underline|overline|through)-?(solid|double|dotted|dashed|wavy)?$/, + ([, dir, style]) => ({ 'text-decoration': `${dir} ${style || ''}` }), + ], + // 文字阴影 ts-sm ts-lg + ['ts-sm', { 'text-shadow': 'var(--text-shadow-sm)' }], + ['ts-lg', { 'text-shadow': 'var(--text-shadow-lg)' }], + ], + shortcuts: [], + }; +} diff --git a/examples/react-site/tiny-uno/rules/keyframes/bounce.js b/examples/react-site/tiny-uno/rules/keyframes/bounce.js new file mode 100644 index 000000000..6829cd1fd --- /dev/null +++ b/examples/react-site/tiny-uno/rules/keyframes/bounce.js @@ -0,0 +1,20 @@ +import help from './help'; + +export function bounce(prop, from, to) { + // eslint-disable-next-line no-unused-vars + const { min, max, unit, mid } = help(from, to); + return ` + from,20%,53%,80%,to { + ${prop}: ${from}; + } + 40%,43% { + ${prop}: ${to}; + } + 70%{ + ${prop}: ${mid(50) + unit}; + } + 90%{ + ${prop}: ${mid(13.2) + unit}; + } + `; +} diff --git a/examples/react-site/tiny-uno/rules/keyframes/help.js b/examples/react-site/tiny-uno/rules/keyframes/help.js new file mode 100644 index 000000000..867d18fa6 --- /dev/null +++ b/examples/react-site/tiny-uno/rules/keyframes/help.js @@ -0,0 +1,19 @@ +/** + * + * @param {string} from + * @param {string} to + * @returns Object:{ min,max,unit} + */ +export default function (from, to) { + let min = parseFloat(from); + let max = parseFloat(to); + let unit = from.replace(min.toString(), ''); + + let mid = v => ((max - min) / 100) * v + min; + return { + min, + max, + unit, + mid, + }; +} diff --git a/examples/react-site/tiny-uno/rules/keyframes/index.js b/examples/react-site/tiny-uno/rules/keyframes/index.js new file mode 100644 index 000000000..06617a8c8 --- /dev/null +++ b/examples/react-site/tiny-uno/rules/keyframes/index.js @@ -0,0 +1,3 @@ +export * from './line'; +export * from './bounce'; +export * from './shake'; diff --git a/examples/react-site/tiny-uno/rules/keyframes/line.js b/examples/react-site/tiny-uno/rules/keyframes/line.js new file mode 100644 index 000000000..18be99a71 --- /dev/null +++ b/examples/react-site/tiny-uno/rules/keyframes/line.js @@ -0,0 +1,10 @@ +export function line(prop, from, to) { + return ` +from { + ${prop}: ${from}; +} +to { + ${prop}: ${to}; +} +`; +} diff --git a/examples/react-site/tiny-uno/rules/keyframes/shake.js b/examples/react-site/tiny-uno/rules/keyframes/shake.js new file mode 100644 index 000000000..4d23fae11 --- /dev/null +++ b/examples/react-site/tiny-uno/rules/keyframes/shake.js @@ -0,0 +1,19 @@ +import help from './help'; +/** + * 指定范围的上下10%的幅度,进行摆动 + */ +export function shake(prop, from, to) { + // eslint-disable-next-line no-unused-vars + const { min, max, unit, mid } = help(from, to); + return ` + from,to { + ${prop}: ${from}; + } + 10%,30%,50%,70%,90%{ + ${prop}: ${mid(-10) + unit}; + } + 20%,40%,60%,80% { + ${prop}: ${mid(110) + unit}; + } + `; +} diff --git a/examples/react-site/tiny-uno/rules/layout.js b/examples/react-site/tiny-uno/rules/layout.js new file mode 100644 index 000000000..c5098383e --- /dev/null +++ b/examples/react-site/tiny-uno/rules/layout.js @@ -0,0 +1,55 @@ +// shortkey的 字典变量 +const _s = { + none: 'none', + block: 'block', + inline: 'inline', + flex: 'flex', + grid: 'grid', + ib: 'inline-block', + if: 'inline-flex', + ig: 'inline-grid', + + abs: 'absolute', + rel: 'relative', + fixed: 'fixed', + sticky: 'sticky', + static: 'static', + + r: 'row', + c: 'column', + center: 'center', + start: 'flex-start', + end: 'flex-end', + around: 'space-around', + between: 'space-between', + evenly: 'space-evenly', + stretch: 'stretch', +}; + +export default function builder(option) { + return { + rules: [ + // display d-none, d-block d-flex d-inline d-grid d-ib d-if d-ig + [/^d-(none|block|flex|inline|grid|ib|if|ig)$/, ([, pos]) => ({ display: `${_s[pos]}` })], + // position abs rel fixed sticky static + [/^(rel|abs|fixed|sticky|static)$/, ([, pos]) => ({ position: `${_s[pos]}` })], + // 绝对定位 abs-0 fixed-0 + ['abs-0', { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0 }], + ['fixed-0', { position: 'fixed', left: 0, right: 0, top: 0, bottom: 0 }], + + // flex布局 父元素: f-r f-c f-center f-pos-between f-box-stretch f-wrap + [/^f-([rc])$/, ([, dir]) => ({ display: 'flex', 'flex-direction': `${_s[dir]}` })], + [/^f-pos-(start|center|end|around|between|evenly|stretch)$/, ([, dir]) => ({ 'justify-content': `${_s[dir]}` })], + [/^f-box-(start|center|end|stretch)$/, ([, dir]) => ({ 'align-items': `${_s[dir]}` })], + [/^f-(wrap|nowrap)$/, ([, dir]) => ({ 'flex-wrap': dir })], + + // 子元素: fi-1 fi-4 + [/^fi-(\d+)$/, ([, num]) => ({ flex: `${num}` })], + ], + shortcuts: [ + { + 'f-center': 'd-flex f-pos-center f-box-center', + }, + ], + }; +} diff --git a/examples/react-site/tiny-uno/rules/size.js b/examples/react-site/tiny-uno/rules/size.js new file mode 100644 index 000000000..a02f93e8d --- /dev/null +++ b/examples/react-site/tiny-uno/rules/size.js @@ -0,0 +1,43 @@ +// shortkey的 字典变量 +const _s = { + m: 'margin', + p: 'padding', + + w: 'width', + h: 'height', + + r: '-right', + l: '-left', + b: '-bottom', + t: '-top', +}; + +export default function builder(option) { + const isRem = !!option.isRem; + const $t = num => (isRem ? `${num}rem` : `${num}px`); + return { + rules: [ + // 高宽, 字体,行高 f12 lh20 w200 h200 box78 + [/^(w|h)(\d+)$/, ([, attr, num]) => ({ [`${_s[attr]}`]: $t(num) })], + // 高宽百分比 wp33 hp50 + [/^(w|h)p(\d+)$/, ([, attr, num]) => ({ [`${_s[attr]}`]: `${num}%` })], + + // 内外边距 *可负* m10 mr10 mt-10 mx20 my-10 p10 pr10 + [/^([mp])([rlbt]?)(-?\d+)$/, ([, attr, pos, num]) => ({ [`${_s[attr]}${pos ? _s[pos] : ''}`]: $t(num) })], + // 内外边距auto m-auto mt-auto mx-auto + [/^([mp])([rlbt]?)-auto$/, ([, attr, pos]) => ({ [`${_s[attr]}${pos ? _s[pos] : ''}`]: 'auto' })], + + // abs 定位时 *可负* left0 right50 bottom-20 top-unset + [/^(left|top|right|bottom)(-?\d+)$/, ([, pos, num]) => ({ [`${pos}`]: $t(num) })], + [/^(left|top|right|bottom)-(auto|unset)$/, ([, pos, val]) => ({ [`${pos}`]: val })], + ], + shortcuts: [ + [/^(m|p)x(-?\d+)$/, ([, t, c]) => `${t}l${c} ${t}r${c}`], + [/^(m|p)y(-?\d+)$/, ([, t, c]) => `${t}t${c} ${t}b${c}`], + + [/^(m|p)x-auto$/, ([, t, c]) => `${t}l-auto ${t}r-auto`], + [/^(m|p)y-auto$/, ([, t, c]) => `${t}t-auto ${t}b-auto`], + [/^box(\d+)$/, ([, w]) => `w${w} h${w}`], + ], + }; +} diff --git a/examples/react-site/tiny-uno/rules/transform.js b/examples/react-site/tiny-uno/rules/transform.js new file mode 100644 index 000000000..41e9671d1 --- /dev/null +++ b/examples/react-site/tiny-uno/rules/transform.js @@ -0,0 +1,32 @@ +// shortkey的 字典变量 +const _s = { c: 'center', l: 'left', t: 'top', r: 'right', b: 'bottom' }; + +export default function builder(option) { + const isRem = !!option.isRem; + const $t = num => (isRem ? `${num}rem` : `${num}px`); + return { + rules: [ + // 启用过渡 trans + ['trans', { transition: 'all var(--trans-time)' }], + // transform中心点 to-c to-lt to-t + [/^to-([cltrb])([cltrb]?)$/, ([, dir1, dir2]) => ({ 'transform-origin': `${_s[dir1]} ${dir2 ? _s[dir2] : ''}` })], + + // 旋转 【可负可小数】 tr15 tr45.25 tr-45.25 + [/^tr(-?\d+\.?\d*)$/, ([, ang]) => ({ transform: `rotateZ(${ang}deg)` })], + + // 缩放 【可小数】 ts2 ts3.5 h:ts3.5 + [/^ts(\d+\.?\d*)$/, ([, ang]) => ({ transform: `scale(${ang})` })], // 匹配小数 + + // 平移 【可负可小数】 tt-10 tt-10#10 tt-10.5#12.5 + [ + /^tt(-?\d+\.?\d*)(#-?\d+\.?\d*)?$/, + ([, x, y]) => { + let target; + if (y) target = y.slice(1); // 去# + return { transform: `translate(${$t(x)},${target ? $t(target) : '0'})` }; + }, + ], + ], + shortcuts: [], + }; +} diff --git a/examples/react-site/tiny-uno/rules/utils.js b/examples/react-site/tiny-uno/rules/utils.js new file mode 100644 index 000000000..afad034c0 --- /dev/null +++ b/examples/react-site/tiny-uno/rules/utils.js @@ -0,0 +1,45 @@ +// shortkey的 字典变量 +const _s = { + def: 'default', + hand: 'pointer', + disable: 'not-allowed', +}; +const clamp = (v, min, max) => Math.min(Math.max(v, min), max); + +export default function builder(option) { + return { + rules: [ + // 省略号 ellipsis ellipsis2 ellipsis3 + ['ellipsis', { overflow: 'hidden', ' text-overflow': 'ellipsis', 'white-space': 'nowrap' }], + [ + /^ellipsis(\d+)$/, + ([, num]) => ({ + display: '-webkit-box', + overflow: 'hidden', + '-webkit-line-clamp': num, + ' -webkit-box-orient': 'vertical', + 'overflow-wrap': 'anywhere', + }), + ], + // 光标样式 cur-hand + [/^cur-(def|hand|disable)$/, ([, shape]) => ({ cursor: `${_s[shape]}` })], + // overflow of-auto ofx-hidden ofy-scroll + [/^of(\w?)-(auto|scroll|hidden|visible)$/, ([, axis, mode]) => ({ [`overflow${axis ? '-' + axis : ''}`]: mode })], + // 图片填充 img-cover + [/^img-(cover|contain|fill)$/, ([, mode]) => ({ 'object-fit': mode })], + // z-index z100 + [/^z(-?\d+)$/, ([, num]) => ({ 'z-index': num })], + // 选择相关 noselect allselect nomouse 参见选择变体: select:c-primary select:bg-dark + ['noselect', { 'user-select': 'none' }], + ['allselect', { 'user-select': 'all' }], + ['noevent', { 'pointer-events': 'none' }], + // 可见性 hide show + ['hide', { visibility: 'hidden' }], + ['show', { visibility: 'visible' }], + // 透明度 op100 + [/^op(\d+)$/, ([, val]) => ({ opacity: clamp(val / 100, 0, 1) })], + ['decoration-none', { 'text-decoration': 'none' }], + ], + shortcuts: [], + }; +} diff --git a/examples/react-site/tiny-uno/variants/child.js b/examples/react-site/tiny-uno/variants/child.js new file mode 100644 index 000000000..05e22209e --- /dev/null +++ b/examples/react-site/tiny-uno/variants/child.js @@ -0,0 +1,9 @@ +// "child:" child:f16 child:box32 +export default (matcher) => { + if (!matcher.startsWith('child:')) return matcher; + return { + matcher: matcher.slice(6), + selector: (s) => `${s}>*`, + body: (s)=>s + }; +}; diff --git a/examples/react-site/tiny-uno/variants/hover.js b/examples/react-site/tiny-uno/variants/hover.js new file mode 100644 index 000000000..b055551b9 --- /dev/null +++ b/examples/react-site/tiny-uno/variants/hover.js @@ -0,0 +1,12 @@ +// hover: h:c-error +export default (matcher) => { + if (!matcher.startsWith('h:')) return matcher; + return { + matcher: matcher.slice(2), + selector: (s) => `${s}:hover`, + body: (body) => { + body.push(['transition', 'all var(--trans-time)']); + return body; + }, + }; +}; diff --git a/examples/react-site/tiny-uno/variants/important.js b/examples/react-site/tiny-uno/variants/important.js new file mode 100644 index 000000000..939efa65b --- /dev/null +++ b/examples/react-site/tiny-uno/variants/important.js @@ -0,0 +1,13 @@ +// ! !c-error +export default matcher => { + if (!matcher.startsWith('!')) return matcher; + return { + matcher: matcher.slice(1), + body: body => { + body.forEach(e => { + e[1] && (e[1] += ' !important'); + }); + return body; + }, + }; +}; diff --git a/examples/react-site/tiny-uno/variants/mediaquery.js b/examples/react-site/tiny-uno/variants/mediaquery.js new file mode 100644 index 000000000..9c8f57fa6 --- /dev/null +++ b/examples/react-site/tiny-uno/variants/mediaquery.js @@ -0,0 +1,14 @@ +// mediaQuery , xs:w120 sm:w60 md:w80 lg:w120 md:w240 lg:w480 +export default (matcher, context) => { + const reg = '^(xs|sm|md|lg):'; + const [, mediaQuery = ''] = matcher.match(new RegExp(reg)) ?? []; + + if (!mediaQuery) { + return matcher; + } + let bp = context.theme.breakpoints; + return { + matcher: matcher.replace(/^.+?:(.*)$/, '$1'), + parent: `@media (min-width: ${bp[mediaQuery]})`, + }; +}; diff --git a/examples/react-site/tiny-uno/variants/prefix.js b/examples/react-site/tiny-uno/variants/prefix.js new file mode 100644 index 000000000..2be1a8d0d --- /dev/null +++ b/examples/react-site/tiny-uno/variants/prefix.js @@ -0,0 +1,8 @@ +// 这是一个特殊的变体。 当用户需要使用缀时,传入前缀,返回一个变体函数 +export default prefix => matcher => { + if (!matcher.startsWith(prefix)) return 'nomatch'; + return { + matcher: matcher.slice(prefix.length), + selector: s => `${s}`, + }; +}; diff --git a/examples/react-site/tiny-uno/variants/range.js b/examples/react-site/tiny-uno/variants/range.js new file mode 100644 index 000000000..a4d92515e --- /dev/null +++ b/examples/react-site/tiny-uno/variants/range.js @@ -0,0 +1,12 @@ +// range: 配合 range 使用 range:c-error +export default (matcher) => { + if (!matcher.startsWith('range:')) return matcher; + return { + matcher: matcher.slice(6), + selector: (s) => `.range:hover ${s}`, + body: (body) => { + body.push(['transition', 'all var(--trans-time)']); + return body; + }, + }; +}; diff --git a/examples/react-site/tiny-uno/variants/select.js b/examples/react-site/tiny-uno/variants/select.js new file mode 100644 index 000000000..9dfb4f920 --- /dev/null +++ b/examples/react-site/tiny-uno/variants/select.js @@ -0,0 +1,14 @@ +// select:c-primary select:bg-black ::selection只可以应用于少数的CSS属性:color, background +export default matcher => { + if (!matcher.startsWith('select:')) return matcher; + return { + matcher: matcher.slice(7), + selector: s => `${s} ::selection`, + body: body => { + body.forEach(e => { + e[1] && (e[1] += '!important'); + }); + return body; + }, + }; +}; diff --git a/examples/react-site/tiny.config.js b/examples/react-site/tiny.config.js new file mode 100644 index 000000000..b8a2c21da --- /dev/null +++ b/examples/react-site/tiny.config.js @@ -0,0 +1,13 @@ +module.exports = { + toolkit: '@opentiny/tiny-toolkit-docs', + tasks: { + start: [ + // 此处配置tiny start的前置执行命令,会在套件的start命令前执行 + // { command: 'node scripts/test.js' } + ], + build: [ + // 此处配置tiny build的前置执行命令,会在套件的build命令前执行 + // { command: 'node scripts/test.js' } + ], + }, +}; diff --git a/examples/react-site/tsconfig.json b/examples/react-site/tsconfig.json new file mode 100644 index 000000000..1b9567171 --- /dev/null +++ b/examples/react-site/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "allowJs": true , + "noImplicitAny": false, + "baseUrl": ".", + "paths": { + "@opentiny/react-*": [ + "packages/react-*", + "packages/react/src/*" + ], + "@opentiny/vue-renderless*": [ + "packages/renderless/src*" + ] + }, + "types": [ + "node", + "vite/client" + ], + "jsx": "react-jsx" + }, + "include": [ + "packages/**/*.ts", + "packages/**/*.tsx", + "packages/**/*.jsx", + ], + "exclude": [ + "**/node_modules", + "**/dist*", + "**/*.md" + ] +} \ No newline at end of file diff --git a/examples/react-site/uno.config.js b/examples/react-site/uno.config.js new file mode 100644 index 000000000..f6c62910b --- /dev/null +++ b/examples/react-site/uno.config.js @@ -0,0 +1,35 @@ +import UnocssIcons from '@unocss/preset-icons'; +import presetTinyUno from './tiny-uno/index'; + +export default { + include: [/\.js$/, /\.ts$/, /\.vue$/, /\.html$/, /\.jsx$/, /\.tsx$/], // 增加js ,ts扫描 + presets: [ + presetTinyUno({ + isRem: false, + prefix: '', + // breakpoints: { + // xs: '0px', + // sm: '100px', + // md: '204px', + // lg: '3001px', + // }, + }), + // 非常多的图标,默认可以引用,https://icones.js.org/ + UnocssIcons({ + prefix: 'i-', + extraProperties: { + display: 'inline-block', + }, + collections: { + ti: { + copy: '', + code: '', + codeslash: + '', + check: + '', + }, + }, + }), + ], +}; diff --git a/examples/react-site/vite.config.js b/examples/react-site/vite.config.js new file mode 100644 index 000000000..7708ea8d1 --- /dev/null +++ b/examples/react-site/vite.config.js @@ -0,0 +1,63 @@ +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; +import Unocss from 'unocss/vite'; +import createHtmlPlugin from 'vite-plugin-html'; +import path from 'path'; +import UnoCssConfig from './uno.config'; +import AutoComponents from 'unplugin-vue-components/vite'; +import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'; +import vueJsx from '@vitejs/plugin-vue-jsx'; +import Markdown from 'vite-plugin-md'; +import { MdExt, mdInstall } from './md.extend.config'; + +export default defineConfig({ + envDir: './env', + base: '/tiny-react', + plugins: [ + vue({ + include: [/\.vue$/, /\.md$/], + }), + vueJsx({ + include: [/\.js$/, /\.jsx$/, /\.ts$/, /\.tsx$/], + }), + createHtmlPlugin(), + // 支持md转为vue组件: https://github.com/antfu/vite-plugin-md#configuration--options + Markdown({ + headEnabled: true, + markdownItOptions: { + html: true, + linkify: true, + typographer: true, + }, + markdownItSetup(md) { + mdInstall(md); + }, + markdownItUses: MdExt, + }), + Unocss(UnoCssConfig), + // 自动导入和项目组件 https://github.com/antfu/unplugin-vue-components#configuration + AutoComponents({ + resolvers: [NaiveUiResolver()], + extensions: ['vue', 'md'], + include: [/\.vue$/, /\.vue\?vue/, /\.md$/], + }), + ], + define: { + 'process.env': { ...process.env }, + }, + resolve: { + alias: { + '@': path.resolve('src'), + '@demos': path.resolve('demos'), + '@demo': path.resolve('src/views/components/demo.vue') + }, + }, + server: { + host: '0.0.0.0', + port: 3101, + fs: { + strict: false, + allow: ['..'], + }, + }, +}); diff --git a/internals/cli/package.json b/internals/cli/package.json index 39ae83917..6cd6e2cea 100644 --- a/internals/cli/package.json +++ b/internals/cli/package.json @@ -5,9 +5,9 @@ "version": "1.0.5-mf.0", "description": "internal-cli", "devDependencies": { - "@opentiny-internal/unplugin-virtual-template": "workspace:*", "@babel/core": "^7.5.5", "@babel/preset-env": "7.16.7", + "@opentiny-internal/unplugin-virtual-template": "workspace:*", "@rollup/plugin-babel": "^6.0.3", "@rollup/plugin-commonjs": "^24.0.1", "@types/fs-extra": "^11.0.1", @@ -46,13 +46,18 @@ "sync-icons": "esno src/commands/create/sync-icons.ts", "create:icon-saas": "esno src/index.ts create:icon-saas", "clean:build": "esno src/commands/clean.ts", - "create:mapping": "esno src/commands/create/create-mapping.ts" + "create:mapping": "esno src/commands/create/create-mapping.ts", + "build:entry-react": "esno src/index.ts build:entry-react", + "create:mapping-react": "esno src/commands/create/create-mapping-react.ts", + "build:react": "esno src/index.ts build:react" }, "dependencies": { "@vue/babel-helper-vue-jsx-merge-props": "^1.4.0", "esno": "^0.16.3", "fast-glob": "^3.2.12", + "rollup-plugin-replace": "^2.2.0", "vite-plugin-dts": "^3.0.0", + "vite-plugin-svgr": "^3.2.0", "vite-svg-loader": "^3.6.0" } } diff --git a/internals/cli/src/commands/build/build-entry-react.ts b/internals/cli/src/commands/build/build-entry-react.ts new file mode 100644 index 000000000..d87f73ece --- /dev/null +++ b/internals/cli/src/commands/build/build-entry-react.ts @@ -0,0 +1,103 @@ +/** + * 生成入口文件,包括 pc.js / mobile.js / mobile-first.js / index.js + */ +import fs from 'fs-extra' +import { EOL as endOfLine } from 'node:os' +import { + pathFromWorkspaceRoot, + capitalizeKebabCase, + prettierFormat, + logGreen +} from '../../shared/utils' +import { getAllModules } from './build-ui-react' +import handlebarsRender from './handlebars.render' + +const version = (({ key }) => { + const packageJSON = fs.readJSONSync(pathFromWorkspaceRoot('packages/react/package.json')) + const packageJsonOption = packageJSON[key] || packageJSON + + return packageJsonOption +})({ key: 'version' }) + +const outputDir = 'packages/react' + +const fileNames = { + all: 'index.ts', + pc: 'pc.ts', + mobile: 'mobile.ts', + 'mobile-first': 'mobile-first.ts' +} + +function getMainTemplate({ mode }) { + return `{{{include}}} + import { $prefix } from '@opentiny/react-common' + + const components = [{{{components}}}] + + export const version = '${version}' + + export { + {{{components}}} + } + + export default { + {{{components}}} + } as any + ` +} + +function getComponents(mode) { + const modules = getAllModules() + + const components = modules + .filter((item) => item.type === 'component') + .filter((item) => mode === 'all' || !item.mode || item.mode.includes(mode)) + + return components +} + +function createEntry(mode) { + const OUTPUT_PATH = pathFromWorkspaceRoot(outputDir, fileNames[mode]); + const MAIN_TEMPLATE = getMainTemplate({ mode }) + const includeTemplate: string[] = [] + const componentsTemplate: string[] = [] + const components = getComponents(mode) + const PKG_PATH = pathFromWorkspaceRoot(outputDir, 'package.json') + const PKGContent = fs.readJSONSync(PKG_PATH) + const PKGDeps = { + '@opentiny/react-common': 'workspace:~' + } + + components.forEach((item) => { + const component = capitalizeKebabCase(item.name) + PKGDeps[item.importName] = 'workspace:~' + componentsTemplate.push(` ${component}`) + const importName = mode === 'all' ? item.importName : `${item.importName}/src/${mode}` + includeTemplate.push(`import ${item.name} from '${importName}'`) + }) + + if (mode === 'all') { + PKGContent.dependencies = PKGDeps + fs.writeFileSync(PKG_PATH, JSON.stringify(PKGContent, null, 2)) + } + + const template = handlebarsRender({ + template: MAIN_TEMPLATE, + data: { + include: includeTemplate.join(endOfLine), + components: componentsTemplate.join(',' + endOfLine) + } + }) + + const output = prettierFormat({ str: template }) + + fs.writeFileSync(OUTPUT_PATH, output) +} + +export function buildEntryReact() { + ['all', 'pc', 'mobile', 'mobile-first'].forEach(createEntry) + + logGreen( + `npm run build:entry done. [${outputDir}/index.ts,${outputDir}/pc.ts,${outputDir}/mobile.ts,${outputDir}/mobile-first.ts]` + ) +} diff --git a/internals/cli/src/commands/build/build-ui-react.ts b/internals/cli/src/commands/build/build-ui-react.ts new file mode 100644 index 000000000..8a1a1000b --- /dev/null +++ b/internals/cli/src/commands/build/build-ui-react.ts @@ -0,0 +1,518 @@ +import { logGreen, kebabCase, capitalizeKebabCase } from '../../shared/utils' +import { pathFromWorkspaceRoot } from '../../shared/utils' +import fg from 'fast-glob' +import path from 'node:path' +import { build, defineConfig } from 'vite' +import { getBabelOutputPlugin } from '@rollup/plugin-babel' +import { external } from '../../shared/config' +import type { Plugin, NormalizedOutputOptions, OutputBundle } from 'rollup' +import fs from 'fs-extra' +import { sync as findUpSync } from 'find-up' +import svgr from 'vite-plugin-svgr' +import { requireModules } from './build-ui' +import replace from 'rollup-plugin-replace' + +const moduleMap = require(pathFromWorkspaceRoot('packages/modules-react.json')) +type mode = 'pc' | 'mobile' | 'mobile-first' + +const pathFromPackages = (...args) => pathFromWorkspaceRoot('packages', ...args) + +let scopeName = '@opentiny' +let buildVersion = '1.0.0' + +export interface Module { + /** 源码路径,如 vue/src/button/index.ts */ + path: string + /** 模块类型,可选 component, template, module */ + type: 'component' | 'template' | 'module' + /** 是否排除构建,例如组件尚未开发完,设置 true */ + exclude?: boolean + /** 组件类型支持的模式 */ + mode?: mode[] + /** 模块名称,如 Button */ + name: string + /** 模块构建物路径,如 vue/button/lib/index */ + libPath: string + /** 模块名,如 @opentiny/vue/vue/lib/button/src,@deprecated */ + libName?: string + /** 模块的npm包名,如 @opentiny/vue-button */ + importName: string + /** 构建物文件名,@deprecated */ + tmpName?: string + /** 全局变量名,如 TinyButton */ + global?: string + /** 组件名的大写形态,如 Button */ + UpperName?: string + /** 组件名的小写形态,如 button */ + LowerName?: string + /** 模块的路径 */ + parentDir?: string[] +} + +function getEntryTasks(): Module[] { + // 读取TinyVue组件库入口文件 + return ['index', 'pc', 'mobile'].map((mode) => ({ + path: `react/${mode}.ts`, + dtsRoot: true, + libPath: `react/${mode}`, + type: 'module', + name: kebabCase({ str: `${scopeName}/react` }), + global: capitalizeKebabCase('opentinyReact'), + importName: `${scopeName}/react` + })) +} + +export function getAllModules(): Module[] { + return getSortModules({ filterIntercept: () => true }) +} + +const getSortModules = ({ filterIntercept }: { filterIntercept: Function; }) => { + let modules: Module[] = [] + let componentCount = 0 + const importName = `${scopeName}/react` + Object.entries(moduleMap).forEach(([key, module]) => { + let component = module as Module + + component.name = key + // filterIntercept过滤筛选命令行传过来的组件名称,只输出命令行传递过来的组件 + if (filterIntercept(component) === true && component.exclude !== true) { + const dirs = component.path.split('/') + + // 这段逻辑暂时没有用到 + const componentName = dirs.slice(1, dirs.indexOf('src')) + // UpperName: Todo + component.UpperName = capitalizeKebabCase(componentName.pop() ?? '') + + // LowerName: todo + component.LowerName = kebabCase({ str: component.UpperName }) + + // 工程的父文件夹 + component.parentDir = componentName + + // libPath: 'packages/todo/dist/pc.ts' 组件输出路径 + component.libPath = component.path + .replace('react/src/', 'packages/') + .replace('react-common/src/', 'packages/common/') + .replace('react-locale/src/', 'packages/locale/') + .replace('react-icon/src/', 'packages/icon/') + .replace('/index.ts', '/src/index.js') + .replace('/src/', '/dist/lib/') + .replace('.jsx', '.js') + .replace('.tsx', '.js') + + // libName: '@opentiny/vue/todo/pc' + component.libName = component.libPath + .replace('packages/', '') + .replace('/index', '') + .replace('.js', '') + .replace('/dist/', '/') + .replace(/\/lib$/, '') + + // 处理子目录 + if (componentName.length) { + component.libName = component.libName.replace(componentName.join('/'), '').replace(/^\//, '') + } + + // importName: '@opentiny/vue-tag/pc' + component.importName = `${scopeName}/react` + '-' + component.libName + + // libName: '@opentiny/vue/todo/pc' + component.libName = importName + '/' + component.libName + + // tmpName: 'pc' + component.tmpName = component.libPath.replace('.ts', '').split('/').slice(-1)[0] + + // global: 'TinyTodoPc' + component.global = 'Tiny' + key + + component.importName = `${scopeName}/react-${kebabCase({ str: key })}` + + // "vue-common/src/index.ts" ==> "vue-common/lib/index" + if (component.type === 'module') { + component.libPath = component.path.replace('/src/', '/lib/').replace('index.ts', 'index') + } + + // "vue/src/button/index.ts" ==> "button/lib/index" + if (component.type === 'component') { + component.libPath = component.path.replace('react/src/', '').replace('/index.ts', '/lib/index') + } + + // "vue/src/button/src/mobile-first.vue" ==> "button/lib/mobile-first" + if (component.type === 'template') { + component.libPath = component.path.replace('react/src/', '').replace('/src/', '/lib/').replace(/\..+$/, '') + } + + modules.push(component) + } + + component.type === 'component' && componentCount++ + }) + + return modules +} + +/** + * 根据指定条件搜索原始模块列表 + * @private + * @param {Function} filterIntercept 搜索条件 + */ +const getModules = (filterIntercept: Function) => { + let modules = {} + + if (typeof filterIntercept === 'function') { + for (const key in moduleMap) { + const component = moduleMap[key] + + if (filterIntercept(component) === true && component.exclude !== true) { + modules[key] = component + } + } + } else { + modules = moduleMap + } + + return modules +} + +const getByName = ({ + name, + isSort = true, + inversion = false, + isOriginal = false +}: { + name: string + isSort: boolean + inversion?: boolean + isOriginal?: boolean +}) => { + const callback = (item) => { + const result = new RegExp(`/${name}/|^react-${name}/`).test(item.path) + return inversion ? !result : result + } + + return isOriginal ? getModules(callback) : getSortModules({ filterIntercept: callback }) +} + +function getTasks(names: string[]) { + // 没有指定组件,则全量构建 + if (names.length === 0) { + return [...getAllModules(), ...getEntryTasks()] + } + + return names + .map((name) => + getByName({ + name: kebabCase({ str: name.replace('@opentiny/react-', '') }), + isSort: false + }) + ) + .flat() +} + +const getAllIcons = () => { + const entries = fg.sync('react-icon*/src/*', { cwd: pathFromWorkspaceRoot('packages'), onlyDirectories: true }) + + return entries.map((item) => { + const name = path.basename(item) + + return { + path: item + '/index.ts', + libPath: item.replace('/src/', '/lib/'), + type: 'component', + componentType: 'icon', + name: kebabCase({ str: name }), + global: capitalizeKebabCase(name), + importName: '@opentiny/react-' + item + } as Module + }) +} + +function toEntry(libs) { + return libs.reduce((result, { libPath, path: file }) => { + const tLibPath = libPath.replace('-lib/', '/lib/') + result[tLibPath] = pathFromPackages(file) + return result + }, {}) +} + +function toTsInclued(libs) { + return new Set( + libs + .filter((item) => ['module', 'component'].includes(item.type)) + .map((lib) => `packages/${lib.dtsRoot ? lib.path : path.dirname(lib.path)}`) + ) +} + +function generatePackageJson({ beforeWriteFile }): Plugin { + return { + name: 'opentiny-vue:generate-package-json', + generateBundle(output: NormalizedOutputOptions, bundle: OutputBundle) { + const cache = {} + Object.entries(bundle).forEach(([, item]) => { + // 主入口文件, button/index.ts, common/src/index.ts + if (item.type === 'chunk' && /\/index\.(js|ts)/.test(item.facadeModuleId!)) { + // 从源文件中往上查找最近的 package.json 文件 + const packageJsonFile = findUpSync('package.json', { cwd: item.facadeModuleId! }) + + if (!packageJsonFile) return + + if (cache[packageJsonFile]) return + + let packageJson + try { + packageJson = JSON.parse(fs.readFileSync(packageJsonFile, { encoding: 'utf-8' })) + } catch { } + + const { filePath, content } = beforeWriteFile(path.dirname(item.fileName), packageJson) + + if (content) { + this.emitFile({ + type: 'asset', + fileName: `${filePath}/package.json`, + source: typeof content === 'string' ? content : JSON.stringify(content, null, 2) + }) + } + + const changelogFile = path.join(path.dirname(packageJsonFile), 'CHANGELOG.md') + if (fs.existsSync(changelogFile)) { + this.emitFile({ + type: 'asset', + fileName: `${filePath}/CHANGELOG.md`, + source: fs.readFileSync(changelogFile, { encoding: 'utf-8' }) + }) + } + + cache[packageJsonFile] = true + } + }) + } + } +} + +const getComponentAlias = (alias = {}) => { + getAllModules().forEach((item) => { + if (item.type === 'component') + alias[item.importName] = pathFromWorkspaceRoot('packages', item.path) + }) + return alias +} + +const getReactPlugins = (reactVersion: string) => { + const pluginMap = { + '18': () => { + const react18Plugin = requireModules('examples/react-docs/node_modules/@vitejs/plugin-react') + const react18SvgPlugin = svgr + + return [react18Plugin(), react18SvgPlugin()] + } + } + + return pluginMap[reactVersion]() +} + +function getBaseConfig({ + dts, + dtsInclude +}) { + return defineConfig({ + publicDir: false, + resolve: { + extensions: ['.js', '.ts', '.tsx', '.jsx'], + }, + define: { + 'process.env.BUILD_TARGET': JSON.stringify('component') + }, + plugins: [ + ...getReactPlugins('18'), + generatePackageJson({ + beforeWriteFile: (filePath, content) => { + const dependencies: any = {} + + Object.entries(content.dependencies).forEach(([key, value]) => { + // 只替换 react 系的依赖,方便调试 + // 其他公用的依赖,vue 之前可能发过包 + const newKey = key.replace('@opentiny/react', `${scopeName}/react`) + if ((value as string).includes('workspace:~')) { + dependencies[newKey] = '*' + } + else { + dependencies[newKey] = value + } + }) + + if (filePath.includes('react-common')) { + dependencies.react = '18.2.0' + } + + // 如果是主入口或者svg图标则直接指向相同路径 + if ( + filePath === 'react' || + filePath === 'react-icon' + ) { + content.main = './index.js' + content.module = './index.js' + } else { + content.main = './lib/index.js' + content.module = './lib/index.js' + } + + content.version = buildVersion + content.dependencies = dependencies + content.name = content.name.replace('@opentiny/react', `${scopeName}/react`) + + delete content.devDependencies + delete content.private + delete content.exports + + return { + filePath: filePath.replace(/[\\/]lib$/, ''), + content + } + } + }) + ] + }) +} + +async function batchBuild({ + tasks, + formats, + message, + emptyOutDir, + dts, + outDir +}) { + if (tasks.length === 0) return + logGreen(`====== 开始构建 ${message} ======`) + const entry = toEntry(tasks) + const dtsInclude = toTsInclued(tasks) + + await build({ + ...getBaseConfig({ dts, dtsInclude }), + configFile: false, + build: { + outDir, + emptyOutDir, + minify: false, + rollupOptions: { + plugins: [ + getBabelOutputPlugin({ + presets: [['@babel/preset-env', { loose: true, modules: false }]] + }) as any, + replace({ + '.less': '.css' + }), + { + name: 'replace-scope', + transform(code) { + if (scopeName === '@opentiny') return code + + let modifiedCode = code + while (modifiedCode.match(/@opentiny\/react/g)) { + modifiedCode = modifiedCode.replace('@opentiny/react', `${scopeName}/react`) + } + return { + code: modifiedCode + } + } + } + ], + output: { + strict: false, + manualChunks: {} + }, + external: (source, importer, isResolved) => { + // vite打包入口文件或者没有解析过得包不能排除依赖 + if (isResolved || !importer) { + return false + } + + // 子图标排除周边引用, 这里注意不要排除svg图标 + if (/react-icon\/.+\/index/.test(importer)) { + return !/\.svg/.test(source) + } + + // @opentiny/vue 总入口,需要排除所有依赖 + if (/react\/(index|pc|mobile|mobile-first)\.ts$/.test(importer)) { + return true + } + + if ([ + 'react', + 'react/jsx-runtime' + ].includes(source)) { + return true + } + + if (source.indexOf(scopeName) === 0) { + return true + } + + return external(source) + }, + }, + lib: { + entry, + formats, + fileName: (format, entryName) => `${entryName}.js` + } + } + }) +} + +async function batchBuildAll({ + tasks, + formats, + message, + emptyOutDir, + dts, + npmScope +}) { + const rootDir = pathFromPackages('') + const outDir = path.resolve(rootDir, `dist-react/${npmScope}`) + await batchBuild({ + tasks, + formats, + message, + emptyOutDir, + dts, + outDir + }) +} + +export async function buildReact( + names: string[] = [], + { + buildTarget = '1.0.0', + formats = ['es'], + clean = false, + dts = true, + scope = '@opentiny' + } +) { + scopeName = scope + buildVersion = buildTarget + // 要构建的模块 + let tasks = getTasks(names) + + // 如果指定了打包icon或者没有传入任何组件 + if (names.some((name) => name.includes('icon')) || !names.length) { + tasks.push(...getAllIcons()) + } + + // 构建 @opentiny/react + if (names.some((name) => [`${scopeName}/react`, 'react'].includes(name))) { + tasks.push(...getEntryTasks()) + } + + const message = `TINY for react: ${JSON.stringify(names.length ? names : '全量')}` + + await batchBuildAll({ + tasks, + formats, + message, + emptyOutDir: clean, + dts, + npmScope: scope + }) +} diff --git a/internals/cli/src/commands/build/build-ui.ts b/internals/cli/src/commands/build/build-ui.ts index 9d6df5e1f..b67f24cde 100644 --- a/internals/cli/src/commands/build/build-ui.ts +++ b/internals/cli/src/commands/build/build-ui.ts @@ -384,4 +384,4 @@ export async function buildUi( // 确保只运行一次 emptyOutDir = false } -} +} \ No newline at end of file diff --git a/internals/cli/src/commands/build/index.ts b/internals/cli/src/commands/build/index.ts index 0cbffa566..dd8175971 100644 --- a/internals/cli/src/commands/build/index.ts +++ b/internals/cli/src/commands/build/index.ts @@ -1,3 +1,5 @@ export * from './build-ui' export * from './build-entry' export * from './build-runtime' +export * from './build-ui-react' +export * from './build-entry-react' \ No newline at end of file diff --git a/internals/cli/src/commands/create/common-mapping-react.json b/internals/cli/src/commands/create/common-mapping-react.json new file mode 100644 index 000000000..033028027 --- /dev/null +++ b/internals/cli/src/commands/create/common-mapping-react.json @@ -0,0 +1,12 @@ +{ + "Icon": { + "path": "react-icon/index.ts", + "type": "module", + "exclude": false + }, + "Common": { + "path": "react-common/src/index.ts", + "type": "module", + "exclude": false + } +} \ No newline at end of file diff --git a/internals/cli/src/commands/create/create-mapping-react.ts b/internals/cli/src/commands/create/create-mapping-react.ts new file mode 100644 index 000000000..ce4d7836a --- /dev/null +++ b/internals/cli/src/commands/create/create-mapping-react.ts @@ -0,0 +1,111 @@ +import { capitalize, walkFileTree, pathFromWorkspaceRoot, logGreen, prettierFormat } from '../../shared/utils' +import { quickSort } from '../../shared/module-utils' +import path from 'node:path' +import fs from 'fs-extra' +import commonMappingReact from './common-mapping-react.json' + +const getBuildEntryFile = (file, dirs, subPath) => { + // 模板文件(pc|mobile|mobile-first)需要同级目录有index.ts文件才能成为打包入口 + const isTemplatePath = dirs.includes('index.ts') + const isMainEntry = file.includes('index') && dirs.includes('package.json') + const isPcEntry = file.includes('pc.') && subPath.includes(`src${path.sep}pc.`) && isTemplatePath + const isMobileEntry = file.includes('mobile.') && subPath.includes(`src${path.sep}mobile.`) && isTemplatePath + const isMobileFirstEntry = + file.includes('mobile-first.') && subPath.includes(`src${path.sep}mobile-first.`) && isTemplatePath + return { + isBuildEntryFile: isMainEntry || isPcEntry || isMobileEntry || isMobileFirstEntry, + isMainEntry, + isPcEntry, + isMobileEntry, + isMobileFirstEntry + } +} + +const tempMap = { + 'pc.jsx': 'pc', + 'mobile.jsx': 'mobile', + 'mobile-first.jsx': 'mobile-first', + 'pc.tsx': 'pc', + 'mobile.tsx': 'mobile', + 'mobile-first.tsx': 'mobile-first' +} + +const getTemplateName = (currentPaths, entryObj) => { + const entryMaps = { + isPcEntry: 'Pc', + isMobileEntry: 'Mobile', + isMobileFirstEntry: 'MobileFirst', + isMainEntry: '' + } + const mapKey = Object.keys(entryObj).filter((item) => entryObj[item] && item !== 'isBuildEntryFile')[0] + const subFix = entryMaps[mapKey] + return `${currentPaths.split('-').map(capitalize).join('')}${subFix}` +} + +export const writeModuleMap = (moduleMap) => { + fs.writeFileSync( + pathFromWorkspaceRoot('packages/modules-react.json'), + prettierFormat({ + str: typeof moduleMap === 'string' ? moduleMap : JSON.stringify(moduleMap), + options: { + parser: 'json', + printWidth: 10 + } + }) + ) +} + +function makeReactModules() { + const templates = { ...commonMappingReact } + + walkFileTree({ + isDeep: true, + dirPath: pathFromWorkspaceRoot('packages/react/src'), + fileFilter({ file }) { + return !/node_modules/.test(file) + }, + callback({ file, subPath, dirs, isDirectory }) { + const entryObj = getBuildEntryFile(file, dirs, subPath) + const mode: string[] = [] + + if (entryObj.isMainEntry && dirs.includes('src')) { + const srcPath = subPath.replace(file, 'src') + const srcFiles = fs.readdirSync(srcPath) || [] + srcFiles.forEach((item) => { + if (tempMap[item]) { + mode.push(tempMap[item]) + } + }) + } + + if (entryObj.isBuildEntryFile) { + const modulePath = subPath.slice(subPath.lastIndexOf(`react${path.sep}src`)).replaceAll(path.sep, '/') + const matchArr = modulePath.match(/.+\/(.+?)\/(index\.ts|src\/pc\.|src\/mobile\.|src\/mobile-first\.)/) + if (matchArr?.[1]) { + const compName = getTemplateName(matchArr[1], entryObj) + templates[compName] = { + path: modulePath, + type: entryObj.isMainEntry ? 'component' : 'template', + exclude: false + } + if (mode.length > 0) { + templates[compName].mode = mode + } + } + } + } + }) + + const modulesJson = quickSort({ sortData: templates, returnType: 'object' }) + + writeModuleMap(modulesJson) +} + +try { + makeReactModules() + + logGreen('npm run create:mapping-react done.') +} +catch (e) { + console.log(e) +} \ No newline at end of file diff --git a/internals/cli/src/index.ts b/internals/cli/src/index.ts index b25defad5..b4d3a63dc 100644 --- a/internals/cli/src/index.ts +++ b/internals/cli/src/index.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node import { Command, Option } from 'commander' import { createIconSaas } from './commands/create/index.js' -import { buildUi, buildEntry, buildRuntime } from './commands/build' +import { buildUi, buildEntry, buildRuntime, buildReact, buildEntryReact } from './commands/build' import { releaseAurora } from './commands/release/releaseAurora' const program = new Command() @@ -10,6 +10,8 @@ program.command('release:aurora').description('转换为aurora的包').action(re program.command('create:icon-saas').description('同步生成 icon-saas').action(createIconSaas) +program.command('build:entry-react').description('生成 react 组件库入口').action(buildEntryReact) + program.command('build:entry').description('生成组件库入口').action(buildEntry) program @@ -31,4 +33,15 @@ program .option('-m, --min', '是否压缩输出文件', false) .action(buildRuntime) +program + .command('build:react') + .description('打包 react 组件库') + .argument('[names...]', '构建指定组件,如 button alert;不指定则构建全量组件') + .addOption(new Option('-f --formats ', '目标格式,默认 ["es"]').choices(['es', 'cjs'])) + .addOption(new Option('-t --build-target ', '组件的目标版本')) + .option('-s, --scope ', 'npm scope,默认是 opentiny,会以 @opentiny 发布到 npm') + .option('-c, --clean', '清空构建目录') + .option('--no-dts', '不生成 dts') + .action(buildReact) + program.parse() diff --git a/internals/cli/src/shared/config.ts b/internals/cli/src/shared/config.ts index 3dd9cc996..61d82ca0a 100644 --- a/internals/cli/src/shared/config.ts +++ b/internals/cli/src/shared/config.ts @@ -16,4 +16,4 @@ const external = (deps) => { return EXTENERAL.includes(deps) || /^@opentiny[\\/]|@originjs|echarts|cropperjs|@better-scroll|crypto-js/.test(deps) } -export { external } +export { external } \ No newline at end of file diff --git a/package.json b/package.json index 9a6c66feb..c934a7ac7 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,8 @@ "dev2:saas": "pnpm build:entry && pnpm build:component-cssvar && pnpm -C examples/vue2 dev:saas", "dev2.7": "pnpm build:entry && pnpm build:component-cssvar && pnpm -C examples/vue2.7 dev", "dev2.7:saas": "pnpm build:entry && pnpm build:component-cssvar && pnpm -C examples/vue2.7 dev:saas", + "dev:docs": "pnpm -C examples/docs docs:dev", + "dev:react": "pnpm build:entry-react && pnpm -C examples/react-docs run dev", "// ---------- 启动官网文档 ----------": "", "dev:site": "pnpm build:entry && pnpm build:component-cssvar && pnpm -C examples/sites start", "dev:open-site": "pnpm build:entry && pnpm build:component-cssvar && pnpm -C examples/sites start:open", @@ -108,7 +110,15 @@ "// ---------- 手工构建发布指定组件 (Beta) ----------": "", "preci:deployBeta": "pnpm clean:build", "ci:deployBeta": "pnpm build:ui", - "postci:deployBeta": "lerna publish from-package --yes --dist-tag beta" + "postci:deployBeta": "lerna publish from-package --yes --dist-tag beta", + "analyse:depends": "pnpm --filter @opentiny/analyse_depends start", + "build:entry-react": "pnpm -C internals/cli build:entry-react", + "create:mapping-react": "pnpm -C internals/cli create:mapping-react", + "build:react": "pnpm -C internals/cli build:react", + "build:ui-react": "pnpm create:mapping-react && pnpm build:entry-react && pnpm build:react", + "pub:react": "pnpm --filter=\"./packages/dist-react/**\" publish --no-git-checks --access=public", + "dev:react-site": "pnpm --filter @opentiny/react-site start", + "build:react-site": "pnpm --filter @opentiny/react-site build" }, "dependencies": { "@vue/composition-api": "1.2.2", @@ -241,4 +251,4 @@ "> 1%", "last 2 versions" ] -} +} \ No newline at end of file diff --git a/packages/modules-react.json b/packages/modules-react.json new file mode 100644 index 000000000..846b83360 --- /dev/null +++ b/packages/modules-react.json @@ -0,0 +1,56 @@ +{ + "Alert": { + "path": "react/src/alert/index.ts", + "type": "component", + "exclude": false, + "mode": [ + "mobile-first", + "pc" + ] + }, + "AlertMobileFirst": { + "path": "react/src/alert/src/mobile-first.jsx", + "type": "template", + "exclude": false + }, + "AlertPc": { + "path": "react/src/alert/src/pc.jsx", + "type": "template", + "exclude": false + }, + "Button": { + "path": "react/src/button/index.ts", + "type": "component", + "exclude": false, + "mode": [ + "mobile-first", + "mobile", + "pc" + ] + }, + "ButtonMobile": { + "path": "react/src/button/src/mobile.jsx", + "type": "template", + "exclude": false + }, + "ButtonMobileFirst": { + "path": "react/src/button/src/mobile-first.jsx", + "type": "template", + "exclude": false + }, + "ButtonPc": { + "path": "react/src/button/src/pc.jsx", + "type": "template", + "exclude": false + }, + "Common": { + "path": "react-common/src/index.ts", + "type": "module", + "exclude": false + }, + "Icon": { + "path": "react-icon/index.ts", + "type": "module", + "exclude": false + } +} diff --git a/packages/react-common/package.json b/packages/react-common/package.json new file mode 100644 index 000000000..c3bf8374b --- /dev/null +++ b/packages/react-common/package.json @@ -0,0 +1,19 @@ +{ + "name": "@opentiny/react-common", + "version": "1.0.0", + "description": "", + "main": "src/index.ts", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@opentiny/vue-renderless": "workspace:~", + "@opentiny/vue-theme": "workspace:~", + "classnames": "^2.3.2", + "react": "18.2.0", + "tailwind-merge": "^1.8.0" + } +} diff --git a/packages/react-common/src/csscls.ts b/packages/react-common/src/csscls.ts new file mode 100644 index 000000000..27f274a6e --- /dev/null +++ b/packages/react-common/src/csscls.ts @@ -0,0 +1,65 @@ +interface CssClassObject { + [k: string]: any +} +type CssClassArray = Array +export type CssClass = string | CssClassObject | CssClassArray + +/** + * 简单合并 tailwind 类对象为字符串值 + * + * @param cssClassObject tailwind 类对象 + * @returns string + */ +const stringifyCssClassObject = (cssClassObject: CssClassObject): string => { + const allCssClass: Array = [] + + Object.keys(cssClassObject).forEach((cssClass) => cssClassObject[cssClass] && allCssClass.push(cssClass)) + + return allCssClass.join('\u{20}') +} + +/** + * 简单合并 tailwind 类数组为字符串值 + * + * @param cssClassArray tailwind 类数组 + * @returns string + */ +const stringifyCssClassArray = (cssClassArray: CssClassArray): string => { + const allCssClass: Array = [] + + cssClassArray.forEach((cssClass) => { + if (typeof cssClass === 'string') { + allCssClass.push(cssClass) + } else if (typeof cssClass === 'object') { + allCssClass.push(stringifyCssClassObject(cssClass)) + } + }) + + return allCssClass.join('\u{20}') +} + +/** + * 简单合并 tailwind 类对象为字符串值,去重处理留给 tailwind-merge 处理 + * + * @param {*} cssClasses tailwind 类集合 + * @returns string + */ +export const stringifyCssClass = (cssClasses: Array): string => { + if (!cssClasses || (Array.isArray(cssClasses) && !cssClasses.length)) return '' + + const allCssClass: Array = [] + + cssClasses.forEach((cssClass) => { + if (cssClass) { + if (typeof cssClass === 'string') { + allCssClass.push(cssClass) + } else if (Array.isArray(cssClass)) { + allCssClass.push(stringifyCssClassArray(cssClass)) + } else if (typeof cssClass === 'object') { + allCssClass.push(stringifyCssClassObject(cssClass)) + } + } + }) + + return allCssClass.join('\u{20}') +} diff --git a/packages/react-common/src/event.js b/packages/react-common/src/event.js new file mode 100644 index 000000000..277ce307d --- /dev/null +++ b/packages/react-common/src/event.js @@ -0,0 +1,95 @@ +import { eventBus } from './utils' + +const $busMap = new Map() + +export const emit = (props) => (evName, ...args) => { + const reactEvName = 'on' + + evName.substr(0, 1).toUpperCase() + + evName.substr(1) + + if (props[reactEvName] && typeof props[reactEvName] === 'function') { + props[reactEvName](...args) + } + else { + const $bus = $busMap.get(props) + if ($bus) { + $bus.emit(evName, ...args) + } + } +} +export const on = (props) => (evName, callback) => { + if ($busMap.get(props)) { + const $bus = $busMap.get(props) + $bus.on(evName, callback) + } + else { + const $bus = eventBus() + $bus.on(evName, callback) + $busMap.set(props, $bus) + } +} +export const off = (props) => (evName, callback) => { + const $bus = $busMap.get(props) + if (!$bus) return + $bus.off(evName, callback) +} +export const once = (props) => (evName, callback) => { + let $bus = null + const onceCallback = (...args) => { + callback(...args) + $bus && $bus.off(evName, onceCallback) + } + + if ($busMap.get(props)) { + $bus = $busMap.get(props) + $bus.on(evName, onceCallback) + } + else { + $bus = eventBus() + $bus.on(evName, onceCallback) + $busMap.set(props, $bus) + } +} +export const emitEvent = (vm) => { + const broadcast = (vm, componentName, eventName, ...args) => { + const children = vm.$children + + Array.isArray(children) + && children.forEach((child) => { + const name = child.$options && child.$options.componentName + const component = child + + if (name === componentName) { + component.emit(eventName, ...args) + // todo: 调研 component.$emitter + // component.$emitter && component.$emitter.emit(eventName, params) + } + else { + broadcast(child, componentName, eventName, ...args) + } + }) + } + + return { + dispatch(componentName, eventName, ...args) { + let parent = vm.$parent + if (parent.type === null) return + let name = parent.$options && parent.$options.componentName + while (parent && parent.type && (!name || name !== componentName)) { + parent = parent.$parent + + if (parent) + name = parent.$options && parent.$options.componentName + } + + if (parent) { + parent.emit(eventName, ...args) + // fix: VUE3下事件参数为数组,VUE2下事件参数不是数组,这里修改为和VUE2兼容 + // parent.$emitter && parent.$emitter.emit(...[eventName].concat(params)) + } + }, + broadcast(componentName, eventName, ...args) { + broadcast(vm, componentName, eventName, ...args) + } + } +} diff --git a/packages/react-common/src/fiber.js b/packages/react-common/src/fiber.js new file mode 100644 index 000000000..0d870530f --- /dev/null +++ b/packages/react-common/src/fiber.js @@ -0,0 +1,121 @@ +import { useRef, useEffect, useState } from 'react' +import { compWhiteList } from './virtual-comp.jsx' + +function getFiberByDom(dom) { + const key = Object.keys(dom).find((key) => { + return ( + key.startsWith('__reactFiber$') || // react 17+ + key.startsWith('__reactInternalInstance$') + ) // react <17 + }) + + return dom[key] +} + +function traverseFiber(fiber, breaker, handler) { + if (!fiber) return + typeof handler === 'function' && handler(fiber) + if (typeof breaker !== 'function') { + breaker = () => { } + } + traverseFiber(fiber.sibling, breaker, handler) + breaker(fiber) || traverseFiber(fiber.child, breaker, handler) +} + +function collectFiber(fiber, collector, mapper) { + const collect = [] + traverseFiber( + fiber, + ({ type }) => { + if (type && typeof type !== 'string') { + return !compWhiteList.includes(type.name) + } + return false + }, + (fiber) => { + if (typeof collector === 'function' && collector(fiber)) { + collect.push(fiber) + } + } + ) + return typeof mapper === 'function' + ? collect.map(mapper) + : collect +} + +function collectChildren(fiber) { + return collectFiber(fiber, (fiber) => { + return fiber.type && typeof fiber.type !== 'string' + }) +} + +function collectRefs(fiber) { + return collectFiber( + fiber, + (fiber) => { + if (typeof fiber.type === 'string') { + return fiber.stateNode.getAttribute('v_ref') + } + else { + return fiber.memoizedProps.v_ref + } + }, + (fiber) => { + if (typeof fiber.type === 'string') { + return { + [fiber.stateNode.getAttribute('v_ref')]: fiber.stateNode + } + } + else { + return { + [fiber.memoizedProps.v_ref]: fiber + } + } + } + ).reduce((pre, cur) => { + Object.assign(pre, cur) + return pre + }, {}) +} + +const parentMap = new WeakMap() +export function getParentFiber(fiber, isFirst = true, child = fiber) { + if (!fiber || !fiber.return) return null + if (parentMap.has(child)) return parentMap.get(child) + if (fiber.type && typeof fiber.type !== 'string' && !isFirst) { + parentMap.set(child, fiber) + return fiber + } + return getParentFiber(fiber.return, false, fiber) +} + +export function creatFiberCombine(fiber) { + return { + fiber, + refs: fiber && collectRefs(fiber.child), + children: fiber && collectChildren(fiber.child), + } +} + +export function useFiber() { + const ref = useRef() + const [parent, setParent] = useState() + const [current, setCurrent] = useState() + useEffect(() => { + if (ref.current) { + const current_fiber = getFiberByDom(ref.current) + setParent( + getParentFiber( + current_fiber.return + ) + ) + setCurrent(current_fiber.return) + } + }, []) + + return { + ref, + parent: creatFiberCombine(parent), + current: creatFiberCombine(current) + } +} diff --git a/packages/react-common/src/index.ts b/packages/react-common/src/index.ts new file mode 100644 index 000000000..9913960e3 --- /dev/null +++ b/packages/react-common/src/index.ts @@ -0,0 +1,111 @@ +import * as hooks from 'react' +import { Svg } from './svg-render.jsx' + +import { nextTick, ref, computed, readonly, watch, onBeforeUnmount, inject } from './vue-hooks.js' +import { emit, on, off, once, emitEvent } from './event.js' +import { If, Component, Slot, For, Transition } from './virtual-comp.jsx' +import { filterAttrs, vc, getElementCssClass } from './utils.js' +import { useFiber } from './fiber.js' +import { useVm } from './vm.js' +import { useReactive } from './reactive.js' +import { twMerge } from 'tailwind-merge' +import { stringifyCssClass } from './csscls.js' + +import '@opentiny/vue-theme/base/index.less' + +const vue_hooks = { + nextTick, + ref, + computed, + readonly, + watch, + onBeforeUnmount, + inject +} + +// emitEvent, dispath, broadcast +export const $prefix = 'Tiny' + +export const mergeClass = (...cssClasses) => twMerge(stringifyCssClass(cssClasses)) + +export const useSetup = ({ + props, + // context, + renderless, + api, + extendOptions = {}, + // mono = false, + classes = {}, + constants, + vm, + parent +}) => { + const render = typeof props.tiny_renderless === 'function' ? props.tiny_renderless : renderless + const { dispath, broadcast } = emitEvent(vm) + + const utils = { + vm, + parent, + emit: emit(props), + constants, + nextTick, + dispath, + broadcast, + t() { }, + mergeClass + } + const sdk = render( + props, + { + ...hooks, + useReactive, + ...vue_hooks, + reactive: useReactive + }, + utils, + extendOptions + ) + const attrs = { + a: filterAttrs, + m: mergeClass, + vm: utils.vm, + gcls: (key) => getElementCssClass(classes, key), + } + + if (Array.isArray(api)) { + api.forEach((name) => { + const value = sdk[name] + + if (typeof value !== 'undefined') { + attrs[name] = value + } + }) + } + + return attrs +} + +export { + Svg, + vc, + If, + Component, + Slot, + For, + Transition, + emit, + on, + off, + once, + emitEvent, + useVm, + nextTick, + useFiber, + ref, + computed, + readonly, + useReactive, + watch +} + +export const reactive = useReactive diff --git a/packages/react-common/src/reactive.js b/packages/react-common/src/reactive.js new file mode 100644 index 000000000..42ca5371e --- /dev/null +++ b/packages/react-common/src/reactive.js @@ -0,0 +1,104 @@ +import { useState, useRef } from 'react' +import { computed } from './vue-hooks' + +// 响应式核心 +const reactiveMap = new WeakMap() +const reactive = ( + staticObject, + handler = {}, + path = [], + rootStaticObject = staticObject +) => { + reactiveMap.has(staticObject) || reactiveMap.set(staticObject, new Proxy(staticObject, { + get( + target, + property, + receiver + ) { + const targetVal = target[property] + if (targetVal && targetVal['v-hooks-type'] === computed) { + return targetVal.value + } + + const _path = [...path, property] + const res = typeof targetVal === 'object' + ? reactive(targetVal, handler, _path, rootStaticObject) + : targetVal + + // 监听访问 + handler.get && handler.get({ + result: res, + root: rootStaticObject, + path: _path, + target, + property, + receiver + }) + + return res + }, + set( + target, + property, + value, + receiver + ) { + const targetVal = target[property] + if (targetVal && targetVal['v-hooks-type'] === computed) { + targetVal.value = value + return true + } + + const _path = [...path, property] + + // 监听修改 + handler.set && handler.set({ + target, + property, + receiver, + root: rootStaticObject, + path: _path, + newVal: value, + oldVal: target[property] + }) + + target[property] = value + return true + } + })) + + return reactiveMap.get(staticObject) +} + +export const useReload = () => { + const setReload = useState(0)[1] + return () => setReload(pre => pre + 1) +} + +const isObject = (val) => val !== null && typeof val === 'object' + +// 用于从 hooks 链表中查找 reactive 生成的 state +export function Reactive(obj) { + Object.keys(obj).forEach(key => { + this[key] = obj[key] + }) +} + +export const useReactive = (initalObject) => { + if (!isObject(initalObject)) { + return initalObject + } + const memoried = useRef() + const proxy = useRef() + const reload = useReload() + + if (!memoried.current && !proxy.current) { + memoried.current = new Reactive(initalObject) + proxy.current = reactive(memoried.current, { + set() { + reload() + } + }) + } + return proxy.current +} diff --git a/packages/react-common/src/resolve-props.js b/packages/react-common/src/resolve-props.js new file mode 100644 index 000000000..b44730eb8 --- /dev/null +++ b/packages/react-common/src/resolve-props.js @@ -0,0 +1,35 @@ +// todo: 一个方法去拿到 props 身上的事件,以 on 为前缀 +const reactEventPrefix = /^on[A-Z]/ +export function getEventByReactProps(props) { + const $listeners = {} + Object + .keys(props) + .filter(propName => { + return reactEventPrefix.test(propName) + && typeof props[propName] === 'function' + }) + .map(reactEvName => { + return { + reactEvName, + vueEvName: reactEvName.substr(2).toLowerCase() + } + }) + .forEach(({ reactEvName, vueEvName }) => { + Object.assign($listeners, { + [vueEvName]: props[reactEvName] + }) + }) + return $listeners +} +export function getAttrsByReactProps(props) { + const $attrs = {} + Object + .keys(props) + .filter(propName => { + return !reactEventPrefix.test(propName) && !['children'].includes(propName) + }) + .forEach((attr) => { + $attrs[attr] = props[attr] + }) + return $attrs +} diff --git a/packages/react-common/src/svg-render.jsx b/packages/react-common/src/svg-render.jsx new file mode 100644 index 000000000..030d657d7 --- /dev/null +++ b/packages/react-common/src/svg-render.jsx @@ -0,0 +1,23 @@ +import classNames from 'classnames' +import { If } from './virtual-comp.jsx' + +export const Svg = ({ name = 'Icon', component: Icon }) => { + const funcObj = ({ + [name](props) { + const className = classNames( + 'icon', + 'tiny-svg', + props.className + ) + const v_if = typeof props['v-if'] === 'boolean' ? props['v-if'] : true + const defaultProps = { ...props } + delete defaultProps['v-if'] + return ( + + + + ) + } + }) + return funcObj[name] +} diff --git a/packages/react-common/src/utils.ts b/packages/react-common/src/utils.ts new file mode 100644 index 000000000..b7ee7c535 --- /dev/null +++ b/packages/react-common/src/utils.ts @@ -0,0 +1,110 @@ +/** + * filterAttrs 属性过滤函数 + * @param {object} attrs 由父组件传入,且没有被子组件声明为 props 的一些属性 + * @param {Array} filters 过滤数组,元素可以为字符串,也可以为正则表达式 + * @param {boolean} include 是否返回为被过滤的属性集合,如果为 false,filters 是过滤不要的属性 + * @returns {object} 过滤后的属性对象 + */ +export const filterAttrs = (attrs, filters, include) => { + const props = {} + + for (let name in attrs) { + const find = filters.some((r) => new RegExp(r).test(name)) + + if ((include && find) || (!include && !find)) { + props[name] = attrs[name] + } + } + + return props +} + +/** + * event bus + * $bus.on + * $bus.off + * $bus.emit + */ + +export const eventBus = () => { + const $bus = {} + + const on = (eventName, callback) => { + if (!$bus[eventName]) { + $bus[eventName] = [] + } + + $bus[eventName].push(callback) + } + + const off = (eventName, callback) => { + if (!$bus[eventName]) { + return + } + + $bus[eventName] = $bus[eventName].filter( + subscriber => subscriber !== callback + ) + } + + const emit = (eventName, ...args) => { + if (!$bus[eventName]) { + return + } + + $bus[eventName].forEach(subscriber => subscriber(...args)) + } + + return { + on, + emit, + off + } +} + +/** + * 实现 vue 中 :class 的用法 + */ + +export function VueClassName(className) { + if (typeof className === 'string') { + return className + } + else if (Array.isArray(className)) { + return className.reduce((pre, cur, index) => { + if (typeof cur === 'string') { + return `${pre}${index === 0 ? '' : ' '}${cur}` + } + else { + return `${pre}${index === 0 ? '' : ' '}${VueClassName(cur)}` + } + }, '') + } + else if (typeof className === 'object') { + return Object + .keys(className) + .reduce((pre, key, index) => { + if (className[key]) { + return `${pre}${index === 0 ? '' : ' '}${key}` + } + else { + return pre + } + }, '') + } +} + +export const vc = VueClassName + +export const getElementCssClass = (classes = {}, key) => { + if (typeof key === 'object') { + const keys = Object.keys(key) + let cls = '' + keys.forEach((k) => { + if (key[k] && classes[k]) cls += `${classes[k]} ` + }) + return cls + } else { + return classes[key] || '' + } +} diff --git a/packages/react-common/src/virtual-comp.jsx b/packages/react-common/src/virtual-comp.jsx new file mode 100644 index 000000000..9db48a105 --- /dev/null +++ b/packages/react-common/src/virtual-comp.jsx @@ -0,0 +1,72 @@ +export function If(props) { + if (props['v-if']) { + return (props.children) + } + else { + return '' + } +} + +export function Component(props) { + const Is = props.is || (() => '') + return + + +} + +export function Slot(props) { + const { + name = 'default', + slots = {}, + parent_children + } = props + + const EmptySlot = () => ''; + + const S = slots[name] || EmptySlot + + return (<> + + {parent_children || props.children} + + + + + + + {props.children} + + + ) +} + +export function For(props) { + const { + item: Item, + list = [] + } = props + + const listItems = list.map((item, index, list) => { + return () + }) + + return (<>{listItems}) +} + +export function Transition(props) { + const { + name + } = props + + // 在 useEffect 里监听 dom 变化,拿到变化的 dom,给上面添加样式 + + return <>{props.children} +} + +export const compWhiteList = [ + 'If', + 'Component', + 'Slot', + 'For', + 'Transition' +] diff --git a/packages/react-common/src/vm.js b/packages/react-common/src/vm.js new file mode 100644 index 000000000..3a6cd9e7c --- /dev/null +++ b/packages/react-common/src/vm.js @@ -0,0 +1,127 @@ +import { useFiber, getParentFiber, creatFiberCombine } from './fiber.js' +import { + getEventByReactProps, + getAttrsByReactProps +} from './resolve-props.js' +import { Reactive } from './reactive.js' +import { emit, on, off, once } from './event.js' + +const vmProxy = { + $parent: ({ fiber }) => { + const parentFiber = getParentFiber(fiber) + if (!parentFiber) return null + return createVmProxy( + creatFiberCombine( + parentFiber + ) + ) + }, + $el: ({ fiber }) => fiber.child?.stateNode, + $refs: ({ refs, fiber }) => createRefsProxy(refs, fiber.constructor), + $children: ({ children }) => children.map((fiber) => createVmProxy( + creatFiberCombine( + getParentFiber(fiber) + ) + )), + $listeners: ({ fiber }) => getEventByReactProps(fiber.memoizedProps), + $attrs: ({ fiber }) => getAttrsByReactProps(fiber.memoizedProps), + $slots: ({ fiber }) => fiber.memoizedProps.slots, + $scopedSlots: ({ fiber }) => fiber.memoizedProps.slots, + $options: ({ fiber }) => ({ componentName: fiber.type.name }), + $constants: ({ fiber }) => fiber.memoizedProps._constants, + $template: ({ fiber }) => fiber.memoizedProps.tiny_template, + $renderless: ({ fiber }) => fiber.memoizedProps.tiny_renderless, + $mode: () => 'pc', + state: ({ fiber }) => findStateInHooks(fiber.memoizedState), + $type: ({ fiber }) => fiber.type, + $service: (_, vm) => vm.state?.$service, + $emit: ({ fiber }) => emit(fiber.memoizedProps), + $on: ({ fiber }) => on(fiber.memoizedProps), + $once: ({ fiber }) => once(fiber.memoizedProps), + $off: ({ fiber }) => off(fiber.memoizedProps), + $set: () => (target, propName, value) => target[propName] = value +} + +const vmProxyMap = new WeakMap() +function createVmProxy(fiberCombine) { + if (!vmProxyMap.has(fiberCombine)) { + vmProxyMap.set(fiberCombine, new Proxy(fiberCombine, { + get( + target, + property, + receiver + ) { + if (!vmProxy[property]) { + return target.fiber.memoizedProps[property] + } + return vmProxy[property](target, receiver) + }, + set( + target, + property, + value + ) { + return true + } + })) + } + return vmProxyMap.get(fiberCombine) +} + +function createEmptyProxy() { + return new Proxy({}, { + get() { + return undefined + }, + set() { + return true + } + }) +} + +function createRefsProxy(refs, FiberNode) { + return new Proxy(refs, { + get( + target, + property + ) { + if (target[property] instanceof FiberNode) { + return createVmProxy( + creatFiberCombine(target[property]) + ) + } + else { + return target[property] + } + } + }) +} + +function findStateInHooks(hookStart) { + let curHook = hookStart + // find state from hooks chain by Constructor Reactive + while (curHook) { + const refCurrent = curHook.memoizedState && curHook.memoizedState.current + if (refCurrent instanceof Reactive) break + curHook = curHook.next + } + return curHook + && curHook.memoizedState + && curHook.memoizedState.current +} + +export function useVm() { + const { ref, current, parent } = useFiber() + if (!ref.current) { + return { + ref, + current: createEmptyProxy(), + parent: createEmptyProxy() + } + } + return { + ref, + current: current.fiber && createVmProxy(current), + parent: parent.fiber && createVmProxy(parent) + } +} diff --git a/packages/react-common/src/vue-hooks.js b/packages/react-common/src/vue-hooks.js new file mode 100644 index 000000000..ec4bbb255 --- /dev/null +++ b/packages/react-common/src/vue-hooks.js @@ -0,0 +1,199 @@ +import { useRef, useEffect } from 'react' +import { useReload } from './reactive' + +function Create(target) { + Object.keys(target).forEach(key => { + this[key] = target[key] + }) +} +function Readonly(target) { + Create.call(this, target) +} + +const useDepChange = (dependencies, immediate = false) => { + let isDepChange = false + const pre_dep = useRef() + if (!pre_dep.current) { + isDepChange = true && immediate + } + else { + for (let i in dependencies) { + if (pre_dep.current[i] !== dependencies[i]) { + isDepChange = true + break + } + } + } + pre_dep.current = dependencies + return isDepChange +} + +export const nextTick = (callback) => { + queueMicrotask(callback) +} + +export const ref = (value) => { + const reload = useReload() + const proxy = useRef() + if (!proxy.current) { + proxy.current = new Proxy({ + value + }, { + get(target, property) { + if (property !== 'value') return + return target[property] + }, + set(target, property, newVal) { + if (property !== 'value') return true + target[property] = newVal + reload() + return true + } + }) + } + return proxy.current +} + +export function computed(getter) { + const thisObj = {} + Object.setPrototypeOf(thisObj, computed.prototype) + if (typeof getter === 'function') { + thisObj.get = getter + } + else if (typeof getter === 'object') { + if (typeof getter.get === 'function') { + thisObj.get = getter.get + } + if (typeof getter.set === 'function') { + thisObj.set = getter.set + } + } + return new Proxy({ + value: '' + }, { + get( + target, + property, + receiver + ) { + if (property === 'v-hooks-type') { + return computed + } + else if (property === 'value') { + return thisObj.get() + } + }, + set( + target, + property, + value, + receiver + ) { + if (property === 'value') { + if (typeof thisObj.set === 'function') { + thisObj.set(value) + } + return true + } + return true + } + }) +} + +export const readonly = (target) => { + const proxy = useRef() + if (!proxy.current) { + proxy.current = new Proxy(new Readonly(target), { + get: (target, property) => target[property], + set: () => true + }) + } + return proxy.current +} + +export const watchEffect = (effect, dependencies, options) => { + const cache = useRef() + const isDepChange = useDepChange(dependencies) + if (!cache.current) cache.current = { effect: true } + const { flush } = options || { flust: 'pre' } + const onCleanUp = (callback) => cache.current.clean = callback + if (cache.current.effect && isDepChange) { + const clean = cache.current.clean + typeof clean === 'function' && clean() + if (flush === 'pre') { + effect(onCleanUp) + } + else if (flush === 'sync') { + effect(onCleanUp) + } + else { + nextTick(() => { + effect(onCleanUp) + }) + } + } + return () => cache.current.effect = false +} + +export const watchPostEffect = (effect, dependencies) => watchEffect(effect, dependencies, { flush: 'post' }) + +export const watch = (source, callback, options = {}) => { + const cache = useRef() + let source_value + if (Array.isArray(source)) { + source_value = source.map((item) => typeof item === 'function' && item()) + } + else { + source_value = [(typeof source === 'function' && source())] + } + const isDepChange = useDepChange(source_value, options.immediate) + if (!cache.current) cache.current = { clear: false } + if (isDepChange && !cache.current.clear) { + callback( + source_value.length === 1 ? source_value[0] : source_value, + cache.current.pre + ) + } + cache.current.pre = source_value + return () => cache.current.clear = true +} + +const provideMap = new WeakMap() +export const provide = (vm) => (key, value) => { + if (!provideMap.has(vm)) { + provideMap.set(vm, {}) + } + const provideObj = provideMap.get(vm) + provideObj[key] = value +} + +export const inject = (_parent) => (key, defaultValue, treatDefaultAsFactory) => { + let parent = _parent + let context = null + + while (parent) { + parent = parent.$parent + if (provideMap.has(parent)) { + context = provideMap.get(parent) + break + } + } + + let val = context && context[key] + if (!val) { + val = treatDefaultAsFactory ? defaultValue() : defaultValue + } + return val +} + +export const onBeforeUnmount = (callback) => { + useEffect(() => { + return callback + }, []) +} + +export const onMounted = (callback) => { + useEffect(() => { + callback() + }, []) +} diff --git a/packages/react-icon/index.ts b/packages/react-icon/index.ts new file mode 100644 index 000000000..a31cf4e73 --- /dev/null +++ b/packages/react-icon/index.ts @@ -0,0 +1,39 @@ +import IconLoading from './src/loading' +import IconAdd from './src/add' +import IconCheck from './src/check' +import IconCheckedSur from './src/check' +import IconChevronDown from './src/chevron-down' +import IconClose from './src/close' +import IconError from './src/error' +import IconHalfSelect from './src/half-select' +import IconHelp from './src/help' +import IconSuccess from './src/success' +import IconWarning from './src/warning' + +export { + IconLoading, + IconAdd, + IconCheck, + IconCheckedSur, + IconChevronDown, + IconClose, + IconError, + IconHalfSelect, + IconHelp, + IconSuccess, + IconWarning +} + +export default { + IconLoading, + IconAdd, + IconCheck, + IconCheckedSur, + IconChevronDown, + IconClose, + IconError, + IconHalfSelect, + IconHelp, + IconSuccess, + IconWarning +} diff --git a/packages/react-icon/package.json b/packages/react-icon/package.json new file mode 100644 index 000000000..f3d799d2e --- /dev/null +++ b/packages/react-icon/package.json @@ -0,0 +1,16 @@ +{ + "name": "@opentiny/react-icon", + "version": "1.0.0", + "description": "", + "main": "index.ts", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@opentiny/react-common": "workspace:~", + "@opentiny/vue-theme": "workspace:~" + } +} diff --git a/packages/react-icon/src/add/index.ts b/packages/react-icon/src/add/index.ts new file mode 100644 index 000000000..4baf741e0 --- /dev/null +++ b/packages/react-icon/src/add/index.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2022 - present TinyVue Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Svg } from '@opentiny/react-common' +import { ReactComponent as AddLoading } from '@opentiny/vue-theme/svgs/add.svg' + +export default Svg({ name: 'AddLoading', component: AddLoading }) diff --git a/packages/react-icon/src/check/index.ts b/packages/react-icon/src/check/index.ts new file mode 100644 index 000000000..730745299 --- /dev/null +++ b/packages/react-icon/src/check/index.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2022 - present TinyVue Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Svg } from '@opentiny/react-common' +import { ReactComponent as Check } from '@opentiny/vue-theme/svgs/check.svg' + +export default Svg({ name: 'Check', component: Check }) diff --git a/packages/react-icon/src/checked-sur/index.ts b/packages/react-icon/src/checked-sur/index.ts new file mode 100644 index 000000000..5080c7240 --- /dev/null +++ b/packages/react-icon/src/checked-sur/index.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2022 - present TinyVue Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Svg } from '@opentiny/react-common' +import { ReactComponent as CheckedSur } from '@opentiny/vue-theme/svgs/checked-sur.svg' + +export default Svg({ name: 'CheckedSur', component: CheckedSur }) diff --git a/packages/react-icon/src/chevron-down/index.ts b/packages/react-icon/src/chevron-down/index.ts new file mode 100644 index 000000000..10378f131 --- /dev/null +++ b/packages/react-icon/src/chevron-down/index.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2022 - present TinyVue Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Svg } from '@opentiny/react-common' +import { ReactComponent as ChevronDown } from '@opentiny/vue-theme/svgs/chevron-down.svg' + +export default Svg({ name: 'ChevronDown', component: ChevronDown }) diff --git a/packages/react-icon/src/close/index.ts b/packages/react-icon/src/close/index.ts new file mode 100644 index 000000000..ea5aae990 --- /dev/null +++ b/packages/react-icon/src/close/index.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2022 - present TinyVue Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Svg } from '@opentiny/react-common' +import { ReactComponent as Close } from '@opentiny/vue-theme/svgs/close.svg' + +export default Svg({ name: 'Close', component: Close }) diff --git a/packages/react-icon/src/error/index.ts b/packages/react-icon/src/error/index.ts new file mode 100644 index 000000000..93a519a20 --- /dev/null +++ b/packages/react-icon/src/error/index.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2022 - present TinyVue Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Svg } from '@opentiny/react-common' +import { ReactComponent as Error } from '@opentiny/vue-theme/svgs/error.svg' + +export default Svg({ name: 'Error', component: Error }) diff --git a/packages/react-icon/src/half-select/index.ts b/packages/react-icon/src/half-select/index.ts new file mode 100644 index 000000000..e95220b3a --- /dev/null +++ b/packages/react-icon/src/half-select/index.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2022 - present TinyVue Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Svg } from '@opentiny/react-common' +import { ReactComponent as HalfSelect } from '@opentiny/vue-theme/svgs/halfselect.svg' + +export default Svg({ name: 'HalfSelect', component: HalfSelect }) diff --git a/packages/react-icon/src/help/index.ts b/packages/react-icon/src/help/index.ts new file mode 100644 index 000000000..aff6c1fe4 --- /dev/null +++ b/packages/react-icon/src/help/index.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2022 - present TinyVue Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Svg } from '@opentiny/react-common' +import { ReactComponent as Help } from '@opentiny/vue-theme/svgs/help.svg' + +export default Svg({ name: 'Help', component: Help }) diff --git a/packages/react-icon/src/loading/index.ts b/packages/react-icon/src/loading/index.ts new file mode 100644 index 000000000..d0c5098b7 --- /dev/null +++ b/packages/react-icon/src/loading/index.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2022 - present TinyVue Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Svg } from '@opentiny/react-common' +import { ReactComponent as IconLoading } from '@opentiny/vue-theme/svgs/loading.svg' + +export default Svg({ name: 'IconLoading', component: IconLoading }) diff --git a/packages/react-icon/src/success/index.ts b/packages/react-icon/src/success/index.ts new file mode 100644 index 000000000..c4a945975 --- /dev/null +++ b/packages/react-icon/src/success/index.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2022 - present TinyVue Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Svg } from '@opentiny/react-common' +import { ReactComponent as Success } from '@opentiny/vue-theme/svgs/success.svg' + +export default Svg({ name: 'Success', component: Success }) diff --git a/packages/react-icon/src/warning/index.ts b/packages/react-icon/src/warning/index.ts new file mode 100644 index 000000000..24e4ef591 --- /dev/null +++ b/packages/react-icon/src/warning/index.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2022 - present TinyVue Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Svg } from '@opentiny/react-common' +import { ReactComponent as Warning } from '@opentiny/vue-theme/svgs/warning.svg' + +export default Svg({ name: 'Warning', component: Warning }) diff --git a/packages/react/.depcheckrc.yaml b/packages/react/.depcheckrc.yaml new file mode 100644 index 000000000..2a795f308 --- /dev/null +++ b/packages/react/.depcheckrc.yaml @@ -0,0 +1,2 @@ +ignores: + - "@opentiny/react*" \ No newline at end of file diff --git a/packages/react/mobile-first.ts b/packages/react/mobile-first.ts new file mode 100644 index 000000000..ac4f830e1 --- /dev/null +++ b/packages/react/mobile-first.ts @@ -0,0 +1,14 @@ +import Alert from '@opentiny/react-alert/src/mobile-first' +import Button from '@opentiny/react-button/src/mobile-first' +import { $prefix } from '@opentiny/react-common' + +const components = [Alert, Button] + +export const version = '1.0.0' + +export { Alert, Button } + +export default { + Alert, + Button +} as any diff --git a/packages/react/package.json b/packages/react/package.json new file mode 100644 index 000000000..08431a6e4 --- /dev/null +++ b/packages/react/package.json @@ -0,0 +1,17 @@ +{ + "name": "@opentiny/react", + "version": "1.0.0", + "description": "", + "main": "index.ts", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@opentiny/react-common": "workspace:~", + "@opentiny/react-alert": "workspace:~", + "@opentiny/react-button": "workspace:~" + } +} \ No newline at end of file diff --git a/packages/react/readme.md b/packages/react/readme.md new file mode 100644 index 000000000..360cf7fbe --- /dev/null +++ b/packages/react/readme.md @@ -0,0 +1,313 @@ +# 开发文档 + +## 下载依赖 + +```bash +pnpm i +``` + +## 生成 react 组件入口 + +```bash +pnpm build:entry-react +``` + +## 本地启动 react 调试项目 + +```bash +pnpm dev:react +``` + +## 本地启动 react 文档项目 + +```bash +pnpm dev:react-site +``` + +## 打包 react 组件 + +```bash +pnpm build:ui-react +``` + +运行此命令后,会在 pacakges-react 产生打包产物 +一般是 + packages/dist-react/@opention/button + ... 单个组件产物 + packages/dist-react/@opention/react-common + packages/dist-react/@opention/react + packages/dist-react/@opention/react-icon + +命令参数:传入字符串参数列表可以指定只打包单个组件或多个特定组件,比如 + +```bash +pnpm build:ui-react button +``` + +默认不传的话,会打包所有组件,以及公共任务,比如 react-common、react-icon + +可以通过 -f 指定目标格式,默认 es,可选 es、cjs +可以通过 -t 指定目标版本,默认 18,现在 react 只支持 18 +可以通过 -s 指定发布 npm scope,默认是 opentiny +可以通过 -c 指定是否清空构建目录 +可以通过 --no-dts 指定不生成类型定义文件 + +## 发包 react 组件 + +```bash +pnpm pub:react +``` + +# 目录结构 + +## 打包 react 相关 + +```b +internals/cli + /build + /build-entry-react.ts (packages/react 目录下生成入口) + /build-ui-react.ts (packages/dist-react 下生成打包产物) + /create + /create-mapping-react.ts (packages 下生成构建任务列表 modules.json) + /common-mapping-react.json (定义一些公共的打包任务,如 react-common) +``` + +## 开发 react 模版文件相关 + +packages/react/src/[compName] 目录 + +一个组件模版的目录结构如下 + +```b +alert + /node_modules + /src + /index.ts + /pc.tsx + /mobile.tsx + /mobile-first.ts + /index.ts + /package.json +``` +alert/index 是组件入口 +pc、mobile、mobile-first 是三套模版 + +## 开发 react-icon 相关 + +packages/react-icon/src/[svgName] 目录 + +一个 svg 直接用一个 index.ts 创建 + +如:packages/react-icon/src/add/index.ts + +```ts +import { Svg } from '@opentiny/react-common' +import { ReactComponent as AddLoading } from '@opentiny/vue-theme/svgs/add.svg' + +export default Svg({ name: 'AddLoading', component: AddLoading }) +``` + +## 开发 react-common react 适配层相关 + +react-common 的目录如下,主要是适配层的文件 + +```b +packages/react-common + /src + /csscls.ts 操作样式类名的一些方法 + /event.ts 模拟 vue 事件系统 + /fiber.ts 对 fiber 的一些读取操作 + /reactive.ts 实现数据响应式 + /resolveProps.js 从 react 的 props 上解析事件或属性 + /svg-render.jsx 渲染 svg 组建的公共函数 + /utils.ts 工具函数 + /virtual-comp.jsx 虚拟组件,用于实现 vue 的指令系统 + /vm.js 用户模拟 vue 的 vm 对象 + /vue-hooks.js 用户模拟 vue 的钩子函数 +``` + +# 如何贡献一个 react 组建 + +对于 vue 已有的组件,全新的组件,建议先实现 vue 组件,再去适配 react 模版。 +因为 vue 和 react 共用同一套 renderless 目录底下的逻辑,renderless 中的逻辑,vue 侧是基于 vue 自身的,react 侧则是基于 react 模拟 vue api 的适配层的。所以,先用 vue 的思维实现 vue 组件,是比较可靠的。 + +## 开发 + +假如要实现的组件是 button +1. 在 packages/react/src 底下创建如下目录 + +```js +button + /node_modules + /src + /index.ts + /pc.tsx + /mobile.tsx + /mobile-first.ts + /index.ts + /package.json +``` + +2. 给 package.json 中添加相关依赖 + +```json + "dependencies": { + "@opentiny/vue-renderless": "workspace:~", // 必选 + "@opentiny/react-common": "workspace:~", // 必选 + "@opentiny/react-icon": "workspace:~", // 组件需用用 icon,从这里引入 + "@opentiny/vue-theme": "workspace:~" // 必须,组件的样式一般在这里定义 + } +``` + +3. 创建 pc.tsx/mobile.tsx/mobile-first.tsx,并实现组件模版 + +```tsx +import { renderless, api } from '@opentiny/vue-renderless/button/vue' +import { useSetup } from '@opentiny/react-common' +import '@opentiny/vue-theme/button/index.less' + +export default function Button(props) { + const { + circle, // 解析出一个属性,api 事先定义的 + type = 'default', // 解析出一个属性,有默认值 + } = props + + const defaultProps = Object.assign({ + type + }, props) // 合并默认值属性 + + const { + ref, + parent, + current: vm + } = useVm() // 生成 vm 和 parent 对象,给 useSetup 提供 vm、parent 对象 + // ref 是用来获取组件根部 dom ,进而获取 fiber 节点 + + const { + handleClick, // 从 renderless/**/button/index.ts 里定义的事件,会在 button/vue.ts + state, + a + } = useSetup({ + props: defaultProps, + renderless, + api, + vm, + parent + }) // 通过 useSetup 函数抹平差异并执行组件核心逻辑 renderless 函数 + // renderless 函数是通过 vue api 实现的组件逻辑 + // useSetup 中给 renderless 函数提供了模拟后的 api + + const $attrs = a(props, define_props, false) + // 过滤没有在组件上定义,但是传入的属性 + + return ( + + ) +} + +``` + +4. 创建多模版模式组建 + +button/src/index.ts +```jsx +import pc from './pc.jsx' +import mobile from './mobile.jsx' +import mobileFirst from './mobile-first.jsx' + +export default function (props) { + const { + tiny_mode = 'pc' + } = props + + const S = ({ + pc, + mobile, + 'mobile-first': mobileFirst + })[tiny_mode] + + return (S(props)) +} +``` + +5. 创建组建入口 +button/index.ts +```jsx +import Button from './src' + +export default Button +``` + +## 开发一个 react-icon 组件 + +在 packages/react-icon/src 下创建 iconName 目录 +中创建 index.ts 组件,内容如下: + +```ts +import { Svg } from '@opentiny/react-common' +import { ReactComponent as Check } from '@opentiny/vue-theme/svgs/check.svg' + +export default Svg({ name: 'Check', component: Check }) +``` + +# 用户使用文档 + +## 在项目中使用所有组件 + +- 1.下载整个组建库 +```bash +npm i @pe-3/react +``` + +- 2. 导入组件 +```js +import { Button as TinyButton, React as Tiny React } from '@pe-3/react' +``` + +- 3. 使用组件(查看 api 文档) +```js +function App() { + return (
+ 主要按钮 + +
) +} +``` + +## 在项目中使用单个组建 + +- 1. 下载单个组件 + +```bash +npm i @pe-3/react-button +npm i @pe-3/react-alert +``` + +- 2. 导入单个组件 +```js +import TintButton from '@pe-3/react-button' +import TintAlert from '@pe-3/react-alert' +``` + +- 3. 使用单个组件 +```js +function App() { + return (
+ 主要按钮 + +
) +} +``` + +## 组建 api 文档地址: + +https://opentiny.design/ + +## codesandbox + +https://codesandbox.io/s/hungry-bash-tlch6l?file=/src/App.js \ No newline at end of file diff --git a/packages/react/src/alert/index.ts b/packages/react/src/alert/index.ts new file mode 100644 index 000000000..9bc345130 --- /dev/null +++ b/packages/react/src/alert/index.ts @@ -0,0 +1,3 @@ +import Alert from './src' + +export default Alert diff --git a/packages/react/src/alert/package.json b/packages/react/src/alert/package.json new file mode 100644 index 000000000..b88dfae91 --- /dev/null +++ b/packages/react/src/alert/package.json @@ -0,0 +1,18 @@ +{ + "name": "@opentiny/react-alert", + "version": "1.0.0", + "description": "", + "main": "index.ts", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@opentiny/vue-renderless": "workspace:~", + "@opentiny/react-common": "workspace:~", + "@opentiny/react-icon": "workspace:~", + "@opentiny/vue-theme": "workspace:~" + } +} \ No newline at end of file diff --git a/packages/react/src/alert/src/index.ts b/packages/react/src/alert/src/index.ts new file mode 100644 index 000000000..4023bcdd1 --- /dev/null +++ b/packages/react/src/alert/src/index.ts @@ -0,0 +1,15 @@ +import pc from './pc.jsx' +import mobileFirst from './mobile-first.jsx' + +export default function (props) { + const { + tiny_mode = 'pc' + } = props + + const S = ({ + pc, + 'mobile-first': mobileFirst + })[tiny_mode] + + return (S(props)) +} diff --git a/packages/react/src/alert/src/mobile-first.jsx b/packages/react/src/alert/src/mobile-first.jsx new file mode 100644 index 000000000..15af50778 --- /dev/null +++ b/packages/react/src/alert/src/mobile-first.jsx @@ -0,0 +1,192 @@ +import { renderless, api } from '@opentiny/vue-renderless/alert/vue' +import { IconClose, IconSuccess, IconError, IconHelp, IconWarning, IconChevronDown } from '@opentiny/react-icon' +import { + vc, + If, + Component, + Slot, + useSetup, + useVm, +} from '@opentiny/react-common' + +const $constants = { + ICON_MAP: { + success: IconSuccess, + error: IconError, + info: IconHelp, + warning: IconWarning + }, + TITLE_MAP: { + success: 'ui.alert.success', + error: 'ui.alert.error', + info: 'ui.alert.info', + warning: 'ui.alert.warning' + }, + CONTENT_MAXHEUGHT: 252 +} + +export default function Alert(props) { + const { + type = 'success', + size = 'normal', + center = false, + showIcon = true, + description = '', + slots = {}, + _constants = $constants, + closable = true, + closeText, + title, + showFoldable = true, + singleLine, + scrolling + } = props + + const defaultProps = { + type, + size, + center, + showIcon, + description, + slots, + _constants, + closable, + closeText, + title, + showFoldable + } + + const { ref, current: vm, parent } = useVm() + + const { + state, + handleHeaderClick + } = useSetup({ + props: defaultProps, + renderless, + api, + constants: _constants, + vm, + parent + }) + + return (
+ +
+ + +
+ +
+ + {state.getTitle} + + + + +
+
+
+
+ {description} +
+
+ + +
+ +
+
+ +
+
+ +
+ +
+ {state.getTitle} +
+
+
+
+ {description} +
+
+ +
+ +
+
+
+
+ + + + + {closeText} + +
+
+
) +} diff --git a/packages/react/src/alert/src/pc.jsx b/packages/react/src/alert/src/pc.jsx new file mode 100644 index 000000000..b92a9d952 --- /dev/null +++ b/packages/react/src/alert/src/pc.jsx @@ -0,0 +1,128 @@ +import { renderless, api } from '@opentiny/vue-renderless/alert/vue' +import { IconClose, IconSuccess, IconError, IconHelp, IconWarning } from '@opentiny/react-icon' +import '@opentiny/vue-theme/alert/index.less' +import { + vc, + If, + Component, + Slot, + useSetup, + useVm +} from '@opentiny/react-common' + +const $constants = { + ICON_MAP: { + success: IconSuccess, + error: IconError, + info: IconHelp, + warning: IconWarning + }, + TITLE_MAP: { + success: 'ui.alert.success', + error: 'ui.alert.error', + info: 'ui.alert.info', + warning: 'ui.alert.warning' + }, + CONTENT_MAXHEUGHT: 252 +} + +export default function Alert(props) { + const { + type = 'success', + size = 'normal', + center = false, + showIcon = true, + description = '', + slots = {}, + _constants = $constants, + closable = true, + closeText, + title + } = props + + const defaultProps = { + type, + size, + center, + showIcon, + description, + slots, + _constants, + closable, + closeText, + title + } + + const { ref, current: vm, parent } = useVm() + + const { + state, + handleClose + } = useSetup({ + props: defaultProps, + renderless, + api, + constants: _constants, + vm, + parent + }) + + return ( +
+ +
+ +
+ +
+ + {state.getTitle} + +
+
+
+ + {description} + +
+ +
+ +
+
+
+ + + + + {closeText} + +
+
+
) +} diff --git a/packages/react/src/button/index.ts b/packages/react/src/button/index.ts new file mode 100644 index 000000000..6dada6f44 --- /dev/null +++ b/packages/react/src/button/index.ts @@ -0,0 +1,3 @@ +import Button from './src' + +export default Button diff --git a/packages/react/src/button/package.json b/packages/react/src/button/package.json new file mode 100644 index 000000000..6215539ca --- /dev/null +++ b/packages/react/src/button/package.json @@ -0,0 +1,19 @@ +{ + "name": "@opentiny/react-button", + "version": "1.0.0", + "description": "", + "main": "index.ts", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@opentiny/vue-renderless": "workspace:~", + "@opentiny/vue-theme": "workspace:~", + "@opentiny/react-common": "workspace:~", + "@opentiny/react-icon": "workspace:~", + "@opentiny/vue-theme-mobile": "workspace:~" + } +} diff --git a/packages/react/src/button/src/index.ts b/packages/react/src/button/src/index.ts new file mode 100644 index 000000000..623b65a02 --- /dev/null +++ b/packages/react/src/button/src/index.ts @@ -0,0 +1,17 @@ +import pc from './pc.jsx' +import mobile from './mobile.jsx' +import mobileFirst from './mobile-first.jsx' + +export default function (props) { + const { + tiny_mode = 'pc' + } = props + + const S = ({ + pc, + mobile, + 'mobile-first': mobileFirst + })[tiny_mode] + + return (S(props)) +} \ No newline at end of file diff --git a/packages/react/src/button/src/mobile-first.jsx b/packages/react/src/button/src/mobile-first.jsx new file mode 100644 index 000000000..1218fb0e0 --- /dev/null +++ b/packages/react/src/button/src/mobile-first.jsx @@ -0,0 +1,107 @@ +import { renderless, api } from '@opentiny/vue-renderless/button/vue' +import { useSetup, vc, If, Component, Slot, useVm } from '@opentiny/react-common' +import { IconLoading } from '@opentiny/react-icon' +import { classes } from './token' + +const define_props = [ + 'children', + 'text', + 'loading', + 'autofocus', + 'plain', + 'round', + 'circle', + 'icon', + 'size', + 'type', + 'nativeType', + 'resetTime', + /^on/ +] + +export default function Button(props) { + const { + loading, + autofocus, + size, + icon, + round, + circle, + href, + buttonClass, + tabindex, + text, + type, + nativeType = 'button', + resetTime = 1000 + } = props + + const defaultProps = Object.assign({ + nativeType, + resetTime, + }, props) + + const { + ref, + parent, + current: vm + } = useVm() + + const { + handleClick, + state, + a, + m, + gcls, + } = useSetup({ + api, + renderless, + props: defaultProps, + classes, + ref, + parent, + vm + }) + + const $attrs = a(props, define_props, false) + + return ( + + ) +} diff --git a/packages/react/src/button/src/mobile.jsx b/packages/react/src/button/src/mobile.jsx new file mode 100644 index 000000000..e464bbb1f --- /dev/null +++ b/packages/react/src/button/src/mobile.jsx @@ -0,0 +1,88 @@ +import { renderless, api } from '@opentiny/vue-renderless/button/vue' +import { useSetup, vc, If, Component, Slot, useVm } from '@opentiny/react-common' +import { IconLoading } from '@opentiny/react-icon' +import '@opentiny/vue-theme-mobile/button/index.less' + +const define_props = [ + 'children', + 'text', + 'loading', + 'autofocus', + 'plain', + 'round', + 'circle', + 'icon', + 'size', + 'type', + 'nativeType', + 'resetTime', + /^on/ +] + +export default function Button(props) { + const { + text, + loading, + round, + icon, + size, + type = 'default', + nativeType = 'button', + resetTime = 1000 + } = props + + const defaultProps = Object.assign({ + type, + nativeType, + resetTime + }, props) + + const { + ref, + parent, + current: vm + } = useVm() + + const { + handleClick, + state, + a + } = useSetup({ + props: defaultProps, + renderless, + api, + parent, + vm + }) + + const $attrs = a(props, define_props, false) + + return ( + + ) +} diff --git a/packages/react/src/button/src/pc.jsx b/packages/react/src/button/src/pc.jsx new file mode 100644 index 000000000..f3c322f04 --- /dev/null +++ b/packages/react/src/button/src/pc.jsx @@ -0,0 +1,99 @@ +import { renderless, api } from '@opentiny/vue-renderless/button/vue' +import { useSetup, If, Component, vc, useVm } from '@opentiny/react-common' +import { IconLoading } from '@opentiny/react-icon' +import '@opentiny/vue-theme/button/index.less' + +const define_props = [ + 'children', + 'text', + 'loading', + 'autofocus', + 'plain', + 'round', + 'circle', + 'icon', + 'size', + 'type', + 'nativeType', + 'resetTime', + /^on/ +] + +export default function Button(props) { + const { + children, + text, + loading, + autofocus, + round, + circle, + icon, + size, + tabindex, + type = 'default', + nativeType = 'button', + resetTime = 1000 + } = props + + const defaultProps = Object.assign({ + type, + nativeType, + resetTime + }, props) + + const { + ref, + parent, + current: vm + } = useVm() + + const { + handleClick, + state, + a + } = useSetup({ + props: defaultProps, + renderless, + api, + vm, + parent + }) + + const $attrs = a(props, define_props, false) + + return ( + + ) +} diff --git a/packages/react/src/button/src/token.ts b/packages/react/src/button/src/token.ts new file mode 100644 index 000000000..610775dd0 --- /dev/null +++ b/packages/react/src/button/src/token.ts @@ -0,0 +1,66 @@ +export const classes = { + 'button': + 'inline-block sm:max-w-[9rem] text-center overflow-hidden overflow-ellipsis whitespace-nowrap transition-button duration-300 delay-[0ms]', + 'size-default': 'h-10 text-sm sm:h-7 sm:text-xs', + 'size-medium': 'h-10 text-sm sm:h-8 sm:text-xs', + 'size-small': 'h-8 text-sm sm:h-7 sm:text-xs', + 'size-mini': 'h-7 sm:h-6 sm:text-xs', + 'type-default': + 'text-black border-color-border hover:border-color-border-hover active:border-color-border-active sm:cursor-pointer', + 'type-primary': + 'text-white border-color-brand bg-color-brand hover:border-color-brand-hover hover:bg-color-brand-hover active:border-color-brand-active active:bg-color-brand-active sm:cursor-pointer', + 'type-success': + 'text-white border-color-success bg-color-success hover:border-color-success-hover hover:bg-color-success-hover active:border-color-success-active active:bg-color-success-active sm:cursor-pointer', + 'type-info': + 'text-white border-color-info-secondary bg-color-info-secondary hover:border-color-info-secondary-hover hover:bg-color-info-secondary-hover active:border-color-info-secondary-active active:bg-color-info-secondary-active sm:cursor-pointer', + 'type-warning': + 'text-white border-color-warning bg-color-warning hover:border-color-warning-hover hover:bg-color-warning-hover active:border-color-warning-active active:bg-color-warning-active sm:cursor-pointer', + 'type-danger': + 'text-white border-color-error bg-color-error hover:border-color-error-hover hover:bg-color-error-hover active:border-color-error-active active:bg-color-error-active sm:cursor-pointer', + 'type-text': + 'border-none bg-transparent cursor-pointer text-color-text-placeholder active:text-color-text-primary sm:hover:text-color-text-primary sm:active:!text-color-brand-active', + 'type-default-disabled': 'text-color-text-disabled bg-color-bg-3 border-transparent hover:cursor-not-allowed', + 'type-primary-disabled': 'text-white bg-color-brand-disabled border-transparent hover:cursor-not-allowed', + 'type-success-disabled': 'text-white bg-color-success-disabled border-transparent hover:cursor-not-allowed', + 'type-info-disabled': 'text-white bg-color-info-secondary-disabled border-transparent hover:cursor-not-allowed', + 'type-warning-disabled': 'text-white bg-color-alert-disabled border-transparent hover:cursor-not-allowed', + 'type-danger-disabled': 'text-white bg-color-error-disabled border-transparent hover:cursor-not-allowed', + 'type-text-disabled': 'text-color-text-disabled hover:cursor-not-allowed', + 'type-default-plain': + 'text-black border-color-border hover:border-color-border-hover active:border-color-border-active sm:cursor-pointer', + 'type-primary-plain': + 'text-color-brand border-color-brand hover:text-color-brand-hover hover:border-color-brand-hover active:text-color-brand-active active:border-color-brand-active bg-white sm:cursor-pointer', + 'type-success-plain': + 'text-color-success border-color-success hover:text-color-success-hover hover:border-color-success-hover active:text-color-success-active active:border-color-success-active bg-white sm:cursor-pointer', + 'type-info-plain': + 'text-color-info-secondary border-color-info-secondary hover:text-color-info-secondary-hover hover:border-color-info-secondary-hover active:text-color-info-secondary-active active:border-color-info-secondary-active bg-white sm:cursor-pointer', + 'type-warning-plain': + 'text-color-warning border-color-warning hover:text-color-warning-hover hover:border-color-warning-hover active:text-color-warning-active active:border-color-warning-active bg-white sm:cursor-pointer', + 'type-danger-plain': + 'text-color-error border-color-error hover:text-color-error-hover hover:border-color-error-hover active:text-color-error-active active:border-color-error-active bg-white sm:cursor-pointer', + 'type-text-plain': 'text-color-brand hover:text-color-brand-hover active:text-color-brand-active', + 'type-default-plain-disabled': + 'text-color-text-disabled bg-white border-color-text-disabled hover:cursor-not-allowed', + 'type-primary-plain-disabled': + 'text-color-brand-disabled bg-white border-color-brand-disabled hover:cursor-not-allowed', + 'type-success-plain-disabled': + 'text-color-success-disabled bg-white border-color-success-disabled hover:cursor-not-allowed', + 'type-info-plain-disabled': + 'text-color-info-secondary-disabled bg-white border-color-info-secondary-disabled hover:cursor-not-allowed', + 'type-warning-plain-disabled': + 'text-color-alert-disabled bg-white border-color-alert-disabled hover:cursor-not-allowed', + 'type-danger-plain-disabled': + 'text-color-error-disabled bg-white border-color-error-disabled hover:cursor-not-allowed', + 'type-text-plain-disabled': 'text-color-text-disabled hover:cursor-not-allowed', + 'no-round': 'rounded-sm', + 'is-round': 'rounded-full', + 'is-border': 'border-0.5 sm:border', + 'no-circle': 'sm:min-w-[4.5rem] pl-3 pr-3', + 'is-circle': 'sm:min-w-[0.4375rem] sm:rounded-full sm:pl-2 sm:pr-2', + 'button-icon': '-mt-0.5 sm:text-base fill-current', + 'button-icon-default': 'text-color-icon-primary hover:text-color-icon-hover active:text-color-icon-active', + 'button-icon-disabled': 'text-color-icon-disabled hover:cursor-not-allowed', + 'loading-svg': 'animate-spin-2 mr-1 fill-current -inset-0.5', + 'button-link': + 'text-color-link hover:text-color-link-hover active:color-link-hover active:hover:text-color-link-hover sm:hover:text-color-link-hover' +} diff --git a/packages/renderless/src/checkbox/react.ts b/packages/renderless/src/checkbox/react.ts new file mode 100644 index 000000000..35b03dc10 --- /dev/null +++ b/packages/renderless/src/checkbox/react.ts @@ -0,0 +1,22 @@ +// import { +// addToStore, +// removeFromStore, +// handleChange, +// computedGetModelGet, +// computedGetModelSet, +// computedIsChecked, +// computedIsGroup, +// computedStore, +// computedIsLimitDisabled, +// computedIsDisabled, +// computedIsDisplayOnly, +// computedIsGroupDisplayOnly, +// computedFormItemSize, +// computedCheckboxSize, +// mounted, +// toggleEvent, +// dispatchDisplayedValue, +// getDisplayedValue, +// computedIsShowText, +// computedShowText +// } from './index' diff --git a/packages/renderless/tsconfig.json b/packages/renderless/tsconfig.json index 58a950440..6e8d56081 100644 --- a/packages/renderless/tsconfig.json +++ b/packages/renderless/tsconfig.json @@ -25,5 +25,5 @@ }, "include": [ "src/**/*.ts" - ] +, "src/button/react.js" ] } diff --git a/packages/theme/scripts/build-component-cssvar.js b/packages/theme/scripts/build-component-cssvar.js index ddf50c7d3..de723fa30 100644 --- a/packages/theme/scripts/build-component-cssvar.js +++ b/packages/theme/scripts/build-component-cssvar.js @@ -3,6 +3,20 @@ const fg = require('fast-glob') const ignoreNames = ['base', 'theme'] +function groupBy(array, fn) { + const map = {} + array.forEach((item) => { + const key = fn(item) + const collection = map[key] + if (!collection) { + map[key] = [item] + } else { + collection.push(item) + } + }) + return map +} + fg(['**/*-theme.js']).then((files) => { const components = files .map((file) => { @@ -40,17 +54,3 @@ fg(['**/*-theme.js']).then((files) => { fs.writeFileSync(`src/theme/${themeKey}/component.js`, contents) }) }) - -function groupBy(array, fn) { - const map = {} - array.forEach((item) => { - const key = fn(item) - const collection = map[key] - if (!collection) { - map[key] = [item] - } else { - collection.push(item) - } - }) - return map -} diff --git a/packages/vue-common/src/adapter/vue2/index.ts b/packages/vue-common/src/adapter/vue2/index.ts index e26bccf07..2d9eb47cd 100644 --- a/packages/vue-common/src/adapter/vue2/index.ts +++ b/packages/vue-common/src/adapter/vue2/index.ts @@ -216,6 +216,7 @@ const createVm = (vm, instance, context = undefined) => { export const tools = (context, mode) => { const instance = hooks.getCurrentInstance()?.proxy as any + console.log(instance) const root = instance?.$root const { route, router } = useRouter(instance) const i18n = root?.$i18n