diff --git a/packages/duoyun-ui/docs/en/01-guide/06-integrate.md b/packages/duoyun-ui/docs/en/01-guide/06-integrate.md new file mode 100644 index 00000000..a79afbb9 --- /dev/null +++ b/packages/duoyun-ui/docs/en/01-guide/06-integrate.md @@ -0,0 +1,64 @@ +# Used in projects like React + +[Most front-end frameworks/libraries](https://custom-elements-everywhere.com/) can seamlessly use custom elements, such as assigning Attribute/Property, registering events, +However, there is no [type hint](https://code.visualstudio.com/docs/editor/intellisense) when using custom elements directly, and ESLint is not supported, so DuoyunUI re-exported it and perfectly adapted to React/Vue/Svelte. + +## React + +> [!NOTE] +> Only the experimental version of React supports custom elements. Use `npm install react@experimental react-dom@experimental` to install the experimental version of React. + +Use DuoyunUI like any other React component library: + + + +## Vue + +DuoyunUI also exports Vue components, which are used the same as React. The only difference is that the path is changed from `react` to `vue`, +in addition, custom elements need to be specified in the Vue configuration file: + +```js +{ + compilerOptions: { + isCustomElement: (tag) => tag.startsWith('dy-'); + } +} +``` + +In the Vue project, it also supports directly writing custom elements, but [distinguish](../02-elements/card#api) is it Attribute or Property: + + + +## Svelte + +DuoyunUI does not re-export as a Svelte component, and you can use the custom element directly: + + + +> [!NOTE] +> Use the `Sveltekit`, please make sure the `Svelte` is installed as a `dependencies` instead of `DevDependenCies`, otherwise the type cannot be import successfully; +> if you compile the error of "Unexpected token 'export'", please add the following code to `vite.config.ts`: +> +> ```js +> { +> ssr: { +> noExternal: ['@mantou/gem', 'duoyun-ui']; +> } +> } +> ``` + +## SSR + +DuoyunUI does not support SSR, to be precise, Next/Nuxt/Svelte do not support SSR for custom elements. The ShadowDOM of custom elements is generated at runtime. To ensure proper server-side rendering, import `@mantou/gem/helper/ssr-shim` at the entry point of your front-end code. + +- Next.js: `pages/_app.tsx` +- Nuxt.js: `app.config.ts` +- SvelteKit: `src/hooks.server.ts` + +If you want to use DuoyunUI routing, you can use ``. To avoid layout issues on the initial page load, add the following global styles: + +```css +:not(:defined) { + display: none; +} +``` diff --git a/packages/duoyun-ui/docs/zh/01-guide/06-integrate.md b/packages/duoyun-ui/docs/zh/01-guide/06-integrate.md new file mode 100644 index 00000000..cc8b072b --- /dev/null +++ b/packages/duoyun-ui/docs/zh/01-guide/06-integrate.md @@ -0,0 +1,65 @@ +# 在 React 等项目中使用 + +[大多数前端框架/库](https://custom-elements-everywhere.com/)都能无缝使用自定义元素,比如分配 Attribute/Property,注册事件, +但是直接使用自定义元素没有[类型提示](https://code.visualstudio.com/docs/editor/intellisense)、不支持 ESLint,所以 DuoyunUI 进行了重导出,完美适配了 React/Vue/Svelte。 + +## React + +> [!NOTE] +> React 的实验版才支持自定义元素,使用 `npm install react@experimental react-dom@experimental` 安装 React 实验版。 + +跟使用其他 React 组件库一样使用 DuoyunUI: + + + +## Vue + +DuoyunUI 也导出了 Vue 组件,使用和 React 一样,唯一的区别是路径将 `react` 改成 `vue`, +另外需要在 Vue 配置文件中指定自定义元素: + +```js +{ + compilerOptions: { + isCustomElement: (tag) => tag.startsWith('dy-'); + } +} +``` + +在 Vue 中也支持直接写自定义元素,但是要[区分](../02-elements/card#api)是 Attribute 还是 Property: + + + +## Svelte + +DuoyunUI 没有重导出为 Svelte 组件,直接使用自定义元素即可: + + + +> [!NOTE] +> 使用 `SvelteKit` 请确保 `svelte` 安装成 `dependencies` 而非 `devDependencies`,否则类型不能成功导入; +> 如果编译出现“Unexpected token 'export'”的错误请在 `vite.config.ts` 中添加下面代码: +> +> ```js +> { +> ssr: { +> noExternal: ['@mantou/gem', 'duoyun-ui']; +> } +> } +> ``` + +## SSR + +DuoyunUI 不支持 SSR,确切的说是 Next/Nuxt/Svelte 不支持自定义元素 SSR,自定义元素的 ShadowDOM 是运行时生成, +为了能在服务端正确运行,在前端代码的入口位置导入 `@mantou/gem/helper/ssr-shim`: + +- Next.js: `pages/_app.tsx` +- Nuxt.js: `app.config.ts` +- SvelteKit: `src/hooks.server.ts` + +如果要使用 DuoyunUI 的路由,可以使用 ``。为了避免首屏排版错误添加下面全局样式: + +```css +:not(:defined) { + display: none; +} +``` diff --git a/packages/duoyun-ui/gem-book.cli.json b/packages/duoyun-ui/gem-book.cli.json index 769c38b4..3d32ae22 100644 --- a/packages/duoyun-ui/gem-book.cli.json +++ b/packages/duoyun-ui/gem-book.cli.json @@ -3,6 +3,6 @@ "icon": "../../logo.png", "i18n": true, "sourceBranch": "docs", - "plugin": ["media", "api", "sandpack"], + "plugin": ["media", "api", "sandpack", "raw"], "debug": true } diff --git a/packages/duoyun-ui/package.json b/packages/duoyun-ui/package.json index 401b42a7..052bee47 100644 --- a/packages/duoyun-ui/package.json +++ b/packages/duoyun-ui/package.json @@ -1,6 +1,6 @@ { "name": "duoyun-ui", - "version": "1.0.1", + "version": "1.0.2", "description": "WebComponts UI", "exports": { "./elements/*": "./elements/*.js", diff --git a/packages/duoyun-ui/src/elements/space.ts b/packages/duoyun-ui/src/elements/space.ts index 3ef68d79..3c700920 100644 --- a/packages/duoyun-ui/src/elements/space.ts +++ b/packages/duoyun-ui/src/elements/space.ts @@ -24,7 +24,7 @@ const style = createCSSSheet(css` @customElement('dy-space') @adoptedStyle(style) export class DuoyunSpaceElement extends GemElement { - @attribute size: 'nomal' | 'small' | 'large'; + @attribute size: 'normal' | 'small' | 'large'; constructor() { super({ isLight: true }); diff --git a/packages/gem-book/src/element/elements/main.ts b/packages/gem-book/src/element/elements/main.ts index 22a253bd..9fad2768 100644 --- a/packages/gem-book/src/element/elements/main.ts +++ b/packages/gem-book/src/element/elements/main.ts @@ -191,15 +191,7 @@ const style = createCSSSheet(css` opacity: 1; } code { - font-family: - Source Code Pro, - ui-monospace, - SFMono-Regular, - Menlo, - Monaco, - Consolas, - Liberation Mono, - Courier New, + font-family: Source Code Pro, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace; padding: 0.15em 0.4em 0.1em; font-size: 0.9em; @@ -244,6 +236,9 @@ export class Main extends GemElement { .link:hover { border-color: currentColor; } + .link svg:last-child { + margin-inline-end: 0.2em; + } `; #hashChangeHandle = () => { diff --git a/packages/gem-book/src/element/elements/pre.ts b/packages/gem-book/src/element/elements/pre.ts index 70b35efb..76436935 100644 --- a/packages/gem-book/src/element/elements/pre.ts +++ b/packages/gem-book/src/element/elements/pre.ts @@ -381,7 +381,7 @@ export class Pre extends GemElement { ? this.#getRanges(this.range).map(([start, end]) => { let result = ''; for (let i = start - 1; i < (end || lines.length); i++) { - result += lines[i] + '\n'; + result += (lines[i] || '') + '\n'; } return result; }) diff --git a/packages/gem-book/src/plugins/raw.ts b/packages/gem-book/src/plugins/raw.ts index b6d44913..600183eb 100644 --- a/packages/gem-book/src/plugins/raw.ts +++ b/packages/gem-book/src/plugins/raw.ts @@ -1,26 +1,63 @@ import type { GemBookElement } from '../element'; +type State = { + content: string; + error?: any; +}; + customElements.whenDefined('gem-book').then(() => { const { GemBookPluginElement } = customElements.get('gem-book') as typeof GemBookElement; - const { config, Gem, devMode } = GemBookPluginElement; - const { attribute, customElement } = Gem; + const { config, Gem, devMode, theme } = GemBookPluginElement; + const { attribute, customElement, html, createCSSSheet, css, adoptedStyle } = Gem; + + const style = createCSSSheet(css` + :host { + display: contents; + } + .loading, + .error { + display: flex; + place-items: center; + place-content: center; + min-height: 20em; + background: rgba(${theme.textColorRGB}, 0.05); + } + .loading { + opacity: 0.5; + } + .error { + color: ${theme.cautionColor}; + } + @keyframes display { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } + } + gem-book-pre { + animation: display 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards; + } + `); @customElement('gbp-raw') - class _GbpRawElement extends GemBookPluginElement { + @adoptedStyle(style) + class _GbpRawElement extends GemBookPluginElement { @attribute src: string; @attribute codelang: string; @attribute range: string; @attribute highlight: string; - mounted() { - this.#renderContent(); - } + state: State = { + content: '', + }; - updated() { - this.#renderContent(); + get #codelang() { + return this.codelang || this.src.split('.').pop() || ''; } - #getRemoteUrl() { + get #url() { if (!this.src) return ''; let url = this.src; @@ -35,19 +72,33 @@ customElements.whenDefined('gem-book').then(() => { return url; } - async #renderContent() { - if (!this.src) return; - this.innerHTML = 'Loading...'; + mounted() { + this.effect( + async () => { + if (!this.src) return; + this.setState({ error: false }); + try { + const resp = await fetch(this.#url); + if (resp.status === 404) throw new Error(resp.statusText || 'Not Found'); + this.setState({ content: await resp.text() }); + } catch (error) { + this.setState({ error }); + } + }, + () => [this.src], + ); + } - const text = await (await fetch(this.#getRemoteUrl())).text(); - const div = document.createElement('div'); - div.textContent = text; - const content = div.innerHTML; + render() { + const { content, error } = this.state; + if (error) return html`
${error}
`; + if (!content) return html`
Loading...
`; - const extension = this.src.split('.').pop() || ''; - this.innerHTML = `${content}`; + return html` + ${content} + `; } } }); diff --git a/packages/gem-devtools/src/elements/section.ts b/packages/gem-devtools/src/elements/section.ts index 178f826d..d9575482 100644 --- a/packages/gem-devtools/src/elements/section.ts +++ b/packages/gem-devtools/src/elements/section.ts @@ -15,7 +15,7 @@ import { Item, Path, BuildIn, panelStore } from '../store'; import { theme } from '../theme'; import { inspectValue } from '../scripts/inspect-value'; import { execution } from '../common'; -import { setValue } from '../scripts/set-value'; +import { setGemPropValue } from '../scripts/set-value'; const maybeBuildInPrefix = '[[Gem?]] '; const buildInPrefix = '[[Gem]] '; @@ -214,13 +214,16 @@ export class Section extends GemElement { ? [kebabToCamelCase(item.name)] : [item.name]; const onInput = (evt: Event) => { - execution(setValue, [path, (evt.target as HTMLInputElement).value]); + execution(setGemPropValue, [path, (evt.target as HTMLInputElement).value]); }; const onInputNumber = (evt: Event) => { - execution(setValue, [path, Number((evt.target as HTMLInputElement).value)]); + execution(setGemPropValue, [path, Number((evt.target as HTMLInputElement).value) || 0]); }; const toggleValue = (evt: MouseEvent) => { - execution(setValue, [path, (evt.target as HTMLInputElement).textContent?.trim() === 'true' ? false : true]); + execution(setGemPropValue, [ + path, + (evt.target as HTMLInputElement).textContent?.trim() === 'true' ? false : true, + ]); }; switch (item.type) { case 'string': diff --git a/packages/gem-devtools/src/scripts/set-value.ts b/packages/gem-devtools/src/scripts/set-value.ts index db3e8529..e2d48f9e 100644 --- a/packages/gem-devtools/src/scripts/set-value.ts +++ b/packages/gem-devtools/src/scripts/set-value.ts @@ -1,9 +1,13 @@ import { Path } from '../store'; -export const setValue = (path: Path, value: string | number | boolean | null) => { +declare let $0: any; + +export const setGemPropValue = (path: Path, value: string | number | boolean | null) => { const key = String(path.pop()); const obj = window.__GEM_DEVTOOLS__PRELOAD__.readProp(path); obj[key] = value; + + $0.update(); }; diff --git a/packages/gem-examples/package.json b/packages/gem-examples/package.json index 8afebda2..a351dbd4 100644 --- a/packages/gem-examples/package.json +++ b/packages/gem-examples/package.json @@ -9,7 +9,7 @@ }, "dependencies": { "@mantou/gem": "^1.7.1", - "duoyun-ui": "^1.0.1" + "duoyun-ui": "^1.0.2" }, "devDependencies": { "@gemjs/config": "^1.6.11",