From e0ad1d0f3735cad99b6ca6f6ac772034f2398dd8 Mon Sep 17 00:00:00 2001 From: xiaomin_hp Date: Mon, 28 Sep 2026 23:21:04 +0800 Subject: [PATCH] feat: add edentext-vue package with Vue 3 integration for EdenText editor - Introduced a new Vue 3 wrapper for the EdenText editor, allowing for easy integration and usage within Vue applications. - Created the main component `EdentextEditor.vue` to handle the editor's lifecycle and expose necessary APIs. - Added supporting files including styles, types, and configuration for building the package. - Implemented a demo application to showcase the usage of the `EdentextEditor` component. - Included SVG assets for favicon and branding. - Established a build process using Vite for packaging the library. --- .gitignore | 3 + README.md | 33 ++ docs/edentext-vue.md | 255 ++++++++++++++++ examples/edentext-vue-demo/README.md | 19 ++ examples/edentext-vue-demo/index.html | 13 + examples/edentext-vue-demo/package.json | 21 ++ .../public/assets/EdenText.png | Bin 0 -> 9895 bytes .../public/assets/favicon.svg | 23 ++ examples/edentext-vue-demo/public/favicon.svg | 4 + examples/edentext-vue-demo/src/App.vue | 31 ++ examples/edentext-vue-demo/src/demo.css | 23 ++ examples/edentext-vue-demo/src/main.ts | 5 + examples/edentext-vue-demo/vite.config.ts | 15 + package-lock.json | 289 +++++++++++++++++- package.json | 14 +- packages/edentext-vue/README.md | 29 ++ packages/edentext-vue/package.json | 33 ++ scripts/prepare-edentext-vue-package.mjs | 13 + src/App.svelte | 73 ++++- src/lib/components/AboutDialog.svelte | 11 +- src/lib/components/ribbon/Ribbon.svelte | 9 +- src/lib/editor/extensions/tableHeaderRow.ts | 3 +- src/lib/native/bootstrap.ts | 35 +++ src/lib/native/types.ts | 34 +++ src/lib/vue/EdentextEditor.vue | 102 +++++++ src/lib/vue/edentext-vue.css | 6 + src/lib/vue/index.ts | 6 + src/lib/vue/types.ts | 28 ++ src/main.ts | 15 +- tests/browser.mjs | 10 +- tsconfig.json | 3 +- tsconfig.lib.json | 20 ++ vite.lib.config.ts | 57 ++++ 33 files changed, 1198 insertions(+), 37 deletions(-) create mode 100644 docs/edentext-vue.md create mode 100644 examples/edentext-vue-demo/README.md create mode 100644 examples/edentext-vue-demo/index.html create mode 100644 examples/edentext-vue-demo/package.json create mode 100644 examples/edentext-vue-demo/public/assets/EdenText.png create mode 100644 examples/edentext-vue-demo/public/assets/favicon.svg create mode 100644 examples/edentext-vue-demo/public/favicon.svg create mode 100644 examples/edentext-vue-demo/src/App.vue create mode 100644 examples/edentext-vue-demo/src/demo.css create mode 100644 examples/edentext-vue-demo/src/main.ts create mode 100644 examples/edentext-vue-demo/vite.config.ts create mode 100644 packages/edentext-vue/README.md create mode 100644 packages/edentext-vue/package.json create mode 100644 scripts/prepare-edentext-vue-package.mjs create mode 100644 src/lib/native/bootstrap.ts create mode 100644 src/lib/native/types.ts create mode 100644 src/lib/vue/EdentextEditor.vue create mode 100644 src/lib/vue/edentext-vue.css create mode 100644 src/lib/vue/index.ts create mode 100644 src/lib/vue/types.ts create mode 100644 tsconfig.lib.json create mode 100644 vite.lib.config.ts diff --git a/.gitignore b/.gitignore index edc30c69..9452286e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ .vite debug coverage +output +tmp-vue-native.png +tmp-vue-native-fixed.png dist docs/others node_modules diff --git a/README.md b/README.md index 1cde46be..6d0360fc 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,39 @@ npm run build # production build → dist/ Built with Svelte 5, TypeScript, Vite and TipTap 3 (ProseMirror). +## Vue 3 package + +The repository also builds the browser-only Vue 3 integration package +`edentext-vue`. It mounts the original EdenText `App.svelte` unchanged and +exposes only a thin command bridge for new/open/save-as and language settings. +The Vue layer does not rebuild the Ribbon or maintain a second document model. + +```bash +npm install edentext-vue vue +``` + +```vue + + + +``` + +Build and inspect the publishable package with `npm run build:lib` and +`npm pack ./packages/edentext-vue --dry-run`. See +[`docs/edentext-vue.md`](docs/edentext-vue.md) for the Vue API and npm release +workflow. + ## Self-hosting There is no backend and no state on the server; documents stay in the browser. diff --git a/docs/edentext-vue.md b/docs/edentext-vue.md new file mode 100644 index 00000000..ba6e24da --- /dev/null +++ b/docs/edentext-vue.md @@ -0,0 +1,255 @@ +# EdenText Vue 3 npm 包使用与发布指南 + +`edentext-vue` 是 EdenText 原生 Web 应用的 Vue 3 薄封装。Vue 组件只负责挂载和销毁原生 `App.svelte`,以及把少量宿主命令转发给原生 EdenText:新建、打开、保存、另存、界面语言和文档默认语言。 + +Ribbon、编辑器、分页、页眉页脚、表格、图片、审阅、查找替换、模板、ODT/DOCX/PDF 等功能全部来自原生 EdenText,不在 Vue 层重新实现。 + +## 对外 API + +### 安装 + +```bash +npm install edentext-vue vue +``` + +`vue` 是 peer dependency。该包只支持 Vue 3 和浏览器,不支持 SSR、Node 服务端转换,也不包含 React 入口。 + +### 最小调用 + +宿主必须给组件一个明确高度: + +```vue + + + + + +``` + +组件内部显示的是 EdenText 原生 UI,不需要也不应该在 Vue 页面外部重新做一个工具栏。 + +### Props + +```ts +interface EdentextEditorProps { + /** 原生 EdenText 界面语言。可选:en、de、es、fr、pt、ru、ja、zh-Hans、zh-Hant。 */ + locale?: UiLocale; + /** 文档默认语言、拼写检查语言和导出语言。可选值由 EdenText 的语言表决定。 */ + documentLanguage?: string; + /** 挂载完成后调用一次原生“新建文档”,用于打开空文档。 */ + initialNewDocument?: boolean; + /** 可选:EdenText.png 和 favicon.svg 所在的静态资源目录。 */ + assetBaseUrl?: string; +} +``` + +`locale` 和 `documentLanguage` 在组件挂载后发生变化时,会调用原生 EdenText 的语言设置函数,不会重建编辑器,也不会替换正文。 + +### 实例 API + +```ts +interface EdentextEditorApi { + newDocument(): void; + + /** 打开原生文件选择器。 */ + openFile(): Promise; + + /** 复用原生导入器打开 ODT/DOCX。filename 用于判断格式和显示文档名。 */ + openDocument( + source: File | Blob | ArrayBuffer | Uint8Array, + filename?: string, + ): Promise; + + /** 调用原生 Save;已打开文件会保存回原文件,未保存文件会进入原生保存流程。 */ + save(): Promise; + + /** 调用原生 Save As;由原生 UI 选择目标文件或触发浏览器下载。 */ + saveAs(format: 'odt' | 'docx'): Promise; + + /** 调用原生导出流程;ODT/DOCX 走 Save As,PDF 走 EdenText 原生 PDF 导出。 */ + exportDocument(format: 'odt' | 'docx' | 'pdf'): Promise; + + setUiLocale(locale: UiLocale): void; + setDocumentLanguage(language: string): void; + focus(): void; + destroy(): void; +} +``` + +通过 Vue 模板 ref 使用: + +```vue + + + +``` + +`ready` 事件只在原生 EdenText editor 实例真正创建完成后触发;在此之前不要调用实例方法。`error` 事件报告挂载错误。组件卸载时会自动卸载 Svelte 原生应用和其资源;通常不需要手动调用 `destroy()`,手动调用只适用于宿主明确要提前销毁的场景。 + +### 新建、打开、另存和语言 + +```ts +editor.value?.newDocument(); +await editor.value?.openFile(); +await editor.value?.openDocument(bytes, '合同.docx'); +await editor.value?.save(); +await editor.value?.saveAs('odt'); +await editor.value?.exportDocument('pdf'); +editor.value?.setUiLocale('zh-Hans'); +editor.value?.setDocumentLanguage('zh-CN'); +``` + +这些方法都调用原生 EdenText 已有的命令,因此确认弹窗、密码处理、最近文件、格式识别、File System Access API 和浏览器下载行为保持原生一致。 + +Vue 层不提供 `v-model`,也不暴露 TipTap、ProseMirror、Svelte 实例或另外一套 JSON 文档状态。宿主需要把文件上传到服务端时,可以在调用 `openDocument` 前自行读取文件;导出文件则通过原生 `save`/`saveAs` 流程完成。 + +## 仓库内运行 Vue demo + +```bash +npm install +npm run build:lib +npm run demo:vue +``` + +然后访问 Vite 输出的本地地址。demo 只创建一个 Vue 容器并挂载 `EdentextEditor`,不创建额外 header 或工具栏;`initial-new-document` 会让原生 EdenText 打开空文档。 + +npm 部署时如果使用包内 logo/icon,建议把 `dist/assets/EdenText.png` 和 `dist/assets/favicon.svg` 复制到宿主静态目录,并通过 `asset-base-url` 指向该目录。 + +也可以只构建 demo: + +```bash +npm run build:demo:vue +``` + +## 构建 npm 包 + +根目录执行: + +```bash +npm install +npm run check:lib +npm run build:lib +npm pack ./packages/edentext-vue --dry-run +``` + +`build:lib` 会完成三件事: + +1. 用 `vite.lib.config.ts` 构建 ESM 入口; +2. 用 `vue-tsc` 输出 `dist/vue/index.d.ts` 等声明文件; +3. 把字体、Hunspell 词典、同义词库和许可证资源复制到 `dist/assets/`。 + +产物位于 `packages/edentext-vue/dist/`,包的导出约定为: + +```json +{ + "name": "edentext-vue", + "peerDependencies": { "vue": "^3.4.0" }, + "exports": { + ".": { + "types": "./dist/vue/index.d.ts", + "import": "./dist/index.js" + }, + "./style.css": "./dist/style.css", + "./assets/*": "./dist/assets/*" + } +} +``` + +## 发布到 npm + +先登录并确认包名可用: + +```bash +npm login +npm whoami +npm view edentext-vue name +``` + +修改 `packages/edentext-vue/package.json` 的版本号,然后执行发布前检查: + +```bash +npm run check +npm run check:lib +npm test +npm run build:app +npm run build:lib +npm pack ./packages/edentext-vue --dry-run +``` + +确认 dry-run 内容后发布: + +```bash +npm publish ./packages/edentext-vue --access public +``` + +`prepack` 会把根目录的 `LICENSE` 和 `THIRD-PARTY-NOTICES.md` 放入包目录,`postpack` 会清理临时文件。发布前应确认 tarball 至少包含: + +- `dist/index.js` +- `dist/vue/index.d.ts` +- `dist/style.css` +- `dist/assets/*` +- `LICENSE` +- `THIRD-PARTY-NOTICES.md` + +发布后用干净的 Vue 3 项目验证: + +```bash +npm install vue edentext-vue +``` + +验证原生 Ribbon、空文档、输入编辑、原生打开、保存/另存、UI 国际化、内容默认语言、字体和词典资源均正常加载。 + +## 设计边界 + +- Vue 是集成入口,不是第二个编辑器实现。 +- 原生 EdenText UI 和功能继续由 `src/App.svelte` 维护。 +- 不发布 React 组件,不发布 `@edentext/core`。 +- 浏览器端运行;不支持 SSR 和 Node 服务端文档转换。 +- 许可证为 `AGPL-3.0-only`。 diff --git a/examples/edentext-vue-demo/README.md b/examples/edentext-vue-demo/README.md new file mode 100644 index 00000000..7fe055ba --- /dev/null +++ b/examples/edentext-vue-demo/README.md @@ -0,0 +1,19 @@ +# EdenText Vue Demo + +This is a standalone Vue 3 demo that consumes the local `edentext-vue` package +through the same package entry used by npm consumers. + +```bash +npm install --no-package-lock +npm run dev +``` + +From the repository root, the equivalent command is: + +```bash +npm run demo:vue +``` + +Open the printed Vite URL. The demo renders only the Vue host and the native +EdenText application. Its original Ribbon, editor, pagination and status bar +are visible, and `initial-new-document` starts with an empty document. diff --git a/examples/edentext-vue-demo/index.html b/examples/edentext-vue-demo/index.html new file mode 100644 index 00000000..b6728103 --- /dev/null +++ b/examples/edentext-vue-demo/index.html @@ -0,0 +1,13 @@ + + + + + + + EdenText Vue Demo + + +
+ + + diff --git a/examples/edentext-vue-demo/package.json b/examples/edentext-vue-demo/package.json new file mode 100644 index 00000000..535f74f1 --- /dev/null +++ b/examples/edentext-vue-demo/package.json @@ -0,0 +1,21 @@ +{ + "name": "edentext-vue-demo", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1", + "build": "vite build", + "typecheck": "vue-tsc --noEmit" + }, + "dependencies": { + "edentext-vue": "file:../../packages/edentext-vue", + "vue": "^3.5.43" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.9", + "typescript": "^6.0.3", + "vite": "^8.0.14", + "vue-tsc": "^3.3.11" + } +} diff --git a/examples/edentext-vue-demo/public/assets/EdenText.png b/examples/edentext-vue-demo/public/assets/EdenText.png new file mode 100644 index 0000000000000000000000000000000000000000..8bef119068632f9b961d377377c79e6920fbdc5a GIT binary patch literal 9895 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 D%SsXy literal 0 HcmV?d00001 diff --git a/examples/edentext-vue-demo/public/assets/favicon.svg b/examples/edentext-vue-demo/public/assets/favicon.svg new file mode 100644 index 00000000..e3ea910d --- /dev/null +++ b/examples/edentext-vue-demo/public/assets/favicon.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/edentext-vue-demo/public/favicon.svg b/examples/edentext-vue-demo/public/favicon.svg new file mode 100644 index 00000000..4ada9567 --- /dev/null +++ b/examples/edentext-vue-demo/public/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/examples/edentext-vue-demo/src/App.vue b/examples/edentext-vue-demo/src/App.vue new file mode 100644 index 00000000..bd193ee5 --- /dev/null +++ b/examples/edentext-vue-demo/src/App.vue @@ -0,0 +1,31 @@ + + + diff --git a/examples/edentext-vue-demo/src/demo.css b/examples/edentext-vue-demo/src/demo.css new file mode 100644 index 00000000..4e2f9340 --- /dev/null +++ b/examples/edentext-vue-demo/src/demo.css @@ -0,0 +1,23 @@ +html, +body, +#app { + width: 100%; + height: 100%; + min-width: 960px; + margin: 0; +} + +body { + overflow: hidden; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +.demo-root { + width: 100%; + height: 100%; +} diff --git a/examples/edentext-vue-demo/src/main.ts b/examples/edentext-vue-demo/src/main.ts new file mode 100644 index 00000000..59b43e7b --- /dev/null +++ b/examples/edentext-vue-demo/src/main.ts @@ -0,0 +1,5 @@ +import { createApp } from 'vue'; +import App from './App.vue'; +import './demo.css'; + +createApp(App).mount('#app'); diff --git a/examples/edentext-vue-demo/vite.config.ts b/examples/edentext-vue-demo/vite.config.ts new file mode 100644 index 00000000..8f69f842 --- /dev/null +++ b/examples/edentext-vue-demo/vite.config.ts @@ -0,0 +1,15 @@ +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; + +const demoRoot = dirname(fileURLToPath(import.meta.url)); + +export default defineConfig({ + plugins: [vue()], + server: { + fs: { + allow: [resolve(demoRoot, '../..')], + }, + }, +}); diff --git a/package-lock.json b/package-lock.json index 7344b798..689e052e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "devDependencies": { "@sveltejs/vite-plugin-svelte": "^7.1.2", "@tsconfig/svelte": "^5.0.8", + "@vitejs/plugin-vue": "^6.0.9", "@vitest/coverage-v8": "^4.1.10", "dictionary-de": "^3.0.0", "dictionary-en": "^4.0.0", @@ -58,7 +59,9 @@ "svelte-check": "^4.7.1", "typescript": "^6.0.3", "vite": "^8.0.14", - "vitest": "^4.1.9" + "vitest": "^4.1.9", + "vue": "^3.5.43", + "vue-tsc": "^3.3.11" } }, "node_modules/@asamuzakjp/css-color": { @@ -1219,6 +1222,23 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.9", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.9.tgz", + "integrity": "sha512-rD/MORlhaZMlXWW0rEn4FB4wMinWC0z/D7Ye160S5+Rs1mC8ZDAdcDp4SjKfUhG5t8q1ktcPVw4xuTkPRlzrSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, "node_modules/@vitest/coverage-v8": { "version": "4.1.10", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz", @@ -1363,6 +1383,185 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.43.tgz", + "integrity": "sha512-zdiLhnbe1QQqgDT8xZMpNmyqZ3qlI+/Q/FHQco57Kwl/b05HhCzN6eVGN9QU9rbga4CrS0H5SYY8VGHZCt/1Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/shared": "3.5.43", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-core/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@vue/compiler-core/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.43.tgz", + "integrity": "sha512-PEZoAk3NQmsn/ejMzSOCyTYqwGqczrWm70PuhBKjjv1+TCoQAaO/zOqNwjV+honlNstT5ILxtc+8r8UUfj+iEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.43", + "@vue/shared": "3.5.43" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.43.tgz", + "integrity": "sha512-FCbrG3XNCRl+js3huuKx4IVHBLTvMkJhVepjbxSPu1gn4yWLaYtGNQdjJGZaMytXB6qb76qQDDmDSLy/vkmleQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/compiler-core": "3.5.43", + "@vue/compiler-dom": "3.5.43", + "@vue/compiler-ssr": "3.5.43", + "@vue/shared": "3.5.43", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.28", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.43.tgz", + "integrity": "sha512-GF62orf7KiJX9RqrHNGrYBudsQGD0OhJ5nDs90O8UiDuS40+YMYomiXu6w6EuvtXuRDc3MSNis3EaaSKAVSWpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.43", + "@vue/shared": "3.5.43" + } + }, + "node_modules/@vue/language-core": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.11.tgz", + "integrity": "sha512-QJmpliwAVpC/OxubIByPAhNzsQPRc8/gxlN2qnVzVfIMjMDz/9RnXRFoetjz5yEgXVXyp4LqhXq3V53PjmNzFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.2.1", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.4" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.43.tgz", + "integrity": "sha512-G/c9GyOZNI2jVaaS6OX1EF1SSFSv7H0ERqNTl4+DTFMlZmB5eVAB53aLQNam/7NL2NPtaDD7RdVrzf8uJzMuOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.43" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.43.tgz", + "integrity": "sha512-hU6U6VnVhBGQDpvlnnDlIB8ZGJBiOcgk2lh/0InltHiz3D8oSkluvuvY+do1G2H3+udeKFsmaBlgVYP7gXQzEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.43", + "@vue/shared": "3.5.43" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.43.tgz", + "integrity": "sha512-Bb2Jc0YjjJdMt1SJmb9b2L/IWd3I8lIT9x9eS/xvvP9CiVgna0ffua74xKRmt4/uSJ+0r4iN8ex1jrqkhQGWQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.43", + "@vue/runtime-core": "3.5.43", + "@vue/shared": "3.5.43", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.43.tgz", + "integrity": "sha512-l2Ygjv9NehV94PSBxNWsAHC0j/eIIKbn92mBuWXAPGLnn6HfJ6MH5ubsd+Nk0YoZ5FRuxWI1P2VSoh+dbPQhCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.43", + "@vue/runtime-dom": "3.5.43", + "@vue/shared": "3.5.43" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.43.tgz", + "integrity": "sha512-uksS7YGMR5NZyr4JNq0Rp+QyLns0ueaz20KwzIPW9R0LH1Vnt4E+XUM29PNseEbf1www2gOuhuDi5AKOIXag9Q==", + "dev": true, + "license": "MIT" + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -1376,6 +1575,13 @@ "node": ">=0.4.0" } }, + "node_modules/alien-signals": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.2.1.tgz", + "integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==", + "dev": true, + "license": "MIT" + }, "node_modules/aria-query": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", @@ -1541,6 +1747,13 @@ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/data-urls": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", @@ -2464,10 +2677,17 @@ "node": ">=4" } }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", "dev": true, "funding": [ { @@ -2541,6 +2761,13 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -2589,9 +2816,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", "dev": true, "funding": [ { @@ -2609,7 +2836,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -3434,6 +3661,52 @@ } } }, + "node_modules/vscode-uri": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.2.0.tgz", + "integrity": "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.43.tgz", + "integrity": "sha512-o5qZoksdnjIKvW1srZ3ab7pcDNYAerBjRe54D0LBLfRdCYFrSgBHVXokMas35czQc0//lmx4/tuY4ZNQ+Rf2Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.43", + "@vue/compiler-sfc": "3.5.43", + "@vue/runtime-dom": "3.5.43", + "@vue/server-renderer": "3.5.43", + "@vue/shared": "3.5.43" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-tsc": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.11.tgz", + "integrity": "sha512-gOb0B9rtU2+f1dszwPqSH5kAieIF9ReeLhD3kSRNHv5WZZUQz/JdVXW0RTdqhNTMlQkqKzrTTviqKr/4FYZraQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.28", + "@vue/language-core": "3.3.11" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, "node_modules/w3c-keyname": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", diff --git a/package.json b/package.json index 702c6c86..033ae413 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,14 @@ "scripts": { "dev": "vite --host", "build": "vite build", + "build:app": "vite build", + "build:lib": "vite build --config vite.lib.config.ts && vue-tsc -p tsconfig.lib.json --declaration --emitDeclarationOnly --outDir packages/edentext-vue/dist", + "demo:vue": "npm --prefix examples/edentext-vue-demo run dev", + "build:demo:vue": "npm --prefix examples/edentext-vue-demo run build", "preview": "vite preview", "check": "svelte-check --tsconfig ./tsconfig.json", + "check:lib": "vue-tsc -p tsconfig.lib.json --noEmit", + "pack:lib": "npm run build:lib && npm pack ./packages/edentext-vue", "test": "vitest run", "test:watch": "vitest", "test:lo": "vitest run tests/lo-roundtrip.test.ts tests/lo-fuzz.test.ts tests/lo-render-consistency.test.ts tests/lo-password.test.ts", @@ -30,6 +36,7 @@ "devDependencies": { "@sveltejs/vite-plugin-svelte": "^7.1.2", "@tsconfig/svelte": "^5.0.8", + "@vitejs/plugin-vue": "^6.0.9", "@vitest/coverage-v8": "^4.1.10", "dictionary-de": "^3.0.0", "dictionary-en": "^4.0.0", @@ -43,7 +50,9 @@ "svelte-check": "^4.7.1", "typescript": "^6.0.3", "vite": "^8.0.14", - "vitest": "^4.1.9" + "vitest": "^4.1.9", + "vue": "^3.5.43", + "vue-tsc": "^3.3.11" }, "dependencies": { "@noble/ciphers": "^2.4.0", @@ -78,5 +87,8 @@ "jspdf": "^4.2.1", "odf-kit": "^0.13.5", "utif2": "^4.1.0" + }, + "allowScripts": { + "core-js@3.49.0": true } } diff --git a/packages/edentext-vue/README.md b/packages/edentext-vue/README.md new file mode 100644 index 00000000..4c597e24 --- /dev/null +++ b/packages/edentext-vue/README.md @@ -0,0 +1,29 @@ +# edentext-vue + +Vue 3 thin wrapper for the original EdenText browser editor. It mounts the +native EdenText UI and exposes only host commands such as new, open, save as, +UI language and document language. It does not recreate the toolbar and does +not expose TipTap, ProseMirror or Svelte APIs. + +```vue + + + +``` + +The package is browser-only, supports Vue 3, contains no React entry or React +dependency, and is licensed under AGPL-3.0-only. See the repository guide at +[`docs/edentext-vue.md`](../../docs/edentext-vue.md) for the complete API and +npm publishing workflow. diff --git a/packages/edentext-vue/package.json b/packages/edentext-vue/package.json new file mode 100644 index 00000000..aadb90e4 --- /dev/null +++ b/packages/edentext-vue/package.json @@ -0,0 +1,33 @@ +{ + "name": "edentext-vue", + "version": "0.1.0", + "description": "A browser-only Vue 3 rich-text document editor with ODT, DOCX and PDF export.", + "type": "module", + "license": "AGPL-3.0-only", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/vue/index.d.ts", + "files": [ + "dist", + "README.md", + "LICENSE", + "THIRD-PARTY-NOTICES.md" + ], + "scripts": { + "prepack": "node ../../scripts/prepare-edentext-vue-package.mjs", + "postpack": "node ../../scripts/prepare-edentext-vue-package.mjs --clean" + }, + "exports": { + ".": { + "types": "./dist/vue/index.d.ts", + "import": "./dist/index.js" + }, + "./style.css": "./dist/style.css", + "./assets/*": "./dist/assets/*", + "./package.json": "./package.json" + }, + "peerDependencies": { + "vue": "^3.4.0" + }, + "sideEffects": ["./dist/style.css"] +} diff --git a/scripts/prepare-edentext-vue-package.mjs b/scripts/prepare-edentext-vue-package.mjs new file mode 100644 index 00000000..021b41f1 --- /dev/null +++ b/scripts/prepare-edentext-vue-package.mjs @@ -0,0 +1,13 @@ +import { copyFile, rm } from 'node:fs/promises'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const packageDir = resolve(root, 'packages/edentext-vue'); +const files = ['LICENSE', 'THIRD-PARTY-NOTICES.md']; + +if (process.argv.includes('--clean')) { + await Promise.all(files.map((name) => rm(resolve(packageDir, name), { force: true }))); +} else { + await Promise.all(files.map((name) => copyFile(resolve(root, name), resolve(packageDir, name)))); +} diff --git a/src/App.svelte b/src/App.svelte index 18032c69..803c2ab5 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -76,7 +76,9 @@ import StyleManagerDialog from './lib/components/StyleManagerDialog.svelte'; import NoteOptionsDialog from './lib/components/NoteOptionsDialog.svelte'; import SaveFormatDialog from './lib/components/SaveFormatDialog.svelte'; - import { t, locale } from './lib/i18n/i18n.svelte'; + import { t, locale, setLocale } from './lib/i18n/i18n.svelte'; + import type { Locale } from './lib/i18n/config'; + import type { NativeEdenTextApi, NativeEdenTextAppProps } from './lib/native/types'; import { fnv1a } from './lib/utils/hash'; import { withShortcut } from './lib/i18n/shortcut'; import { DEFAULT_SHORTCUTS, matchesEvent, shortcutHint } from './lib/editor/shortcuts'; @@ -94,6 +96,23 @@ launchQueue?: { setConsumer: (c: (p: { files: FileSystemFileHandle[] }) => void) => void }; }; + // App.svelte remains the native EdenText application. These props only provide a + // narrow command bridge for framework wrappers; they do not replace any native + // toolbar, editor, pagination, or document state. + let { + initialUiLocale, + initialDocumentLanguage, + initialNewDocument = false, + assetBaseUrl, + onReady, + }: NativeEdenTextAppProps = $props(); + let nativeApiReady = false; + + function appAsset(name: string): string { + const base = assetBaseUrl?.replace(/\/$/, ''); + return base ? `${base}/${name}` : name; + } + let editor: Editor | null = $state(null); let tick: number = $state(0); let currentPage: number = $state(1); @@ -260,6 +279,10 @@ ({ main: documentLanguage, other: documentLanguageOther } = pickDocumentLanguage(documentLanguage, documentLanguageOther, code)); } + function setUiLocale(next: Locale): void { + setLocale(next); + } + // The document name (without .odt). Source of truth for the save filename; // set on open, editable in the header, blank → heading-derived fallback. let documentName: string = $state(loadDocName()); @@ -708,9 +731,9 @@ void clearEmbeddedFontStore(); } - function handleNew() { + function handleNew(skipConfirm = false) { if (!editor) return; - if (isDocNonEmpty() && !confirm(t().dialogs.confirmNew)) return; + if (!skipConfirm && isDocNonEmpty() && !confirm(t().dialogs.confirmNew)) return; loadContent('

'); // onUpdate fires → autosave documentEpoch++; resetHistory(); @@ -1005,6 +1028,43 @@ await applyImport(new Uint8Array(await file.arrayBuffer()), null, file.name); } + // Framework wrappers pass bytes into the same importer used by the native file + // picker. No parallel Vue document model is created here. + async function openDocument(source: File | Blob | ArrayBuffer | Uint8Array, filename?: string): Promise { + const bytes = source instanceof Uint8Array + ? source + : source instanceof ArrayBuffer + ? new Uint8Array(source) + : new Uint8Array(await source.arrayBuffer()); + const sourceName = filename + ?? (typeof File !== 'undefined' && source instanceof File ? source.name : undefined) + ?? 'document.odt'; + await applyImport(bytes, null, sourceName); + } + + // The Vue wrapper receives this object after the real native editor exists. The + // methods below call the same functions wired to EdenText's own ribbon buttons. + $effect(() => { + if (nativeApiReady || !editor) return; + nativeApiReady = true; + if (initialUiLocale) setUiLocale(initialUiLocale); + if (initialDocumentLanguage) setDocumentLanguage(initialDocumentLanguage); + if (initialNewDocument) handleNew(true); + + const api: NativeEdenTextApi = { + newDocument: () => handleNew(), + openFile: () => handleOpen(), + openDocument, + save: () => handleSave(), + saveAs: (format) => handleSaveAs(format), + exportDocument: (format) => format === 'pdf' ? handleExportPdf() : handleSaveAs(format), + setUiLocale, + setDocumentLanguage, + focus: () => editor?.commands.focus(), + }; + queueMicrotask(() => onReady?.(api)); + }); + // Both exporters take the same document-wide arguments, and every save path needs // one of them. The exporter module loads on first use. function exportLanguage() { @@ -1392,6 +1452,7 @@ onNewComment={addComment} {navigatorOpen} onToggleNavigator={() => (navigatorOpen = !navigatorOpen)} + {assetBaseUrl} /> {:else} @@ -1400,7 +1461,7 @@
@@ -1438,7 +1499,7 @@
-
- + diff --git a/src/lib/components/AboutDialog.svelte b/src/lib/components/AboutDialog.svelte index 4de2f98b..0af0c938 100644 --- a/src/lib/components/AboutDialog.svelte +++ b/src/lib/components/AboutDialog.svelte @@ -2,7 +2,12 @@ import { t } from '../i18n/i18n.svelte'; const REPO_URL = 'https://github.com/stffnb/edentext'; - let { open = $bindable(false) }: { open?: boolean } = $props(); + let { open = $bindable(false), assetBaseUrl = '' }: { open?: boolean; assetBaseUrl?: string } = $props(); + + function appAsset(name: string): string { + const base = assetBaseUrl.replace(/\/$/, ''); + return base ? `${base}/${name}` : name; + } let dialogEl: HTMLDialogElement | null = $state(null); @@ -40,9 +45,9 @@ - + - +

{t().about.tagline}

diff --git a/src/lib/components/ribbon/Ribbon.svelte b/src/lib/components/ribbon/Ribbon.svelte index 102b91ea..9345a10b 100644 --- a/src/lib/components/ribbon/Ribbon.svelte +++ b/src/lib/components/ribbon/Ribbon.svelte @@ -86,6 +86,7 @@ onNew, onNewFromTemplate, onOpen, onSave, onSaveAs, onSaveTemplate, onExportPdf, onPrintPdf, onPrint, onAbout, onDocProperties, onProtect, onAutoCorrect, onAutoText, onNewComment, navigatorOpen = false, onToggleNavigator, recentFiles = [], onOpenRecent, onForgetRecent, + assetBaseUrl = '', }: { editor: Editor | null; tick: number; @@ -143,6 +144,7 @@ onDocProperties?: () => void; onProtect?: () => void; hasPassword?: boolean; + assetBaseUrl?: string; onAutoCorrect?: () => void; onAutoText?: () => void; onNewComment?: () => void; @@ -150,6 +152,11 @@ onToggleNavigator?: () => void; } = $props(); + function appAsset(name: string): string { + const base = assetBaseUrl.replace(/\/$/, ''); + return base ? `${base}/${name}` : name; + } + const TABS = ['home', 'insert', 'layout', 'references', 'review', 'view'] as const; const CONTEXTUAL = ['tableDesign', 'tableLayout', 'pictureFormat', 'shapeFormat', 'headerFooter'] as const; type Tab = (typeof TABS)[number] | (typeof CONTEXTUAL)[number]; @@ -470,7 +477,7 @@
diff --git a/src/lib/editor/extensions/tableHeaderRow.ts b/src/lib/editor/extensions/tableHeaderRow.ts index 0c45a76c..dc31a55d 100644 --- a/src/lib/editor/extensions/tableHeaderRow.ts +++ b/src/lib/editor/extensions/tableHeaderRow.ts @@ -78,7 +78,8 @@ export function toggleHeaderStyle(axis: HeaderAxis) { return ({ state, tr, dispatch, commands }: CommandProps): boolean => { // A styled table: flip the Table Style Option the gallery's checkbox shows. if (styledTable(state)) { - return commands.setTableLook(LOOK_REGION[axis], !isHeaderStyled(state, axis)); + return (commands as unknown as { setTableLook: (region: string, enabled: boolean) => boolean }) + .setTableLook(LOOK_REGION[axis], !isHeaderStyled(state, axis)); } const info = headerCellPositions(state, axis); if (!info) return false; diff --git a/src/lib/native/bootstrap.ts b/src/lib/native/bootstrap.ts new file mode 100644 index 00000000..bea3e4fa --- /dev/null +++ b/src/lib/native/bootstrap.ts @@ -0,0 +1,35 @@ +import { locale } from '../i18n/i18n.svelte'; +import { applyTheme, loadTheme } from '../storage/theme'; + +let users = 0; +let stopThemeListener: (() => void) | null = null; + +/** + * Starts the small browser bootstrap that the original Svelte entry performs + * before mounting App.svelte. Keeping it here lets the Vue host mount the + * original application without duplicating its CSS/theme initialization. + */ +export function startNativeApp(): () => void { + if (users++ === 0) { + applyTheme(loadTheme()); + document.documentElement.lang = locale(); + + const media = window.matchMedia('(prefers-color-scheme: dark)'); + const onThemeChange = () => { + if (loadTheme() === 'auto') applyTheme('auto'); + }; + media.addEventListener('change', onThemeChange); + stopThemeListener = () => media.removeEventListener('change', onThemeChange); + } + + let active = true; + return () => { + if (!active) return; + active = false; + users = Math.max(0, users - 1); + if (users === 0) { + stopThemeListener?.(); + stopThemeListener = null; + } + }; +} diff --git a/src/lib/native/types.ts b/src/lib/native/types.ts new file mode 100644 index 00000000..6f84c583 --- /dev/null +++ b/src/lib/native/types.ts @@ -0,0 +1,34 @@ +import type { DocumentFormat } from '../storage/documentName'; +import type { DocumentLanguage } from '../storage/documentLanguage'; +import type { Locale } from '../i18n/config'; + +/** + * Commands exposed by the native EdenText application surface. + * + * This deliberately contains commands, not TipTap, ProseMirror, Svelte, or a + * second document model. Consumers embed the original EdenText UI and ask it + * to perform the same actions that its own ribbon performs. + */ +export interface NativeEdenTextApi { + newDocument(): void; + openFile(): Promise; + openDocument(source: File | Blob | ArrayBuffer | Uint8Array, filename?: string): Promise; + save(): Promise; + saveAs(format: DocumentFormat): Promise; + exportDocument(format: DocumentFormat | 'pdf'): Promise; + setUiLocale(locale: Locale): void; + setDocumentLanguage(language: DocumentLanguage): void; + focus(): void; +} + +export interface NativeEdenTextAppProps { + /** Initial chrome language. The native UI picker remains the source of truth. */ + initialUiLocale?: Locale; + /** Initial spell-check/export language for the current document. */ + initialDocumentLanguage?: DocumentLanguage; + /** Start with the native application's blank-document command after mount. */ + initialNewDocument?: boolean; + /** Optional base URL for native logo/icon assets when the package is self-hosted. */ + assetBaseUrl?: string; + onReady?: (api: NativeEdenTextApi) => void; +} diff --git a/src/lib/vue/EdentextEditor.vue b/src/lib/vue/EdentextEditor.vue new file mode 100644 index 00000000..6d95e4f1 --- /dev/null +++ b/src/lib/vue/EdentextEditor.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/src/lib/vue/edentext-vue.css b/src/lib/vue/edentext-vue.css new file mode 100644 index 00000000..72b39bdd --- /dev/null +++ b/src/lib/vue/edentext-vue.css @@ -0,0 +1,6 @@ +/* Host-only styles. The editor UI itself is EdenText's native App.svelte CSS. */ +.edentext-vue-host { + width: 100%; + height: 100%; + min-height: 320px; +} diff --git a/src/lib/vue/index.ts b/src/lib/vue/index.ts new file mode 100644 index 00000000..5445fa55 --- /dev/null +++ b/src/lib/vue/index.ts @@ -0,0 +1,6 @@ +import EdentextEditor from './EdentextEditor.vue'; + +export { EdentextEditor }; +export type { EdentextEditorApi, EdentextEditorProps, UiLocale, DocumentFormat, DocumentLanguage, NativeEdenTextApi } from './types'; + +export default EdentextEditor; diff --git a/src/lib/vue/types.ts b/src/lib/vue/types.ts new file mode 100644 index 00000000..e5ee83fe --- /dev/null +++ b/src/lib/vue/types.ts @@ -0,0 +1,28 @@ +import type { DocumentFormat } from '../storage/documentName'; +import type { DocumentLanguage } from '../storage/documentLanguage'; +import type { Locale } from '../i18n/config'; +import type { NativeEdenTextApi } from '../native/types'; + +/** Props for the thin Vue host around the original EdenText application. */ +export interface EdentextEditorProps { + /** EdenText chrome language; the native language picker remains available. */ + locale?: Locale; + /** Default language of the document content and spell checker. */ + documentLanguage?: DocumentLanguage; + /** Run the native New command once the original editor has mounted. */ + initialNewDocument?: boolean; + /** Base URL containing EdenText.png and favicon.svg, if assets are hosted separately. */ + assetBaseUrl?: string; +} + +/** + * The public Vue API is a command surface over native EdenText. It intentionally + * does not expose TipTap, ProseMirror, Svelte, or a second content model. + */ +export interface EdentextEditorApi extends NativeEdenTextApi { + /** Unmounts the native EdenText application from the Vue host. */ + destroy(): void; +} + +export type UiLocale = Locale; +export type { DocumentFormat, DocumentLanguage, NativeEdenTextApi }; diff --git a/src/main.ts b/src/main.ts index 238dd524..c28d89dd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,7 @@ import { mount } from 'svelte'; import App from './App.svelte'; import './styles/global.css'; -import { loadTheme, applyTheme } from './lib/storage/theme'; -import { locale } from './lib/i18n/i18n.svelte'; +import { startNativeApp } from './lib/native/bootstrap'; import { startTabPresence, pruneOldDocuments } from './lib/storage/docScope'; // This tab holds its document while it lives; the ones no tab has held for a while @@ -10,16 +9,8 @@ import { startTabPresence, pruneOldDocuments } from './lib/storage/docScope'; startTabPresence(); pruneOldDocuments(); -// Apply saved theme before mount to prevent flash of wrong theme -applyTheme(loadTheme()); - -// Resolve the UI locale before mount and reflect it on (chrome a11y) -document.documentElement.lang = locale(); - -// Keep 'auto' in sync when system theme changes -window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { - if (loadTheme() === 'auto') applyTheme('auto'); -}); +// Apply the same native theme/chrome bootstrap used by the Vue host. +startNativeApp(); // Offline support (public/sw.js). Not in dev, where a worker would serve the cached // build over the one being edited. diff --git a/tests/browser.mjs b/tests/browser.mjs index ceb06060..12555baf 100644 --- a/tests/browser.mjs +++ b/tests/browser.mjs @@ -9,6 +9,8 @@ import * as playwright from 'playwright-core'; export const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); export const MOD = process.platform === 'darwin' ? 'Meta' : 'Control'; +const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; +const npxCommand = process.platform === 'win32' ? 'npx.cmd' : 'npx'; export function checker() { const failures = []; @@ -26,8 +28,8 @@ export async function previewServer(port) { console.log('no dist/, building…'); execSync('npm run build', { cwd: ROOT, stdio: 'inherit' }); } - const proc = spawn('npm', ['run', 'preview', '--', '--port', String(port), '--strictPort'], - { cwd: ROOT, stdio: 'ignore', detached: true }); + const proc = spawn(npmCommand, ['run', 'preview', '--', '--port', String(port), '--strictPort'], + { cwd: ROOT, stdio: 'ignore', detached: true, shell: process.platform === 'win32' }); for (let i = 0; i < 60; i++) { await new Promise((r) => setTimeout(r, 500)); if (await fetch(`http://localhost:${port}/`).then(() => true).catch(() => false)) return proc; @@ -40,8 +42,8 @@ export async function previewServer(port) { export async function devServer(port) { const up = () => fetch(`http://localhost:${port}/`).then(() => true).catch(() => false); if (await up()) return null; - const proc = spawn('npx', ['vite', '--port', String(port), '--strictPort'], - { cwd: ROOT, stdio: 'ignore', detached: true }); + const proc = spawn(npxCommand, ['vite', '--port', String(port), '--strictPort'], + { cwd: ROOT, stdio: 'ignore', detached: true, shell: process.platform === 'win32' }); for (let i = 0; i < 120 && !(await up()); i++) await new Promise((r) => setTimeout(r, 500)); if (!(await up())) throw new Error('dev server did not start'); return proc; diff --git a/tsconfig.json b/tsconfig.json index 1b2a4f19..26f7d93c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,5 +11,6 @@ "moduleDetection": "force", "strict": true }, - "include": ["src/**/*.ts", "src/**/*.svelte", "src/vite-env.d.ts"] + "include": ["src/**/*.ts", "src/**/*.svelte", "src/vite-env.d.ts"], + "exclude": ["src/lib/vue/**/*"] } diff --git a/tsconfig.lib.json b/tsconfig.lib.json new file mode 100644 index 00000000..c4a9bf3e --- /dev/null +++ b/tsconfig.lib.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "packages/edentext-vue/dist", + "rootDir": "src/lib", + "allowJs": false, + "checkJs": false, + "types": ["vite/client", "svelte"] + }, + "include": [ + "src/lib/vue/**/*.ts", + "src/lib/vue/**/*.vue", + "src/lib/native/**/*.ts", + "src/vite-env.d.ts" + ], + "exclude": [] +} diff --git a/vite.lib.config.ts b/vite.lib.config.ts new file mode 100644 index 00000000..facdbe13 --- /dev/null +++ b/vite.lib.config.ts @@ -0,0 +1,57 @@ +import { fileURLToPath } from 'node:url'; +import { cp } from 'node:fs/promises'; +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; +import { svelte } from '@sveltejs/vite-plugin-svelte'; +import pkg from './package.json' with { type: 'json' }; + +const projectRoot = fileURLToPath(new URL('.', import.meta.url)); +const packageRoot = fileURLToPath(new URL('./packages/edentext-vue/', import.meta.url)); + +function copyPackageAssets() { + return { + name: 'copy-edentext-package-assets', + async closeBundle() { + const assets = [ + ['src/assets/fonts', 'dist/assets/fonts'], + ['public/dictionaries', 'dist/assets/dictionaries'], + ['public/thesaurus', 'dist/assets/thesaurus'], + ['public/font-licenses', 'dist/assets/font-licenses'], + ['public/EdenText.png', 'dist/assets/EdenText.png'], + ['public/favicon.svg', 'dist/assets/favicon.svg'], + ] as const; + for (const [source, target] of assets) { + await cp(`${projectRoot}/${source}`, `${packageRoot}/${target}`, { recursive: true, force: true }); + } + }, + }; +} + +export default defineConfig({ + plugins: [vue({ include: [/\.vue$/] }), svelte({ include: [/\.svelte$/] }), copyPackageAssets()], + publicDir: false, + build: { + outDir: 'packages/edentext-vue/dist', + emptyOutDir: true, + lib: { + entry: fileURLToPath(new URL('./src/lib/vue/index.ts', import.meta.url)), + name: 'EdentextVue', + formats: ['es'], + fileName: 'index', + cssFileName: 'style', + }, + rollupOptions: { + external: ['vue'], + output: { assetFileNames: (asset) => asset.name === 'style.css' ? 'style.css' : 'assets/[name]-[hash][extname]' }, + }, + }, + define: { + __APP_VERSION__: JSON.stringify(pkg.version), + }, + resolve: { + alias: { + 'hunspell-asm': 'hunspell-asm/dist/cjs/index.js', + 'emscripten-wasm-loader': 'emscripten-wasm-loader/dist/cjs/index.js', + }, + }, +});