diff --git a/CHANGELOG.md b/CHANGELOG.md index 360a85469..265f56ba4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,38 @@ -# Changelog (03/11/2025) +# Changelog (15/06/2026) Changelog was generated by [Generi](https://github.com/betterwrite/generi). Any questions, consult the documentation. +### v1.4.0 + +* **π§ fix(pdf):** cover with correct creator field size - [[d398c258](https://github.com/Novout/betterwrite/commit/d398c258)] +* **π§ chore:** update epub-gen3 - [[316d0ebe](https://github.com/Novout/betterwrite/commit/316d0ebe)] +* **πΏ ci:** fix stubs in runner text - [[ead66f46](https://github.com/Novout/betterwrite/commit/ead66f46)] +* **π feat:** landing background - [[8d09f5d1](https://github.com/Novout/betterwrite/commit/8d09f5d1)] +* **π feat:** some features for editor and preferences - [[8f3b8091](https://github.com/Novout/betterwrite/commit/8f3b8091)] +* **π§ test:** use file in setup - [[249d0738](https://github.com/Novout/betterwrite/commit/249d0738)] +* **π§ chore:** epub in app package field - [[5fd1435b](https://github.com/Novout/betterwrite/commit/5fd1435b)] +* **π§ chore:** update vite to v8 - [[34f392d8](https://github.com/Novout/betterwrite/commit/34f392d8)] +* **π© refactor:** choice microphone - [[003e717f](https://github.com/Novout/betterwrite/commit/003e717f)] +* **π© refactor!:** voice typing implement - [[9db0712a](https://github.com/Novout/betterwrite/commit/9db0712a)] +* **π© refactor:** unique bionic option - [[537c0d57](https://github.com/Novout/betterwrite/commit/537c0d57)] +* **π§ fix:** gap in header principal item - [[e3099f3b](https://github.com/Novout/betterwrite/commit/e3099f3b)] +* **π§ chore:** lint - [[43d38269](https://github.com/Novout/betterwrite/commit/43d38269)] +* **π© refactor:** alert erros in def components - [[d117e8b1](https://github.com/Novout/betterwrite/commit/d117e8b1)] +* **π feat!:** encrypt option for .bw - [[d4a88e04](https://github.com/Novout/betterwrite/commit/d4a88e04)] +* **π feat:** bionic reading docx and pdf - [[3c5b472e](https://github.com/Novout/betterwrite/commit/3c5b472e)] +* **π feat(epub):** ignore fixed and image case - [[a06d38b2](https://github.com/Novout/betterwrite/commit/a06d38b2)] +* **π§ chore:** update epub-gen-rs - [[21f27acd](https://github.com/Novout/betterwrite/commit/21f27acd)] +* **π§ chore(epub):** toc title - [[bb3f4aa8](https://github.com/Novout/betterwrite/commit/bb3f4aa8)] +* **π feat:** initial .epub implementation - [[fb0b5db8](https://github.com/Novout/betterwrite/commit/fb0b5db8)] +* **π§ fix!:** clean save in before set entity raw - [[6be2b614](https://github.com/Novout/betterwrite/commit/6be2b614)] +* **π© refactor:** remove head dynamic set - [[3d3ef9b2](https://github.com/Novout/betterwrite/commit/3d3ef9b2)] +* **π§ chore:** remove dropbox support - [[86628245](https://github.com/Novout/betterwrite/commit/86628245)] +* **π§ chore:** ignore deprecation typescript - [[6a512ff2](https://github.com/Novout/betterwrite/commit/6a512ff2)] +* **π§ chore:** update vite - [[739e444f](https://github.com/Novout/betterwrite/commit/739e444f)] +* **π§ chore:** added claude.md - [[e10104cd](https://github.com/Novout/betterwrite/commit/e10104cd)] +* **π feat:** top file - [[7db91b93](https://github.com/Novout/betterwrite/commit/7db91b93)] +* **π§ chore:** readme - [[c94ebf14](https://github.com/Novout/betterwrite/commit/c94ebf14)] + ### v1.3.27 * **π§ chore:** update generi - [[9929a27e](https://github.com/Novout/betterwrite/commit/9929a27e)] diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..74e9cd57a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,113 @@ +# betterwrite β CLAUDE.md + +## Project Overview + +**Better Write** is a 100% client-side creative word processor built as a Vue 3 + TypeScript monorepo. There is no backend β all data lives in the browser (IndexedDB). Exports (PDF, DOCX, HTML, TXT, EPUB) are generated entirely in the browser. The project is deployed as a PWA at betterwrite.io. + +## Monorepo Structure + +Managed with **pnpm workspaces** + **Lerna** + **Nx** (task caching). + +``` +packages/ + app/ # Main Vue 3 SPA β the entry point for everything + better-write-types/ # Shared TypeScript types β check here first before defining new types + contenteditable-ast/ # Custom AST for the entity-model text editor + client-storage/ # IndexedDB abstraction layer + extension/ # .bw file format (ZIP + data.json) + plugin-*/ # Feature plugins (characters, theme, shortcuts, voice, etc.) + plugin-exporter-*/ # Export generators (pdf, docx, html, txt, epub) + plugin-importer/ # File import handling + google-fonts-api/ # Google Fonts integration + color-converter/ # Color utility + image-converter/ # Image processing + languages/ # i18n translation files +``` + +## Key Architecture Concepts + +### Entity Model +The document is not stored as raw HTML. Content is a tree of **entities** (paragraphs, headings, images, page breaks, etc.), each with a `type`, `raw` text, and optional visual overrides. See `packages/better-write-types` and `docs/ENTITY_MODEL.md`. + +### Plugin System +Plugins receive three injection points: `emitter` (mitt event bus), `stores` (Pinia), and `hooks` (lifecycle). All plugins initialize in `packages/app/src/App.vue`. When adding a feature, check whether it belongs in an existing plugin or as a new `plugin-*` package. See `docs/PLUGIN_SYSTEM.md`. + +### State Management +Pinia stores live in `packages/app/src/store/`. There are ~14 stores. Prefer composables in `packages/app/src/use/` over direct store access in components. + +### Export Pipeline +Each export format is a separate `plugin-exporter-*` package. Generators receive the entity tree and produce browser-downloadable output. See `docs/GENERATOR_FLOW.md`. + +### .bw Extension Format +Project files saved as `.bw` are a ZIP archive containing `data.json`. Handled by `packages/extension/`. See `docs/PROJECT_FLOW.md`. + +## Development Setup + +```bash +# Install dependencies +pnpm install + +# Start dev server (port 3000) +pnpm dev + +# Build all packages +pnpm build + +# Run tests +pnpm test + +# Format code +pnpm lint +``` + +Requires a `.env.local` file in `packages/app/` β copy from `.env.example`. + +Node 16+ and pnpm 8+ required. + +## Coding Conventions + +- **TypeScript strict mode** β no `any` without justification. +- **No semicolons**, **single quotes**, **2-space indent** (Prettier enforced). +- **Conventional Commits** required: `feat:`, `fix:`, `chore:`, `refactor:`, `docs:`, `ci:`. +- **Vue 3 Composition API** β use ` diff --git a/packages/app/src/components/page/about/AboutInfo.vue b/packages/app/src/components/page/about/AboutInfo.vue index 08d7063cb..0054ce154 100644 --- a/packages/app/src/components/page/about/AboutInfo.vue +++ b/packages/app/src/components/page/about/AboutInfo.vue @@ -70,9 +70,6 @@
{{ item.name || item.id }}
-+ {{ t('editor.aside.configuration.entity.spellcheck') }} +
++ {{ t('editor.preferences.configuration.editor.layout') }} +
++ {{ t('editor.preferences.configuration.editor.editorWidth') }} +
+{{ t('editor.preferences.configuration.editor.text') }} @@ -135,6 +150,28 @@ :min="8" />
+ {{ t('editor.preferences.configuration.editor.lineHeight') }} +
++ {{ t('editor.preferences.configuration.editor.letterSpacing') }} +
+diff --git a/packages/app/src/components/page/editor/tools/EditorToolsSpeechRecognition.vue b/packages/app/src/components/page/editor/tools/EditorToolsSpeechRecognition.vue index 4f85e61bd..09089a5c4 100644 --- a/packages/app/src/components/page/editor/tools/EditorToolsSpeechRecognition.vue +++ b/packages/app/src/components/page/editor/tools/EditorToolsSpeechRecognition.vue @@ -6,24 +6,27 @@ :initial="{ opacity: 0 }" :enter="{ opacity: 1 }" :delay="0" - class="flex z-50 bg-rgba-blur bg-theme-background-3 items-center justify-center absolute z-50 right-0 transform m-5 p-2 shadow-lg w-60 absolute transform -translate-y-1/2 top-1/2" + class="flex z-50 bg-rgba-blur bg-theme-background-3 items-center justify-center absolute z-50 right-0 transform m-5 p-2 shadow-lg w-72 absolute transform -translate-y-1/2 top-1/2" >
${paragraph}
`), - ) - break - case 'heading-one': - chapter.title = entities().headingOne(entity) - break - case 'heading-two': - chapter.content += entities().headingTwo(entity) - break - case 'heading-three': - chapter.content += entities().headingThree(entity) - break - case 'page-break': - chapter.content += entities().pageBreak() - break - case 'line-break': - chapter.content += entities().lineBreak() - break - case 'image': - chapter.content += entities().image(entity) - break - case 'drau': - chapter.content += entities().svg(entity) - break + if (isValidType(entity)) { + const purged = hooks.substitution.purge(entity.raw) + data.push(options.bionicReading ? bionicHtml(purged) : purged) } } - chapters.push(chapter) + raw.push(data) }) - return chapters + return raw } - const generate = () => { - EPUB( + const generate = async (options: EPUBDocOptions) => { + await ready + + const epub = new Epub( { title: stores.PROJECT.nameRaw, author: stores.PROJECT.creator, description: stores.PROJECT.subject, publisher: stores.PROJECT.producer, - tocTitle: hooks.i18n.t('editor.bar.epub.table') ?? undefined, - tocInTOC: stores.PROJECT.type === 'creative', - date: new Date().toString(), - css: getStyles(stores, hooks), - cover: undefined, + tocTitle: stores.PROJECT.nameRaw ?? undefined, + lang: 'en', + fonts: [], + version: 3, }, - contents(), - ).then( - (content) => download(content), - (_) => hooks.toast.error(hooks.i18n.t('toast.generics.error')), + contents(options), ) + + download(epub, stores.PROJECT.nameRaw) } - const download = (blob: Blob) => { - saveAs(blob, hooks.project.utils().exportFullName('epub')) + const download = (epub: Epub, title: string) => { + const bytes = epub.archive() + const blob = new Blob([bytes as any], { type: 'application/epub+zip' }) + const url = URL.createObjectURL(blob) + const a = Object.assign(document.createElement('a'), { + href: url, + download: `${title}.epub`, + }) + a.click() hooks.toast.success(hooks.i18n.t('toast.project.epub.generate')) } On.externals().PluginEpubGenerate(emitter, [ - () => { - generate() + (options: EPUBDocOptions) => { + generate(options ?? { bionicReading: false }) }, () => {}, ]) diff --git a/packages/plugin-exporter-html/package.json b/packages/plugin-exporter-html/package.json index 0ecbc7010..44d02380d 100644 --- a/packages/plugin-exporter-html/package.json +++ b/packages/plugin-exporter-html/package.json @@ -1,6 +1,6 @@ { "name": "better-write-plugin-exporter-html", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "main": "dist/index.js", @@ -17,9 +17,9 @@ "README.md" ], "dependencies": { - "better-write-contenteditable-ast": "^1.3.27", - "better-write-plugin-core": "^1.3.27", - "better-write-types": "^1.3.27", + "better-write-contenteditable-ast": "^1.4.0", + "better-write-plugin-core": "^1.4.0", + "better-write-types": "^1.4.0", "vue-demi": "latest" }, "peerDependencies": { diff --git a/packages/plugin-exporter-pdf/package.json b/packages/plugin-exporter-pdf/package.json index 22e47e040..16c4b9a4a 100644 --- a/packages/plugin-exporter-pdf/package.json +++ b/packages/plugin-exporter-pdf/package.json @@ -1,6 +1,6 @@ { "name": "better-write-plugin-exporter-pdf", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "main": "dist/index.js", @@ -17,13 +17,13 @@ "README.md" ], "dependencies": { - "better-write-color-converter": "^1.3.27", - "better-write-contenteditable-ast": "^1.3.27", - "better-write-google-fonts-api": "^1.3.27", - "better-write-image-converter": "^1.3.27", - "better-write-plugin-core": "^1.3.27", - "better-write-plugin-theme": "^1.3.27", - "better-write-types": "^1.3.27", + "better-write-color-converter": "^1.4.0", + "better-write-contenteditable-ast": "^1.4.0", + "better-write-google-fonts-api": "^1.4.0", + "better-write-image-converter": "^1.4.0", + "better-write-plugin-core": "^1.4.0", + "better-write-plugin-theme": "^1.4.0", + "better-write-types": "^1.4.0", "vue-demi": "latest" }, "peerDependencies": { diff --git a/packages/plugin-exporter-pdf/src/generate.ts b/packages/plugin-exporter-pdf/src/generate.ts index bc0f30d1d..eda7c1456 100644 --- a/packages/plugin-exporter-pdf/src/generate.ts +++ b/packages/plugin-exporter-pdf/src/generate.ts @@ -358,12 +358,29 @@ export const PluginPDFSet = ( ast.forEach(({ text, italic, bold, underline }) => { const und = underline ? { decoration: 'underline' } : {} - arr.push({ - text, - italics: italic, - bold, - ...und, - }) + if (options.bionicReading) { + text.split(/(\s+)/).forEach((chunk) => { + if (/^\s+$/.test(chunk) || chunk === '') { + arr.push({ text: chunk, italics: italic, bold, ...und }) + } else { + const len = Math.ceil(chunk.length * 0.45) + arr.push({ + text: chunk.slice(0, len), + italics: italic, + bold: true, + ...und, + }) + arr.push({ + text: chunk.slice(len), + italics: italic, + bold, + ...und, + }) + } + }) + } else { + arr.push({ text, italics: italic, bold, ...und }) + } }) return arr @@ -590,7 +607,7 @@ export const PluginPDFSet = ( text: stores.PROJECT.creator, fontSize: 11, font: utils().correctFontInject(stores.PDF.styles.paragraph.font), - margin: [base().pageMargins[0], 250, base().pageMargins[2], 0], + margin: [base().pageMargins[0], 180, base().pageMargins[2], 0], color: isTheme.value ? theme.paragraph : defColor, alignment: 'left', pageBreak: 'after', diff --git a/packages/plugin-exporter-txt/package.json b/packages/plugin-exporter-txt/package.json index 6dfe7b8b8..9c8b992f6 100644 --- a/packages/plugin-exporter-txt/package.json +++ b/packages/plugin-exporter-txt/package.json @@ -1,6 +1,6 @@ { "name": "better-write-plugin-exporter-txt", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "main": "dist/index.js", @@ -17,9 +17,9 @@ "README.md" ], "dependencies": { - "better-write-contenteditable-ast": "^1.3.27", - "better-write-plugin-core": "^1.3.27", - "better-write-types": "^1.3.27", + "better-write-contenteditable-ast": "^1.4.0", + "better-write-plugin-core": "^1.4.0", + "better-write-types": "^1.4.0", "vue-demi": "latest" }, "peerDependencies": { diff --git a/packages/plugin-importer/package.json b/packages/plugin-importer/package.json index 8a1558749..62f4f2ef6 100644 --- a/packages/plugin-importer/package.json +++ b/packages/plugin-importer/package.json @@ -1,6 +1,6 @@ { "name": "better-write-plugin-importer", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "keywords": [ @@ -23,9 +23,9 @@ "README.md" ], "dependencies": { - "better-write-extension": "^1.3.27", - "better-write-plugin-core": "^1.3.27", - "better-write-types": "^1.3.27", + "better-write-extension": "^1.4.0", + "better-write-plugin-core": "^1.4.0", + "better-write-types": "^1.4.0", "vue-demi": "latest" }, "devDependencies": { diff --git a/packages/plugin-importer/src/bw.ts b/packages/plugin-importer/src/bw.ts index 870cfb6c7..a26f9472b 100644 --- a/packages/plugin-importer/src/bw.ts +++ b/packages/plugin-importer/src/bw.ts @@ -9,9 +9,20 @@ export const BWSet = ( ) => { On.externals().PluginImporterBW(emitter, [ async ({ data }: ImporterParams) => { - const content = await readBW(data as any) + const getPassword = async () => + window.prompt(hooks.i18n.t('toast.project.bw.passwordGet')) - hooks.project.onLoadProject(content) + try { + const content = await readBW(data as any, getPassword) + + hooks.project.onLoadProject(content) + } catch (e: any) { + if (e?.message === 'wrong-password') { + hooks.toast.error(hooks.i18n.t('toast.project.bw.passwordWrong')) + } else if (e?.message !== 'cancelled') { + hooks.toast.error(hooks.i18n.t('toast.generics.error')) + } + } }, () => {}, ]) diff --git a/packages/plugin-progress-bar/package.json b/packages/plugin-progress-bar/package.json index 12539187c..922bbaaec 100644 --- a/packages/plugin-progress-bar/package.json +++ b/packages/plugin-progress-bar/package.json @@ -1,6 +1,6 @@ { "name": "better-write-plugin-progress-bar", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "keywords": [ @@ -23,9 +23,9 @@ "README.md" ], "dependencies": { - "better-write-extension": "^1.3.27", - "better-write-plugin-core": "^1.3.27", - "better-write-types": "^1.3.27", + "better-write-extension": "^1.4.0", + "better-write-plugin-core": "^1.4.0", + "better-write-types": "^1.4.0", "vue-demi": "latest" }, "devDependencies": { diff --git a/packages/plugin-schemas/package.json b/packages/plugin-schemas/package.json index e216f0af1..a27411fbf 100644 --- a/packages/plugin-schemas/package.json +++ b/packages/plugin-schemas/package.json @@ -1,6 +1,6 @@ { "name": "better-write-plugin-schemas", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "main": "dist/index.js", @@ -17,8 +17,8 @@ "README.md" ], "dependencies": { - "better-write-plugin-core": "^1.3.27", - "better-write-types": "^1.3.27", + "better-write-plugin-core": "^1.4.0", + "better-write-types": "^1.4.0", "vue-demi": "latest" }, "peerDependencies": { diff --git a/packages/plugin-shortcuts/package.json b/packages/plugin-shortcuts/package.json index f4a609ebe..22cea2057 100644 --- a/packages/plugin-shortcuts/package.json +++ b/packages/plugin-shortcuts/package.json @@ -1,6 +1,6 @@ { "name": "better-write-plugin-shortcuts", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "keywords": [ @@ -23,9 +23,9 @@ "README.md" ], "dependencies": { - "better-write-extension": "^1.3.27", - "better-write-plugin-core": "^1.3.27", - "better-write-types": "^1.3.27", + "better-write-extension": "^1.4.0", + "better-write-plugin-core": "^1.4.0", + "better-write-types": "^1.4.0", "vue-demi": "latest" }, "devDependencies": { diff --git a/packages/plugin-theme/package.json b/packages/plugin-theme/package.json index c7b26353b..9cd9929b6 100644 --- a/packages/plugin-theme/package.json +++ b/packages/plugin-theme/package.json @@ -1,6 +1,6 @@ { "name": "better-write-plugin-theme", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "keywords": [ @@ -23,8 +23,8 @@ "README.md" ], "dependencies": { - "better-write-plugin-core": "^1.3.27", - "better-write-types": "^1.3.27", + "better-write-plugin-core": "^1.4.0", + "better-write-types": "^1.4.0", "vue-demi": "latest" }, "peerDependencies": { diff --git a/packages/plugin-voice-typing/package.json b/packages/plugin-voice-typing/package.json index 7001284c7..263c5ff64 100644 --- a/packages/plugin-voice-typing/package.json +++ b/packages/plugin-voice-typing/package.json @@ -1,6 +1,6 @@ { "name": "better-write-plugin-voice-typing", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "keywords": [ @@ -23,8 +23,8 @@ "README.md" ], "dependencies": { - "better-write-plugin-core": "^1.3.27", - "better-write-types": "^1.3.27", + "better-write-plugin-core": "^1.4.0", + "better-write-types": "^1.4.0", "vue-demi": "latest" }, "devDependencies": { diff --git a/packages/plugin-voice-typing/src/collector.ts b/packages/plugin-voice-typing/src/collector.ts index 9dd10e357..f70890c37 100644 --- a/packages/plugin-voice-typing/src/collector.ts +++ b/packages/plugin-voice-typing/src/collector.ts @@ -1,32 +1,46 @@ import { On } from 'better-write-plugin-core' import { PluginTypes } from 'better-write-types' -import { computed, ref, watch } from 'vue-demi' +import { computed, watch } from 'vue-demi' + +type SR = { + lang: string + continuous: boolean + interimResults: boolean + onresult: ((event: SREvent) => void) | null + onend: (() => void) | null + onerror: (() => void) | null + start(): void + stop(): void +} + +type SRResult = { isFinal: boolean; [index: number]: { transcript: string } } +type SREvent = { resultIndex: number; results: SRResult[] & { length: number } } + +// Minimum gap (ms) between final results to insert a period vs comma +const GAP_PERIOD = 2000 +const GAP_COMMA = 700 export const CollectorSet = ( emitter: PluginTypes.PluginEmitter, stores: PluginTypes.PluginStores, hooks: PluginTypes.PluginHooks, ) => { - const instance = ref