[
  {
    "path": ".dockerignore",
    "content": ".nx\n.dist\n.node_modules"
  },
  {
    "path": ".editorconfig",
    "content": "# Editor configuration, see http://editorconfig.org\nroot = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\n[*.md]\nmax_line_length = off\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": ".eslintignore",
    "content": "node_modules\n"
  },
  {
    "path": ".eslintrc.json",
    "content": "{\n  \"root\": true,\n  \"ignorePatterns\": [\"**/*\"],\n  \"plugins\": [\"@nx\"],\n  \"overrides\": [\n    {\n      \"files\": [\"*.ts\", \"*.tsx\", \"*.js\", \"*.jsx\"],\n      \"rules\": {\n        \"@nx/enforce-module-boundaries\": [\n          \"error\",\n          {\n            \"enforceBuildableLibDependency\": true,\n            \"allow\": [],\n            \"depConstraints\": [\n              {\n                \"sourceTag\": \"*\",\n                \"onlyDependOnLibsWithTags\": [\"*\"]\n              }\n            ]\n          }\n        ]\n      }\n    },\n    {\n      \"files\": [\"*.ts\", \"*.tsx\"],\n      \"extends\": [\"plugin:@nx/typescript\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.js\", \"*.jsx\"],\n      \"extends\": [\"plugin:@nx/javascript\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.spec.ts\", \"*.spec.tsx\", \"*.spec.js\", \"*.spec.jsx\"],\n      \"env\": {\n        \"jest\": true\n      },\n      \"rules\": {}\n    }\n  ]\n}\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n\npermissions:\n  actions: read\n  contents: read\n\njobs:\n  main:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      # Connect your workspace on nx.app and uncomment this to enable task distribution.\n      # The \"--stop-agents-after\" is optional, but allows idle agents to shut down once the \"e2e-ci\" targets have been requested\n      # - run: npx nx-cloud start-ci-run --distribute-on=\"5 linux-medium-js\" --stop-agents-after=\"e2e-ci\"\n\n      # Cache node_modules\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 20\n          cache: 'npm'\n\n      - run: npm ci\n      - uses: nrwl/nx-set-shas@v4\n\n      # 安装 Playwright 浏览器和依赖\n      - name: Install Playwright\n        run: npx playwright install --with-deps\n\n      # Prepend any command with \"nx-cloud record --\" to record its logs to Nx Cloud\n      # - run: npx nx-cloud record -- echo Hello World\n      - run: npx nx affected -t lint test build --base=origin/develop --verbose\n      - run: npx nx affected --parallel 1 -t e2e-ci --base=origin/develop --verbose\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: publish\n\non:\n  push:\n    tags:\n      - \"*\"\n\njobs:\n  publish:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Login to Docker Registry\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.repository_owner }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Set TAG\n        run: echo TAG=${GITHUB_REF#refs/tags/} >> $GITHUB_ENV\n\n      - name: Publish ${{ matrix.svc }}\n        uses: docker/build-push-action@v3\n        with:\n          file: Dockerfile\n          outputs: \"type=registry,push=true\"\n          platforms: linux/amd64, linux/arm64\n          tags: |\n            ghcr.io/${{ github.repository_owner }}/drawnix:latest\n            ghcr.io/${{ github.repository_owner }}/drawnix:${{ env.TAG }}\n            pubuzhixing/drawnix:${{ env.TAG }}\n            pubuzhixing/drawnix:latest"
  },
  {
    "path": ".gitignore",
    "content": "# See http://help.github.com/ignore-files/ for more about ignoring files.\n\n# compiled output\ndist\ntmp\n/out-tsc\n\n# dependencies\nnode_modules\n\n# IDEs and editors\n/.idea\n.project\n.classpath\n.c9/\n*.launch\n.settings/\n*.sublime-workspace\n\n# IDE - VSCode\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n\n# misc\n/.sass-cache\n/connect.lock\n/coverage\n/libpeerconnection.log\nnpm-debug.log\nyarn-error.log\ntestem.log\n/typings\n\n# System Files\n.DS_Store\nThumbs.db\n\n.nx/cache\n.nx/workspace-data\n"
  },
  {
    "path": ".prettierignore",
    "content": "# Add files here to ignore them from prettier formatting\n/dist\n/coverage\n/.nx/cache\n/.nx/workspace-data"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"singleQuote\": true\n}\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\n    \"nrwl.angular-console\",\n    \"esbenp.prettier-vscode\",\n    \"ms-playwright.playwright\",\n    \"firsttris.vscode-jest-runner\"\n  ]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"cSpell.words\": [\n        \"drawnix\"\n    ]\n}"
  },
  {
    "path": "CFPAGE-DEPLOY.md",
    "content": "## \n\n### 1. 打开 Cloudflare Pages\n访问：https://dash.cloudflare.com/pages\n\n### 2. 创建项目\n- 点击 **\"Create a project\"**\n- 选择 **\"Connect to Git\"**\n- 选择您的 GitHub 仓库\n\n### 3. 配置构建设置\n在可视化界面中填写：\n\n```\nFramework preset: None\nBuild command: npm run build:web\nBuild output directory: dist/apps/web\nRoot directory: (留空)\n```\n\n在 **Environment variables** 部分添加：\n```\nNODE_VERSION = 20\n```\n\n### 4. 点击 \"Save and Deploy\"\n就这么简单！\n\n## 项目已包含的配置文件\n\n- `apps/web/public/_redirects` - SPA 路由支持\n- `apps/web/public/_headers` - 基本缓存配置\n- `package.json` 中的 `build:web` 脚本\n\n##  部署后检查\n\n1. 访问分配的 `.pages.dev` 域名\n2. 确认网站正常运行\n3. 测试页面刷新是否正常（SPA 路由）\n\n## 自定义域名（可选）\n\n部署成功后，在 Cloudflare Pages 项目中：\n1. 点击 **\"Custom domains\"**\n2. 添加您的域名\n3. 按提示配置 DNS\n\n就是这么简单！无需复杂的配置文件。\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## 0.4.0-2 (2025-12-05)\n\nThis was a version bump only, there were no code changes.\n\n## 0.4.0-1 (2025-12-05)\n\n\n### 🚀 Features\n\n- improve i18n ([4e44c7b](https://github.com/plait-board/drawnix/commit/4e44c7b))\n- **export:** support export as svg #278 This pr will support export-as-svg feature, it depend on the `toSvgData` method from `plait/core`. ([#278](https://github.com/plait-board/drawnix/issues/278))\n- **i18n:** add Vietnamese translations for UI elements ([892b627](https://github.com/plait-board/drawnix/commit/892b627))\n\n### 🩹 Fixes\n\n- add \"babel-plugin-macros\": \"^3.1.0\" to resolve `npm ci` error in ([2940a09](https://github.com/plait-board/drawnix/commit/2940a09))\n- add tooltip, fix get wrong percentage, change cursor when not allowed ([#318](https://github.com/plait-board/drawnix/pull/318))\n\n### ❤️  Thank You\n\n- Phạm Viết Nghĩa @vigstudio\n- pubuzhixing8 @pubuzhixing8\n- Six\n\n## 0.4.0-0 (2025-11-10)\n\n\n### 🩹 Fixes\n\n- **mind:** fix image scaling issue ([44dd360](https://github.com/plait-board/drawnix/commit/44dd360))\n\n### ❤️  Thank You\n\n- seepine @seepine\n\n## 0.3.3 (2025-10-26)\n\n\n### 🚀 Features\n\n- handle text editing on touch device ([e4a42d0](https://github.com/plait-board/drawnix/commit/e4a42d0))\n\n### 🩹 Fixes\n\n- make menu click trigger show/hide submenu for mobile users to be able to choose the submenu ([93c9254](https://github.com/plait-board/drawnix/commit/93c9254))\n- improve withPinchZoom Maintain pointerRecords accurately, as previous schemes would cause pointerRecords to be confused(using touch events) ([f39b1c8](https://github.com/plait-board/drawnix/commit/f39b1c8))\n- fix moving and selection error since  leave out  pointerMove, some plugins don't work ([193e193](https://github.com/plait-board/drawnix/commit/193e193))\n- improve arrow ([1d111fb](https://github.com/plait-board/drawnix/commit/1d111fb))\n- **freehand:** disable freehand and erase functionalities when user is using two fingers This pr will fix the issue mentioned in #331. 1. Maintain the status of two fingers pressing 2. Prevent freehand drawing and erasing when `isTwoFingerMode` is `true`. ([#331](https://github.com/plait-board/drawnix/issues/331))\n- **toolbar:** make arrow and shape picker button automatically select the first arrow/shape if it's the first time ([451b36e](https://github.com/plait-board/drawnix/commit/451b36e))\n\n### ❤️  Thank You\n\n- Andy Lu (Lu, Yu-An)\n- pubuzhixing8 @pubuzhixing8\n\n## 0.3.2 (2025-10-19)\n\nThis was a version bump only, there were no code changes.\n\n## 0.3.1 (2025-10-16)\n\n\n### 🩹 Fixes\n\n- correct @plait-board/markdown-to-drawnix version ([9ff924e](https://github.com/plait-board/drawnix/commit/9ff924e))\n- **hotkey:** move preventDefault() into specific conditional branching ([#303](https://github.com/plait-board/drawnix/pull/303))\n\n### ❤️  Thank You\n\n- pubuzhixing8 @pubuzhixing8\n\n## 0.3.0 (2025-09-13)\n\n\n### 🚀 Features\n\n- **arrow:** support set arrow mark ([#258](https://github.com/plait-board/drawnix/pull/258))\n- **eraser:** implement eraser feature ([#221](https://github.com/plait-board/drawnix/pull/221))\n- **eraser:** adding i18n for eraser ([427a730](https://github.com/plait-board/drawnix/commit/427a730))\n- **eraser:** Improving all the eraser feature mentioned in #247 ([#249](https://github.com/plait-board/drawnix/pull/249), [#247](https://github.com/plait-board/drawnix/issues/247))\n- **eraser:** drawing erasing trail animation effect ([#295](https://github.com/plait-board/drawnix/pull/295))\n- **i18n:** added i18n tool for multi-Language support ([#232](https://github.com/plait-board/drawnix/pull/232))\n- **i18n:** adding i18n for clean confirm ([7bdf543](https://github.com/plait-board/drawnix/commit/7bdf543))\n- **i18n:** refactor the structure of i18n, adding with-common getI18n for plait objects, complete the translation of zh,en,ru ([#276](https://github.com/plait-board/drawnix/pull/276))\n- **i18n:** add Arabic language ([#280](https://github.com/plait-board/drawnix/pull/280))\n- **popup-toolbar:** add stroke select state, add stroke type text ([#272](https://github.com/plait-board/drawnix/pull/272))\n\n### 🩹 Fixes\n\n- fix dockerfile build logic ([#201](https://github.com/plait-board/drawnix/pull/201))\n- **cursor:** set mind element css to always be inherit ([#260](https://github.com/plait-board/drawnix/pull/260))\n- **freehand&i18n:** fix i18n of freehand toolbar and make secondary toolbar always exist while using freehand element ([#255](https://github.com/plait-board/drawnix/pull/255))\n- **frontend:** comment addDebugLog to prevent potential XSS security issue ([#269](https://github.com/plait-board/drawnix/pull/269))\n- **hotkey:** prevent switch arrow creation mode when mod+a #195 ([#200](https://github.com/plait-board/drawnix/pull/200), [#195](https://github.com/plait-board/drawnix/issues/195))\n- **hotkey:** prevent enter arrow creation mode when press a and there are some selected elements ([#205](https://github.com/plait-board/drawnix/pull/205))\n- **hotkey:** Prevent Arc browser undo on Cmd+Z in Drawnix ([#254](https://github.com/plait-board/drawnix/pull/254))\n- **hotkey:** skip creation hotkey when use press special key and the among of alt, meta and ctrl ([#262](https://github.com/plait-board/drawnix/pull/262))\n- **menu:** Adding margin for the menu components ([c9ecd09](https://github.com/plait-board/drawnix/commit/c9ecd09))\n- **menu:** fix hotkey instruction for every OS ([#274](https://github.com/plait-board/drawnix/pull/274))\n- **mind:** bump plait into 0.84.0 to fix text can not show completely mentioned in #208 ([#261](https://github.com/plait-board/drawnix/pull/261), [#208](https://github.com/plait-board/drawnix/issues/208))\n- **toolbar:** fix issue mentioned in #290 ([#291](https://github.com/plait-board/drawnix/pull/291), [#290](https://github.com/plait-board/drawnix/issues/290))\n- **tutorial:** fix tutorial instruction issues and update styles ([#289](https://github.com/plait-board/drawnix/pull/289))\n\n### ❤️  Thank You\n\n- Andy Lu (Lu, Yu-An) @NaoCoding\n- coderwei @coderwei99\n- MalikAli @MalikAliQassem\n- Peter Chen\n- pubuzhixing8 @pubuzhixing8\n- Six\n- vishwak @PATTASWAMY-VISHWAK-YASASHREE\n\n## 0.2.1 (2025-08-06)\n\n\n### 🩹 Fixes\n\n- **hotkey:** assign t as hotkey to create text element ([#192](https://github.com/plait-board/drawnix/pull/192))\n\n### ❤️  Thank You\n\n- pubuzhixing8 @pubuzhixing8\n\n## 0.2.0 (2025-08-06)\n\nThis was a version bump only, there were no code changes.\n\n## 0.1.4 (2025-08-06)\n\nThis was a version bump only, there were no code changes.\n\n## 0.1.3 (2025-08-06)\n\n\n### 🩹 Fixes\n\n- **hotkey:** prevent hotkey when  type normally ([#189](https://github.com/plait-board/drawnix/pull/189))\n\n### ❤️  Thank You\n\n- pubuzhixing8 @pubuzhixing8\n\n## 0.1.2 (2025-08-06)\n\n\n### 🚀 Features\n\n- **creation:** support creation mode hotkey #183 ([#185](https://github.com/plait-board/drawnix/pull/185), [#183](https://github.com/plait-board/drawnix/issues/183))\n- **mind:** bump plait into 0.82.0 to improve the experience of mind ([f904594](https://github.com/plait-board/drawnix/commit/f904594))\n- **viewer:** support image which in mind node view #125 ([#125](https://github.com/plait-board/drawnix/issues/125))\n\n### ❤️  Thank You\n\n- pubuzhixing8 @pubuzhixing8\n\n## 0.1.1 (2025-07-10)\n\n\n### 🩹 Fixes\n\n- **text:** resolve text can not auto break line #173 #169 ([#173](https://github.com/plait-board/drawnix/issues/173), [#169](https://github.com/plait-board/drawnix/issues/169))\n\n### ❤️  Thank You\n\n- pubuzhixing8 @pubuzhixing8\n\n## 0.1.0 (2025-07-02)\n\n\n### 🚀 Features\n\n- import styles ([ecfe3cd](https://github.com/plait-board/drawnix/commit/ecfe3cd))\n- add script and update ci ([147c028](https://github.com/plait-board/drawnix/commit/147c028))\n- bump plait into 0.62.0-next.7 ([7ab4003](https://github.com/plait-board/drawnix/commit/7ab4003))\n- add main menu ([#14](https://github.com/plait-board/drawnix/pull/14))\n- improve active-toolbar ([fd19725](https://github.com/plait-board/drawnix/commit/fd19725))\n- rename active-toolbar to popup-toolbar and modify tool-button ([aa06c7e](https://github.com/plait-board/drawnix/commit/aa06c7e))\n- support opacity for  color property ([#16](https://github.com/plait-board/drawnix/pull/16))\n- support local storage ([9c0e652](https://github.com/plait-board/drawnix/commit/9c0e652))\n- add product_showcase bump plait into 0.69.0 ([61fe571](https://github.com/plait-board/drawnix/commit/61fe571))\n- add sitemap ([3b9d9a3](https://github.com/plait-board/drawnix/commit/3b9d9a3))\n- improve pinch zoom ([#77](https://github.com/plait-board/drawnix/pull/77))\n- bump plait into 0.76.0 and handle break changes ([#90](https://github.com/plait-board/drawnix/pull/90))\n- improve README ([9e0190d](https://github.com/plait-board/drawnix/commit/9e0190d))\n- add dependencies for packages ([6d89b32](https://github.com/plait-board/drawnix/commit/6d89b32))\n- init dialog and mermaid-to-dialog ([6ff70b9](https://github.com/plait-board/drawnix/commit/6ff70b9))\n- support save as json from hotkey ([120dffa](https://github.com/plait-board/drawnix/commit/120dffa))\n- support sub menu and export jpg ([#132](https://github.com/plait-board/drawnix/pull/132))\n- improve link popup state ([#147](https://github.com/plait-board/drawnix/pull/147))\n- improve seo ([#148](https://github.com/plait-board/drawnix/pull/148))\n- **active-toolbar:** add active toolbar ([7e737a2](https://github.com/plait-board/drawnix/commit/7e737a2))\n- **active-toolbar:** support font color property ([4b2d964](https://github.com/plait-board/drawnix/commit/4b2d964))\n- **app:** use localforage to storage main board content #122 ([#122](https://github.com/plait-board/drawnix/issues/122))\n- **app-toolbar:** support undo/redo operation ([50f8831](https://github.com/plait-board/drawnix/commit/50f8831))\n- **app-toolbar:** add trash and duplicate in app-toolbar ([#28](https://github.com/plait-board/drawnix/pull/28))\n- **clean-board:** complete clean board ([#124](https://github.com/plait-board/drawnix/pull/124))\n- **clean-confirm:** autoFocus ok button ([582172a](https://github.com/plait-board/drawnix/commit/582172a))\n- **color-picker:** support merge operations for update opacity #4 ([#45](https://github.com/plait-board/drawnix/pull/45), [#4](https://github.com/plait-board/drawnix/issues/4))\n- **component:** improve the onXXXChange feature for drawnix component #79 ([#79](https://github.com/plait-board/drawnix/issues/79))\n- **component:** add afterInit to expose board instance ([23d91dc](https://github.com/plait-board/drawnix/commit/23d91dc))\n- **component:** support update value from drawnix component outside ([#103](https://github.com/plait-board/drawnix/pull/103))\n- **component:** fit viewport after children updated ([#104](https://github.com/plait-board/drawnix/pull/104))\n- **creation-toolbar:** support long-press triggers drag selection an… ([#78](https://github.com/plait-board/drawnix/pull/78))\n- **creation-toolbar:** remove default action when click shape and arrow icon in creation toolbar improve tool-button ([a46c2df](https://github.com/plait-board/drawnix/commit/a46c2df))\n- **draw:** bump plait into 0.75.0-next.0 and support fine-grained selection ([#69](https://github.com/plait-board/drawnix/pull/69))\n- **draw-toolbar:** add draw toolbar ([#9](https://github.com/plait-board/drawnix/pull/9))\n- **draw-toolbar:** add shape and arrow panel for draw-toolbar #10 ([#12](https://github.com/plait-board/drawnix/pull/12), [#10](https://github.com/plait-board/drawnix/issues/10))\n- **drawnix:** init drawnix package ([397d865](https://github.com/plait-board/drawnix/commit/397d865))\n- **drawnix:** export utils ([#105](https://github.com/plait-board/drawnix/pull/105))\n- **drawnix-board:** initialize drawnix board ([117e5a8](https://github.com/plait-board/drawnix/commit/117e5a8))\n- **fill:** split fill color and fill opacity setting ([#53](https://github.com/plait-board/drawnix/pull/53))\n- **flowchart:** add terminal shape element ([#80](https://github.com/plait-board/drawnix/pull/80))\n- **freehand:** initialize freehand #2 ([#2](https://github.com/plait-board/drawnix/issues/2))\n- **freehand:** apply gaussianSmooth to freehand curve ([#47](https://github.com/plait-board/drawnix/pull/47))\n- **freehand:** update stroke width to 2 and optimize freehand end points ([#50](https://github.com/plait-board/drawnix/pull/50))\n- **freehand:** improve freehand experience ([#51](https://github.com/plait-board/drawnix/pull/51))\n- **freehand:** add FreehandSmoother to optimize freehand curve ([#62](https://github.com/plait-board/drawnix/pull/62))\n- **freehand:** optimize freehand curve by stylus features ([#63](https://github.com/plait-board/drawnix/pull/63))\n- **freehand:** freehand support theme ([b7c7965](https://github.com/plait-board/drawnix/commit/b7c7965))\n- **freehand:** support closed freehand and add popup for freehand ([#68](https://github.com/plait-board/drawnix/pull/68))\n- **freehand:** bump plait into 0.75.0-next.9 and resolve freehand unexpected resize-handle after moving freehand elements ([#84](https://github.com/plait-board/drawnix/pull/84))\n- **hotkey:** support export png hotkey ([#30](https://github.com/plait-board/drawnix/pull/30))\n- **image:** support free image element and support insert image at m… ([#95](https://github.com/plait-board/drawnix/pull/95))\n- **image:** should hide popup toolbar when selected element include image ([#96](https://github.com/plait-board/drawnix/pull/96))\n- **image:** support drag image to board to add image as draw element or mind node image ([#144](https://github.com/plait-board/drawnix/pull/144))\n- **link:** improve link popup ([eba06e2](https://github.com/plait-board/drawnix/commit/eba06e2))\n- **markdown-to-drawnix:** support markdown to drawnix mind map #134 ([#135](https://github.com/plait-board/drawnix/pull/135), [#134](https://github.com/plait-board/drawnix/issues/134))\n- **menu:** support export to json file ([d0d6ca5](https://github.com/plait-board/drawnix/commit/d0d6ca5))\n- **menu:** support load file action ([758aa6d](https://github.com/plait-board/drawnix/commit/758aa6d))\n- **mermaid:** improve mermaid-to-drawnix ([a928ba1](https://github.com/plait-board/drawnix/commit/a928ba1))\n- **mobile:** adapt mobile device ([7c0742f](https://github.com/plait-board/drawnix/commit/7c0742f))\n- **pencil-mode:** add pencil mode and add drawnix context ([#76](https://github.com/plait-board/drawnix/pull/76))\n- **pinch-zoom:** support pinch zoom for touch device ([#60](https://github.com/plait-board/drawnix/pull/60))\n- **pinch-zoom:** improve pinch zoom functionality and support hand moving ([#75](https://github.com/plait-board/drawnix/pull/75))\n- **popover:** add reusable popover and replace radix popover ([d30388a](https://github.com/plait-board/drawnix/commit/d30388a))\n- **popup:** display icon when color is complete opacity ([#42](https://github.com/plait-board/drawnix/pull/42))\n- **popup-toolbar:** support set branch color remove color property when select transparent #17 ([#17](https://github.com/plait-board/drawnix/issues/17))\n- **popup-toolbar:** bump plait into 0.71.0 and mind node link stroke and node stroke support dashed/dotted style #22 ([#22](https://github.com/plait-board/drawnix/issues/22))\n- **property:** support stroke style setting ([463c92a](https://github.com/plait-board/drawnix/commit/463c92a))\n- **size-slider:** improve size-slider component ([780be9d](https://github.com/plait-board/drawnix/commit/780be9d))\n- **text:** support soft break ([#39](https://github.com/plait-board/drawnix/pull/39))\n- **text:** support update text from outside ([#58](https://github.com/plait-board/drawnix/pull/58))\n- **text:** support insertSoftBreak for text #136 ([#136](https://github.com/plait-board/drawnix/issues/136))\n- **theme-toolbar:** add theme selection toolbar for customizable themes ([dca0e33](https://github.com/plait-board/drawnix/commit/dca0e33))\n- **toolbar:** support zoom toolbar ([76ef5d9](https://github.com/plait-board/drawnix/commit/76ef5d9))\n- **web:** seo ([84cde4b](https://github.com/plait-board/drawnix/commit/84cde4b))\n- **web:** add cloud.umami.is to track views ([#64](https://github.com/plait-board/drawnix/pull/64))\n- **web:** modify initialize-data for adding freehand data ([#65](https://github.com/plait-board/drawnix/pull/65))\n- **web:** add debug console ([#83](https://github.com/plait-board/drawnix/pull/83))\n- **wrapper:** add wrapper component and context hook ([#6](https://github.com/plait-board/drawnix/pull/6))\n- **zoom-toolbar:** support zoom menu ([cc6a6b8](https://github.com/plait-board/drawnix/commit/cc6a6b8))\n\n### 🩹 Fixes\n\n- remove theme-toolbar font-weight style ([#67](https://github.com/plait-board/drawnix/pull/67))\n- revert package lock ([1aa9d42](https://github.com/plait-board/drawnix/commit/1aa9d42))\n- fix pub issue ([156abcb](https://github.com/plait-board/drawnix/commit/156abcb))\n- improve libs build ([9ddb6d9](https://github.com/plait-board/drawnix/commit/9ddb6d9))\n- **app-toolbar:** correct app-toolbar style ([#106](https://github.com/plait-board/drawnix/pull/106))\n- **arrow-line:** optimize the popup toolbar position when selected element is arrow line ([#70](https://github.com/plait-board/drawnix/pull/70))\n- **board:** resolve mobile scrolling issue when resize or moving ([8fdca8e](https://github.com/plait-board/drawnix/commit/8fdca8e))\n- **board:** bump plait into 0.69.1 deselect when text editing end refactor popup toolbar placement ([aef6d23](https://github.com/plait-board/drawnix/commit/aef6d23))\n- **board:** use updateViewBox to fix board wobbles when dragging or resizing ([#94](https://github.com/plait-board/drawnix/pull/94))\n- **color-picker:** support display 0 opacity ([#48](https://github.com/plait-board/drawnix/pull/48))\n- **core:** bump plait into 0.79.1 to fix with-hand issue when press space key #141 ([#149](https://github.com/plait-board/drawnix/pull/149), [#141](https://github.com/plait-board/drawnix/issues/141))\n- **creation-toolbar:** use pointerUp set basic pointer cause onChange do not fire on mobile bind pointermove/pointerup to viewportContainerRef to implement dnd on mobile #20 ([#20](https://github.com/plait-board/drawnix/issues/20))\n- **creation-toolbar:** move out toolbar from board to avoid fired pointer down event when operating ([ddb6092](https://github.com/plait-board/drawnix/commit/ddb6092))\n- **font-color:** fix color can not be assigned when current color is empty ([#55](https://github.com/plait-board/drawnix/pull/55))\n- **freehand:** fix freehand creation issue(caused by throttleRAF) ([#40](https://github.com/plait-board/drawnix/pull/40))\n- **mermaid:** bump mermaid-to-drawnix to 0.0.2 to fix text display issue ([33878d0](https://github.com/plait-board/drawnix/commit/33878d0))\n- **mermaid-to-drawnix:** support group for insertToBoard ([e2f5056](https://github.com/plait-board/drawnix/commit/e2f5056))\n- **mind:** remove branchColor property setting ([#46](https://github.com/plait-board/drawnix/pull/46))\n- **property:** prevent set fill color opacity when color is none ([#56](https://github.com/plait-board/drawnix/pull/56))\n- **react-board:** resolve text should not display in safari ([19fc20f](https://github.com/plait-board/drawnix/commit/19fc20f))\n- **react-board:** support fit viewport after browser window resized ([96f4a0e](https://github.com/plait-board/drawnix/commit/96f4a0e))\n- **size-slider:** correct size slider click handle can not fire ([#57](https://github.com/plait-board/drawnix/pull/57))\n- **text:** fix composition input and abc input trembly issue ([#15](https://github.com/plait-board/drawnix/pull/15))\n- **text:** resolve with-text build error ([#41](https://github.com/plait-board/drawnix/pull/41))\n- **text:** fix text can not editing ([#52](https://github.com/plait-board/drawnix/pull/52))\n- **text:** fix text can not display correctly on windows 10 chrome env #99 ([#100](https://github.com/plait-board/drawnix/pull/100), [#99](https://github.com/plait-board/drawnix/issues/99))\n- **text:** allow scroll to show all text ([#156](https://github.com/plait-board/drawnix/pull/156))\n- **text:** set whiteSpace: pre to avoid \\n is ineffectual ([#165](https://github.com/plait-board/drawnix/pull/165))\n- **use-board-event:** fix board event timing ([0d4a8f1](https://github.com/plait-board/drawnix/commit/0d4a8f1))\n\n### ❤️  Thank You\n\n- pubuzhixing8 @pubuzhixing8\n\n## 0.0.4 (2025-04-15)\n\n\n### 🚀 Features\n\n- ci: a tiny docker image (#127) ([#122](https://github.com/plait-board/drawnix/pull/127))\n- support save as json from hotkey ([120dffa](https://github.com/plait-board/drawnix/commit/120dffa))\n- **app:** use localforage to storage main board content #122 ([#122](https://github.com/plait-board/drawnix/issues/122))\n- **clean-board:** complete clean board ([#124](https://github.com/plait-board/drawnix/pull/124))\n\n### 🩹 Fixes\n\n- **react-board:** support fit viewport after browser window resized ([96f4a0e](https://github.com/plait-board/drawnix/commit/96f4a0e))\n\n### ❤️  Thank You\n\n- lurenyang418 @lurenyang418\n- whyour @whyour\n- pubuzhixing8 @pubuzhixing8\n\n## 0.0.4-3 (2025-03-25)\n\n\n### 🩹 Fixes\n\n- improve libs build ([9ddb6d9](https://github.com/plait-board/drawnix/commit/9ddb6d9))\n- **mermaid:** bump mermaid-to-drawnix to 0.0.2 to fix text display issue ([33878d0](https://github.com/plait-board/drawnix/commit/33878d0))\n\n### ❤️  Thank You\n\n- pubuzhixing8\n\n## 0.0.4-2 (2025-03-19)\n\n\n### 🚀 Features\n\n- init dialog and mermaid-to-dialog ([6ff70b9](https://github.com/plait-board/drawnix/commit/6ff70b9))\n- **mermaid:** improve mermaid-to-drawnix ([a928ba1](https://github.com/plait-board/drawnix/commit/a928ba1))\n\n### ❤️  Thank You\n\n- pubuzhixing8 @pubuzhixing8\n\n## 0.0.4-1 (2025-03-16)\n\nThis was a version bump only, there were no code changes.\n\n## 0.0.4-0 (2025-03-16)\n\n\n### 🚀 Features\n\n- add dependencies for packages ([6d89b32](https://github.com/plait-board/drawnix/commit/6d89b32))\n- **component:** support update value from drawnix component outside ([#103](https://github.com/plait-board/drawnix/pull/103))\n- **component:** fit viewport after children updated ([#104](https://github.com/plait-board/drawnix/pull/104))\n- **drawnix:** export utils ([#105](https://github.com/plait-board/drawnix/pull/105))\n\n### 🩹 Fixes\n\n- **app-toolbar:** correct app-toolbar style ([#106](https://github.com/plait-board/drawnix/pull/106))\n\n### ❤️  Thank You\n\n- pubuzhixing8 @pubuzhixing8\n\n## 0.0.3 (2025-03-14)\n\n\n### 🩹 Fixes\n\n- revert package lock ([1aa9d42](https://github.com/plait-board/drawnix/commit/1aa9d42))\n- fix pub issue ([156abcb](https://github.com/plait-board/drawnix/commit/156abcb))\n- **text:** fix text can not display correctly on windows 10 chrome env #99 ([#100](https://github.com/plait-board/drawnix/pull/100), [#99](https://github.com/plait-board/drawnix/issues/99))\n\n### ❤️  Thank You\n\n- pubuzhixing8 @pubuzhixing8\n\n## 0.0.2 (2025-03-10)\n\n\n### 🚀 Features\n\n- improve README ([9e0190d](https://github.com/plait-board/drawnix/commit/9e0190d))\n\n### ❤️  Thank You\n\n- pubuzhixing8 @pubuzhixing8\n\n## 0.0.1 (2025-03-10)\n\n\n### 🚀 Features\n\n- import styles ([ecfe3cd](https://github.com/plait-board/drawnix/commit/ecfe3cd))\n- add script and update ci ([147c028](https://github.com/plait-board/drawnix/commit/147c028))\n- bump plait into 0.62.0-next.7 ([7ab4003](https://github.com/plait-board/drawnix/commit/7ab4003))\n- add main menu ([#14](https://github.com/plait-board/drawnix/pull/14))\n- improve active-toolbar ([fd19725](https://github.com/plait-board/drawnix/commit/fd19725))\n- rename active-toolbar to popup-toolbar and modify tool-button ([aa06c7e](https://github.com/plait-board/drawnix/commit/aa06c7e))\n- support opacity for  color property ([#16](https://github.com/plait-board/drawnix/pull/16))\n- support local storage ([9c0e652](https://github.com/plait-board/drawnix/commit/9c0e652))\n- add product_showcase bump plait into 0.69.0 ([61fe571](https://github.com/plait-board/drawnix/commit/61fe571))\n- add sitemap ([3b9d9a3](https://github.com/plait-board/drawnix/commit/3b9d9a3))\n- improve pinch zoom ([#77](https://github.com/plait-board/drawnix/pull/77))\n- bump plait into 0.76.0 and handle break changes ([#90](https://github.com/plait-board/drawnix/pull/90))\n- **active-toolbar:** add active toolbar ([7e737a2](https://github.com/plait-board/drawnix/commit/7e737a2))\n- **active-toolbar:** support font color property ([4b2d964](https://github.com/plait-board/drawnix/commit/4b2d964))\n- **app-toolbar:** support undo/redo operation ([50f8831](https://github.com/plait-board/drawnix/commit/50f8831))\n- **app-toolbar:** add trash and duplicate in app-toolbar ([#28](https://github.com/plait-board/drawnix/pull/28))\n- **color-picker:** support merge operations for update opacity #4 ([#45](https://github.com/plait-board/drawnix/pull/45), [#4](https://github.com/plait-board/drawnix/issues/4))\n- **component:** improve the onXXXChange feature for drawnix component #79 ([#79](https://github.com/plait-board/drawnix/issues/79))\n- **component:** add afterInit to expose board instance ([23d91dc](https://github.com/plait-board/drawnix/commit/23d91dc))\n- **creation-toolbar:** support long-press triggers drag selection an… ([#78](https://github.com/plait-board/drawnix/pull/78))\n- **draw:** bump plait into 0.75.0-next.0 and support fine-grained selection ([#69](https://github.com/plait-board/drawnix/pull/69))\n- **draw-toolbar:** add draw toolbar ([#9](https://github.com/plait-board/drawnix/pull/9))\n- **draw-toolbar:** add shape and arrow panel for draw-toolbar #10 ([#12](https://github.com/plait-board/drawnix/pull/12), [#10](https://github.com/plait-board/drawnix/issues/10))\n- **drawnix:** init drawnix package ([397d865](https://github.com/plait-board/drawnix/commit/397d865))\n- **drawnix-board:** initialize drawnix board ([117e5a8](https://github.com/plait-board/drawnix/commit/117e5a8))\n- **fill:** split fill color and fill opacity setting ([#53](https://github.com/plait-board/drawnix/pull/53))\n- **flowchart:** add terminal shape element ([#80](https://github.com/plait-board/drawnix/pull/80))\n- **freehand:** initialize freehand #2 ([#2](https://github.com/plait-board/drawnix/issues/2))\n- **freehand:** apply gaussianSmooth to freehand curve ([#47](https://github.com/plait-board/drawnix/pull/47))\n- **freehand:** update stroke width to 2 and optimize freehand end points ([#50](https://github.com/plait-board/drawnix/pull/50))\n- **freehand:** improve freehand experience ([#51](https://github.com/plait-board/drawnix/pull/51))\n- **freehand:** add FreehandSmoother to optimize freehand curve ([#62](https://github.com/plait-board/drawnix/pull/62))\n- **freehand:** optimize freehand curve by stylus features ([#63](https://github.com/plait-board/drawnix/pull/63))\n- **freehand:** freehand support theme ([b7c7965](https://github.com/plait-board/drawnix/commit/b7c7965))\n- **freehand:** support closed freehand and add popup for freehand ([#68](https://github.com/plait-board/drawnix/pull/68))\n- **freehand:** bump plait into 0.75.0-next.9 and resolve freehand unexpected resize-handle after moving freehand elements ([#84](https://github.com/plait-board/drawnix/pull/84))\n- **hotkey:** support export png hotkey ([#30](https://github.com/plait-board/drawnix/pull/30))\n- **image:** support free image element and support insert image at m… ([#95](https://github.com/plait-board/drawnix/pull/95))\n- **image:** should hide popup toolbar when selected element include image ([#96](https://github.com/plait-board/drawnix/pull/96))\n- **menu:** support export to json file ([d0d6ca5](https://github.com/plait-board/drawnix/commit/d0d6ca5))\n- **menu:** support load file action ([758aa6d](https://github.com/plait-board/drawnix/commit/758aa6d))\n- **mobile:** adapt mobile device ([7c0742f](https://github.com/plait-board/drawnix/commit/7c0742f))\n- **pencil-mode:** add pencil mode and add drawnix context ([#76](https://github.com/plait-board/drawnix/pull/76))\n- **pinch-zoom:** support pinch zoom for touch device ([#60](https://github.com/plait-board/drawnix/pull/60))\n- **pinch-zoom:** improve pinch zoom functionality and support hand moving ([#75](https://github.com/plait-board/drawnix/pull/75))\n- **popover:** add reusable popover and replace radix popover ([d30388a](https://github.com/plait-board/drawnix/commit/d30388a))\n- **popup:** display icon when color is complete opacity ([#42](https://github.com/plait-board/drawnix/pull/42))\n- **popup-toolbar:** support set branch color remove color property when select transparent #17 ([#17](https://github.com/plait-board/drawnix/issues/17))\n- **popup-toolbar:** bump plait into 0.71.0 and mind node link stroke and node stroke support dashed/dotted style #22 ([#22](https://github.com/plait-board/drawnix/issues/22))\n- **property:** support stroke style setting ([463c92a](https://github.com/plait-board/drawnix/commit/463c92a))\n- **size-slider:** improve size-slider component ([780be9d](https://github.com/plait-board/drawnix/commit/780be9d))\n- **text:** support soft break ([#39](https://github.com/plait-board/drawnix/pull/39))\n- **text:** support update text from outside ([#58](https://github.com/plait-board/drawnix/pull/58))\n- **theme-toolbar:** add theme selection toolbar for customizable themes ([dca0e33](https://github.com/plait-board/drawnix/commit/dca0e33))\n- **toolbar:** support zoom toolbar ([76ef5d9](https://github.com/plait-board/drawnix/commit/76ef5d9))\n- **web:** seo ([84cde4b](https://github.com/plait-board/drawnix/commit/84cde4b))\n- **web:** add cloud.umami.is to track views ([#64](https://github.com/plait-board/drawnix/pull/64))\n- **web:** modify initialize-data for adding freehand data ([#65](https://github.com/plait-board/drawnix/pull/65))\n- **web:** add debug console ([#83](https://github.com/plait-board/drawnix/pull/83))\n- **wrapper:** add wrapper component and context hook ([#6](https://github.com/plait-board/drawnix/pull/6))\n- **zoom-toolbar:** support zoom menu ([cc6a6b8](https://github.com/plait-board/drawnix/commit/cc6a6b8))\n\n### 🩹 Fixes\n\n- remove theme-toolbar font-weight style ([#67](https://github.com/plait-board/drawnix/pull/67))\n- **arrow-line:** optimize the popup toolbar position when selected element is arrow line ([#70](https://github.com/plait-board/drawnix/pull/70))\n- **board:** resolve mobile scrolling issue when resize or moving ([8fdca8e](https://github.com/plait-board/drawnix/commit/8fdca8e))\n- **board:** bump plait into 0.69.1 deselect when text editing end refactor popup toolbar placement ([aef6d23](https://github.com/plait-board/drawnix/commit/aef6d23))\n- **board:** use updateViewBox to fix board wobbles when dragging or resizing ([#94](https://github.com/plait-board/drawnix/pull/94))\n- **color-picker:** support display 0 opacity ([#48](https://github.com/plait-board/drawnix/pull/48))\n- **creation-toolbar:** use pointerUp set basic pointer cause onChange do not fire on mobile bind pointermove/pointerup to viewportContainerRef to implement dnd on mobile #20 ([#20](https://github.com/plait-board/drawnix/issues/20))\n- **creation-toolbar:** move out toolbar from board to avoid fired pointer down event when operating ([ddb6092](https://github.com/plait-board/drawnix/commit/ddb6092))\n- **font-color:** fix color can not be assigned when current color is empty ([#55](https://github.com/plait-board/drawnix/pull/55))\n- **freehand:** fix freehand creation issue(caused by throttleRAF) ([#40](https://github.com/plait-board/drawnix/pull/40))\n- **mind:** remove branchColor property setting ([#46](https://github.com/plait-board/drawnix/pull/46))\n- **property:** prevent set fill color opacity when color is none ([#56](https://github.com/plait-board/drawnix/pull/56))\n- **react-board:** resolve text should not display in safari ([19fc20f](https://github.com/plait-board/drawnix/commit/19fc20f))\n- **size-slider:** correct size slider click handle can not fire ([#57](https://github.com/plait-board/drawnix/pull/57))\n- **text:** fix composition input and abc input trembly issue ([#15](https://github.com/plait-board/drawnix/pull/15))\n- **text:** resolve with-text build error ([#41](https://github.com/plait-board/drawnix/pull/41))\n- **text:** fix text can not editing ([#52](https://github.com/plait-board/drawnix/pull/52))\n- **use-board-event:** fix board event timing ([0d4a8f1](https://github.com/plait-board/drawnix/commit/0d4a8f1))\n\n### ❤️  Thank You\n\n- pubuzhixing8 @pubuzhixing8"
  },
  {
    "path": "Dockerfile",
    "content": "FROM node:20 AS builder \n\nWORKDIR /builder\n\nCOPY . /builder\n\nRUN npm install \\\n    && npm run build \n\n\nFROM lipanski/docker-static-website:2.4.0\n\nWORKDIR /home/static\n\nCOPY  --from=builder /builder/dist/apps/web/  /home/static\n\nEXPOSE 80\n\nCMD [\"/busybox-httpd\", \"-f\", \"-v\", \"-p\", \"80\", \"-c\", \"httpd.conf\"]"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Drawnix\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n  <picture style=\"width: 320px\">\n    <source media=\"(prefers-color-scheme: light)\" srcset=\"https://github.com/plait-board/drawnix/blob/develop/apps/web/public/logo/logo_drawnix_h.svg?raw=true\" />\n    <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://github.com/plait-board/drawnix/blob/develop/apps/web/public/logo/logo_drawnix_h_dark.svg?raw=true\" />\n    <img src=\"https://github.com/plait-board/drawnix/blob/develop/apps/web/public/logo/logo_drawnix_h.svg?raw=true\" width=\"360\" alt=\"Drawnix logo and name\" />\n  </picture>\n</p>\n<div align=\"center\">\n  <h2>\n    开源白板工具（SaaS），一体化白板，包含思维导图、流程图、自由画等\n  <br />\n  </h2>\n</div>\n\n<div align=\"center\">\n  <figure>\n    <a target=\"_blank\" rel=\"noopener\">\n      <img src=\"https://github.com/plait-board/drawnix/blob/develop/apps/web/public/product_showcase/case-2.png\" alt=\"Product showcase\" width=\"80%\" />\n    </a>\n    <figcaption>\n      <p align=\"center\">\n        All in one 白板，思维导图、流程图、自由画等\n      </p>\n    </figcaption>\n  </figure>\n  <a href=\"https://hellogithub.com/repository/plait-board/drawnix\" target=\"_blank\">\n    <picture style=\"width: 250\">\n      <source media=\"(prefers-color-scheme: light)\" srcset=\"https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=4dcea807fab7468a962c153b07ae4e4e&claim_uid=zmFSY5k8EuZri43&theme=neutral\" />\n      <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=4dcea807fab7468a962c153b07ae4e4e&claim_uid=zmFSY5k8EuZri43&theme=dark\" />\n      <img src=\"https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=4dcea807fab7468a962c153b07ae4e4e&claim_uid=zmFSY5k8EuZri43&theme=neutral\" alt=\"Featured｜HelloGitHub\" style=\"width: 250px; height: 54px;\" width=\"250\" height=\"54\"/>\n    </picture>\n  </a>\n\n  <br />\n\n  <a href=\"https://trendshift.io/repositories/13979\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/13979\" alt=\"plait-board%2Fdrawnix | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n</div>\n\n[*English README*](https://github.com/plait-board/drawnix/blob/develop/README_en.md)\n\n## 特性\n\n- 💯 免费 + 开源\n- ⚒️ 思维导图、流程图\n- 🖌 画笔\n- 😀 插入图片\n- 🚀 基于插件机制\n- 🖼️ 📃 导出为 PNG, JSON(`.drawnix`)\n- 💾 自动保存（浏览器缓存）\n- ⚡ 编辑特性：撤销、重做、复制、粘贴等\n- 🌌 无限画布：缩放、滚动\n- 🎨 主题模式\n- 📱 移动设备适配\n- 📈 支持 mermaid 语法转流程图\n- ✨ 支持 markdown 文本转思维导图（新支持 🔥🔥🔥）\n\n\n## 关于名称\n\n***Drawnix***  ，源于绘画(  ***Draw***  )与凤凰(  ***Phoenix***  )的灵感交织。\n\n凤凰象征着生生不息的创造力，而 *Draw* 代表着人类最原始的表达方式。在这里，每一次创作都是一次艺术的涅槃，每一笔绘画都是灵感的重生。\n\n创意如同凤凰，浴火方能重生，而  ***Drawnix***  要做技术与创意之火的守护者。\n\n*Draw Beyond, Rise Above.*\n\n\n## 与 Plait 画图框架\n\n*Drawnix* 的定位是一个开箱即用、开源、免费的工具产品，它的底层是 *Plait* 框架，*Plait* 是我司开源的一款画图框架，代表着公司在知识库产品([PingCode Wiki](https://pingcode.com/product/wiki?utm_source=drawnix))上的重要技术沉淀。\n\n\nDrawnix 是插件架构，与前面说到开源工具比技术架构更复杂一些，但是插件架构也有优势，比如能够支持多种 UI 框架（*Angular、React*），能够集成不同富文本框架（当前仅支持 *Slate* 框架），在开发上可以很好的实现业务的分层，开发各种细粒度的可复用插件，可以扩展更多的画板的应用场景。\n\n\n## 仓储结构\n\n```\ndrawnix/\n├── apps/\n│   ├── web                   # drawnix.com\n│   │    └── index.html       # HTML\n├── dist/                     # 构建产物\n├── packages/\n│   └── drawnix/              # 白板应用\n│   └── react-board/          # 白板 React 视图层\n│   └── react-text/           # 文本渲染模块\n├── package.json\n├── ...\n└── README.md\n└── README_en.md\n\n```\n\n## 应用\n\n[*https://drawnix.com*](https://drawnix.com) 是 *drawnix* 的最小化应用。\n\n近期会高频迭代 drawnix.com，直到发布 *Dawn（破晓）* 版本。\n\n\n## 开发\n\n```\nnpm install\n\nnpm run start\n```\n\n## Docker\n\n```\ndocker pull pubuzhixing/drawnix:latest\n```\n\n## 依赖\n\n- [plait](https://github.com/worktile/plait) - 开源画图框架\n- [slate](https://github.com/ianstormtaylor/slate)  - 富文本编辑器框架\n- [floating-ui](https://github.com/floating-ui/floating-ui)  - 一个超级好用的创建弹出层基础库\n\n\n\n## 贡献\n\n欢迎任何形式的贡献：\n\n- 提 Bug\n\n- 贡献代码\n\n## 感谢支持\n\n特别感谢公司对开源项目的大力支持，也感谢为本项目贡献代码、提供建议的朋友。\n\n<p align=\"left\">\n  <a href=\"https://pingcode.com?utm_source=drawnix\" target=\"_blank\">\n      <img src=\"https://cdn-aliyun.pingcode.com/static/site/img/pingcode-logo.4267e7b.svg\" width=\"120\" alt=\"PingCode\" />\n  </a>\n</p>\n\n## License\n\n[MIT License](https://github.com/plait-board/drawnix/blob/master/LICENSE)  "
  },
  {
    "path": "README_en.md",
    "content": "<p align=\"center\">\n  <picture style=\"width: 320px\">\n    <source media=\"(prefers-color-scheme: light)\" srcset=\"https://github.com/plait-board/drawnix/blob/develop/apps/web/public/logo/logo_drawnix_h.svg?raw=true\" />\n    <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://github.com/plait-board/drawnix/blob/develop/apps/web/public/logo/logo_drawnix_h_dark.svg?raw=true\" />\n    <img src=\"https://github.com/plait-board/drawnix/blob/develop/apps/web/public/logo/logo_drawnix_h.svg?raw=true\" width=\"360\" alt=\"Drawnix logo and name\" />\n  </picture>\n</p>\n<div align=\"center\">\n  <h2>\n    Open-source whiteboard tool (SaaS), an all-in-one collaborative canvas that includes mind mapping, flowcharts, freehand and more.\n  <br />\n  </h2>\n</div>\n\n<div align=\"center\">\n  <figure>\n    <a target=\"_blank\" rel=\"noopener\">\n      <img src=\"https://github.com/plait-board/drawnix/blob/develop/apps/web/public/product_showcase/case-2.png\" alt=\"Product showcase\" width=\"80%\" />\n    </a>\n    <figcaption>\n      <p align=\"center\">\n      Whiteboard with mind mapping, flowcharts, freehand drawing and more\n      </p>\n    </figcaption>\n  </figure>\n  <a href=\"https://hellogithub.com/repository/plait-board/drawnix\" target=\"_blank\">\n    <picture style=\"width: 250\">\n      <source media=\"(prefers-color-scheme: light)\" srcset=\"https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=4dcea807fab7468a962c153b07ae4e4e&claim_uid=zmFSY5k8EuZri43&theme=neutral\" />\n      <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=4dcea807fab7468a962c153b07ae4e4e&claim_uid=zmFSY5k8EuZri43&theme=dark\" />\n      <img src=\"https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=4dcea807fab7468a962c153b07ae4e4e&claim_uid=zmFSY5k8EuZri43&theme=neutral\" alt=\"Featured｜HelloGitHub\" style=\"width: 250px; height: 54px;\" width=\"250\" height=\"54\"/>\n    </picture>\n  </a>\n\n  <br />\n\n  <a href=\"https://trendshift.io/repositories/13979\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/13979\" alt=\"plait-board%2Fdrawnix | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n</div>\n\n[*中文*](https://github.com/plait-board/drawnix/blob/develop/README.md)\n\n## Features\n\n- 💯 Free and Open Source\n- ⚒️ Mind Maps and Flowcharts\n- 🖌 Freehand\n- 😀 Image Support\n- 🚀 Plugin-based Architecture - Extensible\n- 🖼️ 📃 Export to PNG, JPG, JSON(.drawnix)\n- 💾 Auto-save (Browser Storage)\n- ⚡ Edit Features: Undo, Redo, Copy, Paste, etc.\n- 🌌 Infinite Canvas: Zoom, Pan\n- 🎨 Theme Support\n- 📱 Mobile-friendly\n- 📈 Support mermaid syntax conversion to flowchart\n- ✨ Support markdown text conversion to mind map（New 🔥🔥🔥）\n\n\n## About the Name\n\n***Drawnix*** is born from the interweaving of ***Draw*** and ***Phoenix***, a fusion of artistic inspiration.\n\nThe *Phoenix* symbolizes endless creativity, while *Draw* represents humanity's most fundamental form of expression. Here, each creation is an artistic rebirth, every stroke a renaissance of inspiration.\n\nLike a Phoenix, creativity must rise from the flames to be reborn, and ***Drawnix*** stands as the guardian of both technical and creative fire.\n\n*Draw Beyond, Rise Above.*\n\n## About Plait Drawing Framework\n\n*Drawnix* is positioned as an out-of-the-box, *open-source*, and free tool product. It is built on top of the *Plait* framework, which is our company's *open-source* drawing framework representing significant technical accumulation in knowledge base products([PingCode Wiki](https://pingcode.com/product/wiki?utm_source=drawnix)).\n\n\n*Drawnix* uses a *plugin architecture*, which is technically more complex than the previously mentioned *open-source* tools. However, this *plugin architecture* has its advantages: it supports multiple *UI frameworks* (*Angular*, *React*), integrates with different *rich text frameworks* (currently only supporting *Slate* framework), enables better business layer separation in development, allows development of various fine-grained reusable plugins, and can expand to more whiteboard application scenarios.\n\n## Repository Structure\n\n```\ndrawnix/\n├── apps/\n│   ├── web                   # drawnix.com\n│   │    └── index.html       # HTML\n├── dist/                     # Build artifacts\n├── packages/\n│   └── drawnix/              # Whiteboard application core\n│   └── react-board/          # Whiteboard react view layer\n│   └── react-text/           # Text rendering module\n├── package.json\n├── ...\n└── README.md\n└── README_en.md\n\n```\n\n## Try It Out\n\n*https://drawnix.com* is the minimal application of *drawnix*.\n\nI will be iterating frequently on *drawnix.com* until the release of the *Dawn* version.\n\n\n## Development\n\n```\nnpm install\n\nnpm run start\n```\n\n## Docker\n\n```\ndocker pull pubuzhixing/drawnix:latest\n```\n\n## Dependencies\n\n- [plait](https://github.com/worktile/plait) - Open source drawing framework\n- [slate](https://github.com/ianstormtaylor/slate) - Rich text editor framework\n- [floating-ui](https://github.com/floating-ui/floating-ui) - An awesome library for creating floating UI elements\n\n\n## Contributing\n\nAny form of contribution is welcome:\n\n- Report bugs\n\n- Contribute code\n\n## Thank you for supporting\n\nSpecial thanks to the company for its strong support for open source projects, and also to the friends who contributed code and provided suggestions to this project.\n\n<p align=\"left\">\n  <a href=\"https://pingcode.com?utm_source=drawnix\" target=\"_blank\">\n      <img src=\"https://cdn-aliyun.pingcode.com/static/site/img/pingcode-logo.4267e7b.svg\" width=\"120\" alt=\"PingCode\" />\n  </a>\n</p>\n\n## License\n\n[MIT License](https://github.com/plait-board/drawnix/blob/master/LICENSE)"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Reporting a Vulnerability\n\nWe have an official discord server for discussing and reporting about Drawnix.\nPlease contact pubuzhixing in the server if the valnerability is confidential and critical.\n[Discord Server Link](https://discord.gg/5d9undgnsP)\n"
  },
  {
    "path": "apps/web/.eslintrc.json",
    "content": "{\n  \"extends\": [\"plugin:@nx/react\", \"../../.eslintrc.json\"],\n  \"ignorePatterns\": [\"!**/*\"],\n  \"overrides\": [\n    {\n      \"files\": [\"*.ts\", \"*.tsx\", \"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.ts\", \"*.tsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/web/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <!-- 基本 SEO Meta 标签 (中文) -->\n    <title>Drawnix - 开源白板工具</title>\n    <meta name=\"description\" content=\"Drawnix 是一款强大的开源白板工具（https://github.com/plait-board/drawnix），集成思维导图、流程图等功能。基于 Plait 框架开发，支持插件扩展，提供自动保存、无限画布等特性。Draw Beyond, Rise Above.\">\n    <meta name=\"keywords\" content=\"Drawnix,白板工具,白板,思维导图,流程图,开源白板,开源思维导图,在线绘图,在线白板,协作工具,协作白板,Plait 框架\">\n    <!-- 基本 SEO Meta 标签 (English) -->\n    <meta name=\"description\" lang=\"en\" content=\"Drawnix is a powerful open-source whiteboard tool featuring mind mapping and flowchart capabilities. Built on the Plait framework, it offers plugin extensibility, auto-save, infinite canvas, and more. Draw Beyond, Rise Above.\">\n    <meta name=\"keywords\" lang=\"en\" content=\"Drawnix,whiteboard tool, whiteboard,mind mapping,flowchart,open source whiteboard, open source mind mapping,online drawing,collaboration tool, collaboration whiteboard,Plait framework\">\n    <!-- 基本 SEO Meta 标签 (Русский) -->\n    <meta name=\"description\" lang=\"ru\" content=\"Drawnix — это виртуальная доска с открытым исходным кодом, позволяющая строить mind-карты и блок-схемы. Написанный на основе фреймворка Plait, он имеет возможности расширения плагинами, автосохранения, бесконечного холта и многое другое. Draw Beyond, Rise Above.\">\n    <meta name=\"keywords\" lang=\"ru\" content=\"Drawnix,доска,виртуальная доска,mind-карты,интеллект-карты,карты мыслей,блоксхемы,блок-схемы,open source доска,доска с открытым кодом,open source mind-карты,mind-карты с открытым кодом,онлайн-рисование,рисовалка онлайн,совместная работа,доска для совместной работы,электронная доска для совместной работы,Plait framework,фреймворк Plait\">\n    <!-- Open Graph Meta 标签 (中文) -->\n    <meta property=\"og:title\" content=\"Drawnix - 开源白板工具 | 思维导图 | 流程图 | 白板 | 协作白板\">\n    <meta property=\"og:description\" content=\"一体化开源白板工具（在线白板 | 协作白板），支持思维导图、流程图，基于 Plait 框架开发\">\n    <meta property=\"og:type\" content=\"website\">\n    <meta property=\"og:url\" content=\"https://drawnix.com\">\n    <meta property=\"og:site_name\" content=\"Drawnix\">\n    <!-- Open Graph Meta 标签 (English) -->\n    <meta property=\"og:title\" lang=\"en\" content=\"Drawnix - Open Source Whiteboard | Mind Mapping | Flowchart | Whiteboard | Collaboration Whiteboard\">\n    <meta property=\"og:description\" lang=\"en\" content=\"An integrated open-source whiteboard tool(online whiteboard | collaboration whiteboard) supporting mind mapping and flowcharts, built on the Plait framework\">\n    <!-- Open Graph Meta 标签 (Русский) -->\n    <meta property=\"og:title\" lang=\"ru\" content=\"Drawnix - Доска с открытым кодом | Mind-карты | Блок-схемы | Электронная доска для совместной работы\">\n    <meta property=\"og:description\" lang=\"ru\" content=\"Интегрированная доска с открытым исходным кодом (онлайн доска | доска для совеместной работы), поддерживающая создание mind-карт и блок-схем, построенная на основе фреймворка Plait\">\n    <!-- Twitter Card Meta 标签 (English) -->\n    <meta name=\"twitter:card\" content=\"summary_large_image\">\n    <meta name=\"twitter:title\" content=\"Drawnix - Open Source Whiteboard | Mind Mapping | Flowchart | Whiteboard | Collaboration Whiteboard\">\n    <meta name=\"twitter:description\" content=\"An integrated open-source whiteboard tool(online whiteboard | collaboration whiteboard) supporting mind mapping and flowcharts, built on the Plait framework\">\n    <!-- 其他重要 Meta 标签 -->\n    <meta name=\"robots\" content=\"index, follow\">\n    <meta name=\"author\" content=\"Drawnix Team\">\n    <link rel=\"canonical\" href=\"https://drawnix.com\">\n    <!-- 语言替代链接 -->\n    <link rel=\"alternate\" hreflang=\"zh-CN\" href=\"https://drawnix.com\">\n    <!-- <link rel=\"alternate\" hreflang=\"en\" href=\"https://drawnix.com/en\"> -->\n    <!-- <link rel=\"alternate\" hreflang=\"ru\" href=\"https://drawnix.com/ru\"> -->\n    <link rel=\"alternate\" hreflang=\"x-default\" href=\"https://drawnix.com\">\n    <!-- 结构化数据 (JSON-LD) - 中英双语 -->\n    <script type=\"application/ld+json\">\n        {\n          \"@context\": \"https://schema.org\",\n          \"@type\": \"SoftwareApplication\",\n          \"name\": \"Drawnix\",\n          \"alternateName\": [\"开源白板工具\", \"Open Source Whiteboard Tool\"],\n          \"description\": {\n            \"zh-CN\": \"Drawnix 是一款强大的开源白板工具，集成思维导图、流程图等功能。基于 Plait 框架开发，支持插件扩展，提供自动保存、无限画布等特性。\",\n            \"en\": \"Drawnix is a powerful open-source whiteboard tool featuring mind mapping and flowchart capabilities. Built on the Plait framework, it offers plugin extensibility, auto-save, infinite canvas, and more.\",\n            \"ru\": \"Drawnix — это мощная виртуальная доска с открытым исходным кодом, позволяющая строить mind-карты и блок-схемы. Написанный на основе фреймворка Plait, он имеет возможности расширения плагинами, автосохранения, бесконечного холта и многое другое.\"\n          },\n          \"url\": \"https://drawnix.com\",\n          \"applicationCategory\": \"DesignApplication\",\n          \"operatingSystem\": \"Any\",\n          \"offers\": {\n            \"@type\": \"Offer\",\n            \"price\": \"0\",\n            \"priceCurrency\": \"USD\"\n          },\n          \"creator\": {\n            \"@type\": \"Organization\",\n            \"name\": \"Drawnix Team\"\n          },\n          \"keywords\": [\"доска\", \"электронная доска\", \"mind-карты\", \"блок-схемы\", \"диаграммы\", \"whiteboard\", \"mind mapping\", \"flowchart\", \"open source\", \"思维导图\", \"流程图\", \"白板\"],\n          \"inLanguage\": [\"zh-CN\", \"en\", \"ru\"],\n          \"license\": \"https://github.com/plait-board/drawnix/blob/master/LICENSE\"\n        }\n    </script>\n    <base href=\"/\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no\" />\n    <link rel=\"icon\" type=\"image/x-icon\" href=\"/favicon.ico\" />\n    <link rel=\"stylesheet\" href=\"/src/styles.scss\" />\n    <script defer src=\"https://cloud.umami.is/script.js\" data-website-id=\"7083aa92-85b1-4a67-a6d4-03d52819ba3d\"></script>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/web/jest.config.ts",
    "content": "/* eslint-disable */\nexport default {\n  displayName: 'web',\n  preset: '../../jest.preset.js',\n  transform: {\n    '^(?!.*\\\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',\n    '^.+\\\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }],\n  },\n  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],\n  coverageDirectory: '../../coverage/apps/web',\n};\n"
  },
  {
    "path": "apps/web/project.json",
    "content": "{\n  \"name\": \"web\",\n  \"$schema\": \"../../node_modules/nx/schemas/project-schema.json\",\n  \"sourceRoot\": \"apps/web/src\",\n  \"projectType\": \"application\",\n  \"tags\": [],\n  \"// targets\": \"to see all targets run: nx show project web --web\",\n  \"targets\": {}\n}\n"
  },
  {
    "path": "apps/web/public/_headers",
    "content": "# 基本缓存配置\n/*\n  Cache-Control: public, max-age=31536000, immutable\n\n/*.html\n  Cache-Control: public, max-age=0, must-revalidate\n\n/\n  Cache-Control: public, max-age=0, must-revalidate\n"
  },
  {
    "path": "apps/web/public/_redirects",
    "content": "# SPA 路由支持\n/*    /index.html   200\n"
  },
  {
    "path": "apps/web/public/robots.txt",
    "content": "id: robots-txt\nname: Robots.txt\ntype: code.txt\ncontent: |-\n  User-agent: *\n  Allow: /\n  \n  # 禁止访问管理后台\n  Disallow: /admin/\n  Disallow: /private/\n  \n  # 站点地图\n  Sitemap: https://drawnix.com/sitemap.xml"
  },
  {
    "path": "apps/web/public/sitemap.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"\n    xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n    xsi:schemaLocation=\"http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd\">\n    <url>\n        <loc>https://drawnix.com/</loc>\n        <lastmod>2024-11-15</lastmod>\n        <changefreq>weekly</changefreq>\n        <priority>1.0</priority>\n    </url>\n    <url>\n        <loc>https://drawnix.com/en</loc>\n        <lastmod>2024-11-15</lastmod>\n        <changefreq>weekly</changefreq>\n        <priority>0.9</priority>\n    </url>\n    <url>\n        <loc>https://drawnix.com/docs</loc>\n        <lastmod>2024-11-15</lastmod>\n        <changefreq>weekly</changefreq>\n        <priority>0.8</priority>\n    </url>\n    <url>\n        <loc>https://drawnix.com/docs/getting-started</loc>\n        <lastmod>2024-11-15</lastmod>\n        <changefreq>monthly</changefreq>\n        <priority>0.7</priority>\n    </url>\n</urlset>"
  },
  {
    "path": "apps/web/src/app/app.module.scss",
    "content": "/* Your styles goes here. */\n"
  },
  {
    "path": "apps/web/src/app/app.spec.tsx",
    "content": "import { render } from '@testing-library/react';\n\nimport App from './app';\n\ndescribe('App', () => {\n  it('should render successfully', () => {\n    // const { baseElement } = render(<App />);\n    // expect(baseElement).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "apps/web/src/app/app.tsx",
    "content": "import { useState, useEffect } from 'react';\nimport { Drawnix } from '@drawnix/drawnix';\nimport { PlaitBoard, PlaitElement, PlaitTheme, Viewport } from '@plait/core';\nimport localforage from 'localforage';\n\ntype AppValue = {\n  children: PlaitElement[];\n  viewport?: Viewport;\n  theme?: PlaitTheme;\n};\n\nconst MAIN_BOARD_CONTENT_KEY = 'main_board_content';\n\nlocalforage.config({\n  name: 'Drawnix',\n  storeName: 'drawnix_store',\n  driver: [localforage.INDEXEDDB, localforage.LOCALSTORAGE],\n});\n\nexport function App() {\n  const [value, setValue] = useState<AppValue>({ children: [] });\n\n  const [tutorial, setTutorial] = useState(false);\n\n  useEffect(() => {\n    const loadData = async () => {\n      const storedData = (await localforage.getItem(\n        MAIN_BOARD_CONTENT_KEY\n      )) as AppValue;\n      if (storedData) {\n        setValue(storedData);\n        if (storedData.children && storedData.children.length === 0) {\n          setTutorial(true);\n        }\n        return;\n      }\n      setTutorial(true);\n    };\n    loadData();\n  }, []);\n  return (\n    <Drawnix\n      value={value.children}\n      viewport={value.viewport}\n      theme={value.theme}\n      onChange={(value) => {\n        const newValue = value as AppValue;\n        localforage.setItem(MAIN_BOARD_CONTENT_KEY, newValue);\n        setValue(newValue);\n        if (newValue.children && newValue.children.length > 0) {\n          setTutorial(false);\n        }\n      }}\n      tutorial={tutorial}\n      afterInit={(board) => {\n        console.log('board initialized');\n\n        // console.log(\n        //   `add __drawnix__web__debug_log to window, so you can call add log anywhere, like: window.__drawnix__web__console('some thing')`\n        // );\n        // (window as any)['__drawnix__web__console'] = (value: string) => {\n        //   addDebugLog(board, value);\n        // };\n      }}\n    ></Drawnix>\n  );\n}\n\nconst addDebugLog = (board: PlaitBoard, value: string) => {\n  const container = PlaitBoard.getBoardContainer(board).closest(\n    '.drawnix'\n  ) as HTMLElement;\n  let consoleContainer = container.querySelector('.drawnix-console');\n  if (!consoleContainer) {\n    consoleContainer = document.createElement('div');\n    consoleContainer.classList.add('drawnix-console');\n    container.append(consoleContainer);\n  }\n  const div = document.createElement('div');\n  div.innerHTML = value;\n  consoleContainer.append(div);\n};\n\nexport default App;\n"
  },
  {
    "path": "apps/web/src/assets/.gitkeep",
    "content": ""
  },
  {
    "path": "apps/web/src/main.tsx",
    "content": "import { StrictMode } from 'react';\nimport * as ReactDOM from 'react-dom/client';\n\nimport App from './app/app';\n\nconst root = ReactDOM.createRoot(\n  document.getElementById('root') as HTMLElement\n);\nroot.render(\n  <StrictMode>\n    <App />\n  </StrictMode>\n);\n"
  },
  {
    "path": "apps/web/src/styles.scss",
    "content": "/* You can add global styles to this file, and also import other style files */\nbody {\n    margin: 0;\n    padding: 0;\n}\nhtml,\nbody {\n    height: 100%;\n    width: 100%;\n    overflow: hidden;\n}\n#root {\n    height: 100%;\n    width: 100%;\n    overflow: hidden;\n}\n.drawnix-console {\n    position: absolute;\n    top: 50%;\n    transform: translateY(-50%);\n    left: 0;\n    height: 200px;\n    width: 100px;\n    overflow: auto;\n    background-color: black;\n    color: white;\n    padding: 8px;\n    opacity: 0.5;\n}"
  },
  {
    "path": "apps/web/tsconfig.app.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../dist/out-tsc\",\n    \"types\": [\n      \"node\",\n      \"@nx/react/typings/cssmodule.d.ts\",\n      \"@nx/react/typings/image.d.ts\",\n      \"vite/client\"\n    ]\n  },\n  \"exclude\": [\n    \"src/**/*.spec.ts\",\n    \"src/**/*.test.ts\",\n    \"src/**/*.spec.tsx\",\n    \"src/**/*.test.tsx\",\n    \"src/**/*.spec.js\",\n    \"src/**/*.test.js\",\n    \"src/**/*.spec.jsx\",\n    \"src/**/*.test.jsx\"\n  ],\n  \"include\": [\"src/**/*.js\", \"src/**/*.jsx\", \"src/**/*.ts\", \"src/**/*.tsx\"]\n}\n"
  },
  {
    "path": "apps/web/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"jsx\": \"react-jsx\",\n    \"allowJs\": false,\n    \"esModuleInterop\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"types\": [\"vite/client\"]\n  },\n  \"files\": [],\n  \"include\": [],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.app.json\"\n    },\n    {\n      \"path\": \"./tsconfig.spec.json\"\n    }\n  ],\n  \"extends\": \"../../tsconfig.base.json\"\n}\n"
  },
  {
    "path": "apps/web/tsconfig.spec.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../dist/out-tsc\",\n    \"module\": \"commonjs\",\n    \"types\": [\n      \"jest\",\n      \"node\",\n      \"@nx/react/typings/cssmodule.d.ts\",\n      \"@nx/react/typings/image.d.ts\"\n    ]\n  },\n  \"include\": [\n    \"jest.config.ts\",\n    \"src/**/*.test.ts\",\n    \"src/**/*.spec.ts\",\n    \"src/**/*.test.tsx\",\n    \"src/**/*.spec.tsx\",\n    \"src/**/*.test.js\",\n    \"src/**/*.spec.js\",\n    \"src/**/*.test.jsx\",\n    \"src/**/*.spec.jsx\",\n    \"src/**/*.d.ts\"\n  ]\n}\n"
  },
  {
    "path": "apps/web/vite.config.ts",
    "content": "/// <reference types='vitest' />\nimport { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';\n\nexport default defineConfig({\n  root: __dirname,\n  cacheDir: '../../node_modules/.vite/apps/web',\n\n  server: {\n    port: 7200,\n    host: 'localhost',\n  },\n\n  preview: {\n    port: 4300,\n    host: 'localhost',\n  },\n\n  plugins: [react(), nxViteTsPaths()],\n\n  // Uncomment this if you are using workers.\n  // worker: {\n  //  plugins: [ nxViteTsPaths() ],\n  // },\n\n  build: {\n    outDir: '../../dist/apps/web',\n    emptyOutDir: true,\n    reportCompressedSize: true,\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n});\n"
  },
  {
    "path": "apps/web-e2e/.eslintrc.json",
    "content": "{\n  \"extends\": [\"plugin:playwright/recommended\", \"../../.eslintrc.json\"],\n  \"ignorePatterns\": [\"!**/*\"],\n  \"overrides\": [\n    {\n      \"files\": [\"*.ts\", \"*.tsx\", \"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.ts\", \"*.tsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"src/**/*.{ts,js,tsx,jsx}\"],\n      \"rules\": {}\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/web-e2e/playwright.config.ts",
    "content": "import { defineConfig, devices } from '@playwright/test';\nimport { nxE2EPreset } from '@nx/playwright/preset';\n\nimport { workspaceRoot } from '@nx/devkit';\n\n// For CI, you may want to set BASE_URL to the deployed application.\nconst baseURL = process.env['BASE_URL'] || 'http://localhost:7200';\n\n/**\n * Read environment variables from file.\n * https://github.com/motdotla/dotenv\n */\n// require('dotenv').config();\n\n/**\n * See https://playwright.dev/docs/test-configuration.\n */\nexport default defineConfig({\n  ...nxE2EPreset(__filename, { testDir: './src' }),\n  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */\n  use: {\n    baseURL,\n    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */\n    trace: 'on-first-retry',\n  },\n  /* Run your local dev server before starting the tests */\n  webServer: {\n    command: 'npx nx serve web',\n    url: 'http://localhost:7200',\n    reuseExistingServer: !process.env.CI,\n    cwd: workspaceRoot,\n  },\n  projects: [\n    {\n      name: 'chromium',\n      use: { ...devices['Desktop Chrome'] },\n    },\n\n    {\n      name: 'firefox',\n      use: { ...devices['Desktop Firefox'] },\n    },\n\n    {\n      name: 'webkit',\n      use: { ...devices['Desktop Safari'] },\n    },\n\n    // Uncomment for mobile browsers support\n    /* {\n      name: 'Mobile Chrome',\n      use: { ...devices['Pixel 5'] },\n    },\n    {\n      name: 'Mobile Safari',\n      use: { ...devices['iPhone 12'] },\n    }, */\n\n    // Uncomment for branded browsers\n    /* {\n      name: 'Microsoft Edge',\n      use: { ...devices['Desktop Edge'], channel: 'msedge' },\n    },\n    {\n      name: 'Google Chrome',\n      use: { ...devices['Desktop Chrome'], channel: 'chrome' },\n    }, */\n  ],\n});"
  },
  {
    "path": "apps/web-e2e/project.json",
    "content": "{\n  \"name\": \"web-e2e\",\n  \"$schema\": \"../../node_modules/nx/schemas/project-schema.json\",\n  \"projectType\": \"application\",\n  \"sourceRoot\": \"apps/web-e2e/src\",\n  \"implicitDependencies\": [\"web\"],\n  \"// targets\": \"to see all targets run: nx show project web-e2e --web\",\n  \"targets\": {}\n}\n"
  },
  {
    "path": "apps/web-e2e/src/example.spec.ts",
    "content": "import { test, expect } from '@playwright/test';\n\ntest('has title', async ({ page }) => {\n  await page.goto('/');\n\n  // Expect h1 to contain a substring.\n  expect(await page.title()).toContain('Drawnix - 开源白板工具');\n  expect(page.locator('drawnix')).toBeTruthy();\n});\n"
  },
  {
    "path": "apps/web-e2e/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"allowJs\": true,\n    \"outDir\": \"../../dist/out-tsc\",\n    \"module\": \"commonjs\",\n    \"sourceMap\": false\n  },\n  \"include\": [\n    \"**/*.ts\",\n    \"**/*.js\",\n    \"playwright.config.ts\",\n    \"src/**/*.spec.ts\",\n    \"src/**/*.spec.js\",\n    \"src/**/*.test.ts\",\n    \"src/**/*.test.js\",\n    \"src/**/*.d.ts\"\n  ]\n}\n"
  },
  {
    "path": "jest.config.ts",
    "content": "import { getJestProjectsAsync } from '@nx/jest';\n\nexport default async () => ({\n  projects: await getJestProjectsAsync(),\n});\n"
  },
  {
    "path": "jest.preset.js",
    "content": "const nxPreset = require('@nx/jest/preset').default;\n\nmodule.exports = { ...nxPreset };\n"
  },
  {
    "path": "nx.json",
    "content": "{\n  \"$schema\": \"./node_modules/nx/schemas/nx-schema.json\",\n  \"namedInputs\": {\n    \"default\": [\n      \"{projectRoot}/**/*\",\n      \"sharedGlobals\"\n    ],\n    \"production\": [\n      \"default\",\n      \"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)\",\n      \"!{projectRoot}/tsconfig.spec.json\",\n      \"!{projectRoot}/.eslintrc.json\",\n      \"!{projectRoot}/eslint.config.js\",\n      \"!{projectRoot}/jest.config.[jt]s\",\n      \"!{projectRoot}/src/test-setup.[jt]s\",\n      \"!{projectRoot}/test-setup.[jt]s\"\n    ],\n    \"sharedGlobals\": []\n  },\n  \"plugins\": [\n    {\n      \"plugin\": \"@nx/vite/plugin\",\n      \"options\": {\n        \"buildTargetName\": \"build\",\n        \"testTargetName\": \"test\",\n        \"serveTargetName\": \"serve\",\n        \"previewTargetName\": \"preview\",\n        \"serveStaticTargetName\": \"serve-static\"\n      }\n    },\n    {\n      \"plugin\": \"@nx/eslint/plugin\",\n      \"options\": {\n        \"targetName\": \"lint\"\n      }\n    },\n    {\n      \"plugin\": \"@nx/playwright/plugin\",\n      \"options\": {\n        \"targetName\": \"e2e\"\n      }\n    },\n    {\n      \"plugin\": \"@nx/jest/plugin\",\n      \"options\": {\n        \"targetName\": \"test\"\n      }\n    }\n  ],\n  \"generators\": {\n    \"@nx/react\": {\n      \"application\": {\n        \"babel\": true,\n        \"style\": \"scss\",\n        \"linter\": \"eslint\",\n        \"bundler\": \"vite\"\n      },\n      \"component\": {\n        \"style\": \"scss\"\n      },\n      \"library\": {\n        \"style\": \"scss\",\n        \"linter\": \"eslint\",\n        \"unitTestRunner\": \"jest\"\n      }\n    }\n  },\n  \"release\": {\n    \"changelog\": {\n      \"workspaceChangelog\": true,\n      \"file\": \"CHANGELOG.md\",\n      \"git\": {\n        \"commit\": false,\n        \"tag\": false\n      }\n    },\n    \"version\": {\n      \"git\": {\n        \"commit\": false,\n        \"tag\": false\n      }\n    }\n  }\n}"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"@drawnix/source\",\n  \"version\": \"0.0.2\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"start\": \"nx serve web --host=0.0.0.0\",\n    \"build\": \"nx run-many -t=build\",\n    \"lint\": \"nx run-many --target=lint --all --fix\",\n    \"build:web\": \"nx build web\",\n    \"test\": \"nx run-many -t=test\",\n    \"release\": \"node scripts/release-version.js\",\n    \"pub\": \"npm run build && node scripts/publish.js\"\n  },\n  \"private\": true,\n  \"dependencies\": {\n    \"@floating-ui/react\": \"^0.26.24\",\n    \"@plait-board/markdown-to-drawnix\": \"^0.0.8\",\n    \"@plait-board/mermaid-to-drawnix\": \"^0.0.7\",\n    \"@plait/common\": \"^0.92.1\",\n    \"@plait/core\": \"^0.92.1\",\n    \"@plait/draw\": \"^0.92.1\",\n    \"@plait/layouts\": \"^0.92.1\",\n    \"@plait/mind\": \"^0.92.1\",\n    \"@plait/text-plugins\": \"^0.92.1\",\n    \"@types/lodash\": \"^4.17.21\",\n    \"ahooks\": \"^3.9.6\",\n    \"browser-fs-access\": \"^0.35.0\",\n    \"classnames\": \"^2.5.1\",\n    \"is-hotkey\": \"^0.2.0\",\n    \"laser-pen\": \"^1.0.1\",\n    \"localforage\": \"^1.10.0\",\n    \"lodash\": \"^4.17.21\",\n    \"mobile-detect\": \"^1.4.5\",\n    \"open-color\": \"^1.9.1\",\n    \"react\": \"19.2.0\",\n    \"react-dom\": \"19.2.0\",\n    \"roughjs\": \"^4.6.6\",\n    \"slate\": \"^0.116.0\",\n    \"slate-dom\": \"^0.116.0\",\n    \"slate-history\": \"^0.115.0\",\n    \"slate-react\": \"^0.116.0\",\n    \"tslib\": \"^2.3.0\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.14.5\",\n    \"@babel/preset-react\": \"^7.14.5\",\n    \"@nx/cypress\": \"19.3.0\",\n    \"@nx/devkit\": \"19.3.0\",\n    \"@nx/eslint\": \"19.3.0\",\n    \"@nx/eslint-plugin\": \"19.3.0\",\n    \"@nx/jest\": \"19.3.0\",\n    \"@nx/js\": \"19.3.0\",\n    \"@nx/playwright\": \"19.3.0\",\n    \"@nx/react\": \"19.3.0\",\n    \"@nx/vite\": \"^20.6.0\",\n    \"@nx/web\": \"19.3.0\",\n    \"@nx/workspace\": \"19.3.0\",\n    \"@playwright/test\": \"^1.36.0\",\n    \"@swc-node/register\": \"~1.9.1\",\n    \"@swc/cli\": \"^0.6.0\",\n    \"@swc/core\": \"~1.5.7\",\n    \"@swc/helpers\": \"~0.5.11\",\n    \"@testing-library/react\": \"16.3.0\",\n    \"@types/is-hotkey\": \"^0.1.10\",\n    \"@types/jest\": \"^29.4.0\",\n    \"@types/node\": \"18.16.9\",\n    \"@types/react\": \"18.3.1\",\n    \"@types/react-dom\": \"18.3.0\",\n    \"@typescript-eslint/eslint-plugin\": \"^7.3.0\",\n    \"@typescript-eslint/parser\": \"^7.3.0\",\n    \"@vitejs/plugin-react\": \"^4.2.0\",\n    \"@vitest/ui\": \"^3.0.8\",\n    \"babel-jest\": \"^29.4.1\",\n    \"babel-plugin-macros\": \"^3.1.0\",\n    \"eslint\": \"~8.57.0\",\n    \"eslint-config-prettier\": \"^9.0.0\",\n    \"eslint-plugin-import\": \"2.27.5\",\n    \"eslint-plugin-jsx-a11y\": \"6.7.1\",\n    \"eslint-plugin-playwright\": \"^0.15.3\",\n    \"eslint-plugin-react\": \"7.32.2\",\n    \"eslint-plugin-react-hooks\": \"4.6.0\",\n    \"jest\": \"^29.4.1\",\n    \"jest-environment-jsdom\": \"^29.4.1\",\n    \"jsdom\": \"~22.1.0\",\n    \"nx\": \"19.3.0\",\n    \"prettier\": \"^2.6.2\",\n    \"sass\": \"^1.55.0\",\n    \"ts-jest\": \"^29.1.0\",\n    \"ts-node\": \"10.9.1\",\n    \"typescript\": \"~5.4.2\",\n    \"vite\": \"^6.2.2\",\n    \"vite-plugin-dts\": \"^4.5.3\",\n    \"vitest\": \"^3.0.8\"\n  }\n}\n"
  },
  {
    "path": "packages/drawnix/.babelrc",
    "content": "{\n  \"presets\": [\n    [\n      \"@nx/react/babel\",\n      {\n        \"runtime\": \"automatic\",\n        \"useBuiltIns\": \"usage\"\n      }\n    ]\n  ],\n  \"plugins\": []\n}\n"
  },
  {
    "path": "packages/drawnix/.eslintrc.json",
    "content": "{\n  \"extends\": [\"plugin:@nx/react\", \"../../.eslintrc.json\"],\n  \"ignorePatterns\": [\"!**/*\"],\n  \"overrides\": [\n    {\n      \"files\": [\"*.ts\", \"*.tsx\", \"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.ts\", \"*.tsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/drawnix/README.md",
    "content": "# drawnix\n\nThis library was generated with [Nx](https://nx.dev).\n\n## Running unit tests\n\nRun `nx test drawnix` to execute the unit tests via [Vitest](https://vitest.dev/).\n"
  },
  {
    "path": "packages/drawnix/jest.config.ts",
    "content": "/* eslint-disable */\nexport default {\n  displayName: 'drawnix',\n  preset: '../../jest.preset.js',\n  transform: {\n    '^(?!.*\\\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',\n    '^.+\\\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }],\n  },\n  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],\n  coverageDirectory: '../../coverage/packages/drawnix',\n};\n"
  },
  {
    "path": "packages/drawnix/package.json",
    "content": "{\n  \"name\": \"@drawnix/drawnix\",\n  \"version\": \"0.4.0-2\",\n  \"main\": \"./index.js\",\n  \"types\": \"./index.d.ts\",\n  \"private\": false,\n  \"dependencies\": {\n    \"@floating-ui/react\": \"^0.26.24\",\n    \"mobile-detect\": \"^1.4.5\",\n    \"open-color\": \"^1.9.1\",\n    \"@plait-board/mermaid-to-drawnix\": \"^0.0.7\",\n    \"@plait-board/markdown-to-drawnix\": \"^0.0.8\",\n    \"laser-pen\": \"^1.0.1\"\n  },\n  \"exports\": {\n    \".\": {\n      \"import\": \"./index.mjs\",\n      \"require\": \"./index.js\",\n      \"types\": \"./index.d.ts\"\n    },\n    \"./index.css\": \"./index.css\"\n  }\n}\n\n"
  },
  {
    "path": "packages/drawnix/project.json",
    "content": "{\n  \"name\": \"drawnix\",\n  \"$schema\": \"../../node_modules/nx/schemas/project-schema.json\",\n  \"sourceRoot\": \"packages/drawnix/src\",\n  \"projectType\": \"library\",\n  \"tags\": [],\n  \"// targets\": \"to see all targets run: nx show project drawnix --web\",\n  \"targets\": {}\n}\n"
  },
  {
    "path": "packages/drawnix/src/components/arrow-mark-picker.tsx",
    "content": "import classNames from 'classnames';\nimport { Island } from './island';\nimport Stack from './stack';\nimport { ToolButton } from './tool-button';\nimport { LineIcon, ArrowIcon } from './icons';\nimport { useBoard } from '@plait-board/react-board';\nimport { ATTACHED_ELEMENT_CLASS_NAME } from '@plait/core';\nimport React from 'react';\nimport { PropertyTransforms } from '@plait/common';\nimport { ArrowLineHandle } from '@plait/draw';\nimport { useI18n } from '../i18n';\n\nexport type ArrowMarkerPickerProps = {\n  end: 'source' | 'target';\n  property: ArrowLineHandle;\n};\n\nexport const ArrowMarkerPicker: React.FC<ArrowMarkerPickerProps> = ({\n  end,\n  property,\n}) => {\n  const board = useBoard();\n  const { marker: currentMarker } = property;\n  const { t } = useI18n();\n\n  const setMarker = (marker: string) => {\n    PropertyTransforms.setProperty(board, {\n      [end]: {\n        ...property,\n        marker,\n      },\n    });\n  };\n\n  return (\n    <Island\n      padding={2}\n      className={classNames(\n        `${ATTACHED_ELEMENT_CLASS_NAME} ${\n          end === 'source' ? 'source-arrow-island' : ''\n        } `\n      )}\n    >\n      <Stack.Row gap={1}>\n        <ToolButton\n          className={classNames(`property-button`)}\n          visible={true}\n          icon={LineIcon}\n          type=\"button\"\n          title={t('line.none')}\n          aria-label={t('line.none')}\n          selected={currentMarker === 'none'}\n          onPointerUp={() => {\n            setMarker('none');\n          }}\n        ></ToolButton>\n        <ToolButton\n          className={classNames(`property-button`)}\n          visible={true}\n          icon={ArrowIcon}\n          type=\"button\"\n          title={t('line.arrow')}\n          aria-label={t('line.arrow')}\n          selected={currentMarker === 'arrow'}\n          onPointerUp={() => {\n            setMarker('arrow');\n          }}\n        ></ToolButton>\n      </Stack.Row>\n    </Island>\n  );\n};\n"
  },
  {
    "path": "packages/drawnix/src/components/arrow-picker.tsx",
    "content": "import classNames from 'classnames';\nimport { Island } from './island';\nimport Stack from './stack';\nimport { ToolButton } from './tool-button';\nimport { StraightArrowIcon, ElbowArrowIcon, CurveArrowIcon } from './icons';\nimport { useBoard } from '@plait-board/react-board';\nimport { Translations, useI18n } from '../i18n';\nimport { BoardTransforms , PlaitBoard } from '@plait/core';\nimport React from 'react';\nimport { BoardCreationMode, setCreationMode } from '@plait/common';\nimport { ArrowLineShape, DrawPointerType } from '@plait/draw';\n\nexport interface ArrowProps {\n  icon: React.ReactNode;\n  title: string;\n  pointer: ArrowLineShape;\n}\n\nexport const ARROWS: ArrowProps[] = [\n  {\n    icon: StraightArrowIcon,\n    title: 'toolbar.arrow.straight',\n    pointer: ArrowLineShape.straight,\n  },\n  {\n    icon: ElbowArrowIcon,\n    title: 'toolbar.arrow.elbow',\n    pointer: ArrowLineShape.elbow,\n  },\n  {\n    icon: CurveArrowIcon,\n    title: 'toolbar.arrow.curve',\n    pointer: ArrowLineShape.curve,\n  },\n];\n\nexport type ArrowPickerProps = {\n  onPointerUp: (pointer: DrawPointerType) => void;\n};\n\nexport const ArrowPicker: React.FC<ArrowPickerProps> = ({ onPointerUp }) => {\n  const board = useBoard();\n  const { t } = useI18n();\n  return (\n    <Island padding={1}>\n      <Stack.Row gap={1}>\n        {ARROWS.map((arrow, index) => {\n          return (\n            <ToolButton\n              key={index}\n              className={classNames({ fillable: false })}\n              type=\"icon\"\n              size={'small'}\n              visible={true}\n              selected={PlaitBoard.isPointer(board, arrow.pointer)}\n              icon={arrow.icon}\n              title={t(arrow.title as keyof Translations)}\n              aria-label={t(arrow.title as keyof Translations)}\n              onPointerDown={() => {\n                setCreationMode(board, BoardCreationMode.drawing);\n                BoardTransforms.updatePointerType(board, arrow.pointer);\n              }}\n              onPointerUp={() => {\n                onPointerUp(arrow.pointer);\n              }}\n            />\n          );\n        })}\n      </Stack.Row>\n    </Island>\n  );\n};\n"
  },
  {
    "path": "packages/drawnix/src/components/clean-confirm/clean-confirm.scss",
    "content": ".clean-confirm {\n  background: white;\n  border-radius: 8px;\n  padding: 20px;\n  width: 300px;\n\n  &__title {\n    font-size: 18px;\n    font-weight: 500;\n    margin: 0 0 8px;\n  }\n\n  &__description {\n    color: #666;\n    font-size: 14px;\n    margin: 0 0 20px;\n  }\n\n  &__actions {\n    display: flex;\n    justify-content: flex-end;\n    gap: 8px;\n  }\n\n  &__button {\n    padding: 8px 16px;\n    border-radius: 6px;\n    font-size: 14px;\n    cursor: pointer;\n    border: none;\n\n    &--cancel {\n      background: #f5f5f5;\n      color: #000;\n      \n      &:hover {\n        background: #e8e8e8;\n      }\n    }\n\n    &--ok {\n      background: white;\n      color: #ff4d4f;\n      border: 1px solid #ff4d4f;\n      \n      &:hover {\n        color: white;\n        background: #ff4d4f;\n      }\n    }\n  }\n}"
  },
  {
    "path": "packages/drawnix/src/components/clean-confirm/clean-confirm.tsx",
    "content": "import { Dialog, DialogContent } from '../dialog/dialog';\nimport { useDrawnix } from '../../hooks/use-drawnix';\nimport './clean-confirm.scss';\nimport { useBoard } from '@plait-board/react-board';\nimport { useI18n } from '../../i18n';\n\nexport const CleanConfirm = ({\n  container,\n}: {\n  container: HTMLElement | null;\n}) => {\n  const { appState, setAppState } = useDrawnix();\n  const { t } = useI18n();\n  const board = useBoard();\n  return (\n    <Dialog\n      open={appState.openCleanConfirm}\n      onOpenChange={(open) => {\n        setAppState({ ...appState, openCleanConfirm: open });\n      }}\n    >\n      <DialogContent className=\"clean-confirm\" container={container}>\n        <h2 className=\"clean-confirm__title\">{t('cleanConfirm.title')}</h2>\n        <p className=\"clean-confirm__description\">\n          {t('cleanConfirm.description')}\n        </p>\n        <div className=\"clean-confirm__actions\">\n          <button\n            className=\"clean-confirm__button clean-confirm__button--cancel\"\n            onClick={() => {\n              setAppState({ ...appState, openCleanConfirm: false });\n            }}\n          >\n            {t('cleanConfirm.cancel')}\n          </button>\n          <button\n            className=\"clean-confirm__button clean-confirm__button--ok\"\n            autoFocus\n            onClick={() => {\n              board.deleteFragment(board.children);\n              setAppState({ ...appState, openCleanConfirm: false });\n            }}\n          >\n            {t('cleanConfirm.ok')}\n          </button>\n        </div>\n      </DialogContent>\n    </Dialog>\n  );\n};\n"
  },
  {
    "path": "packages/drawnix/src/components/color-picker.scss",
    "content": "@import \"open-color/open-color.scss\";\n\n.color-select-item {\n  width: var(--default-button-size);\n  height: var(--default-button-size);\n  border-radius: 50%;\n  display: inline-flex;\n  justify-content: center;\n  align-items: center;\n  border: 1px solid var(--color-gray-30);\n  cursor: pointer;\n  padding: 0;\n  &.active {\n    border-color: var(--color-primary);\n    &.no-color {\n      .selected-icon {\n        background-color: $oc-white;\n      }\n    }\n  }\n  .selected-icon {\n    stroke: currentColor;\n    outline: none;\n    position: absolute;\n    width: var(--default-icon-size);\n    height: var(--default-icon-size);\n\n  }\n  &.no-color {\n    border: none;\n    .no-color-icon {\n      display: block;\n      width: var(-default-button-size);\n      height: var(-default-button-size);\n      color: rgba($oc-black, 0.4);\n    }\n  }\n}"
  },
  {
    "path": "packages/drawnix/src/components/color-picker.tsx",
    "content": "import { useState } from 'react';\nimport { Check, NoColorIcon } from './icons';\nimport Stack from '../components/stack';\nimport './color-picker.scss';\nimport { splitRows } from '../utils/common';\nimport {\n  hexAlphaToOpacity,\n  isDefaultStroke,\n  isNoColor,\n  removeHexAlpha,\n} from '../utils/color';\nimport React from 'react';\nimport { SizeSlider } from './size-slider';\nimport {\n  DEFAULT_COLOR,\n  isNullOrUndefined,\n  MERGING,\n  PlaitHistoryBoard,\n} from '@plait/core';\nimport {\n  CLASSIC_COLORS,\n  NO_COLOR,\n  TRANSPARENT,\n  WHITE,\n} from '../constants/color';\nimport { useBoard } from '@plait-board/react-board';\nimport { Translations, useI18n } from '../i18n';\n\nconst ROWS_CLASSIC_COLORS = splitRows(CLASSIC_COLORS, 4);\n\nexport type ColorPickerProps = {\n  onColorChange: (color: string) => void;\n  onOpacityChange: (opacity: number) => void;\n  currentColor?: string;\n};\n\nexport const ColorPicker = React.forwardRef((props: ColorPickerProps, ref) => {\n  const board = useBoard();\n  const { t } = useI18n();\n  const { currentColor, onColorChange, onOpacityChange } = props;\n  const [selectedColor, setSelectedColor] = useState(\n    (currentColor && removeHexAlpha(currentColor)) ||\n      ROWS_CLASSIC_COLORS[0][0].value\n  );\n  const [opacity, setOpacity] = useState(() => {\n    const _opacity = currentColor && hexAlphaToOpacity(currentColor);\n    return (!isNullOrUndefined(_opacity) ? _opacity : 100) as number;\n  });\n  return (\n    <Stack.Col gap={3}>\n        <SizeSlider\n          title={t('popupToolbar.opacity')}\n          step={5}\n          defaultValue={opacity}\n          onChange={(value) => {\n            setOpacity(value);\n            onOpacityChange(value);\n          }}\n          beforeStart={() => {\n            MERGING.set(board, true);\n            PlaitHistoryBoard.setSplittingOnce(board, true);\n          }}\n          afterEnd={() => {\n            MERGING.set(board, false);\n          }}\n          disabled={selectedColor === CLASSIC_COLORS[0]['value']}\n        ></SizeSlider>\n        <Stack.Col gap={2}>\n          {ROWS_CLASSIC_COLORS.map((colors, index) => (\n            <Stack.Row key={index} gap={2}>\n              {colors.map((color) => {\n                return (\n                  <button\n                    key={color.value}\n                    className={`color-select-item ${\n                      selectedColor === color.value ? 'active' : ''\n                    } ${isNoColor(color.value) ? 'no-color' : ''}`}\n                    style={{\n                      backgroundColor: isNoColor(color.value)\n                        ? TRANSPARENT\n                        : color.value,\n                      color: isDefaultStroke(color.value)\n                        ? WHITE\n                        : DEFAULT_COLOR,\n                    }}\n                    onClick={() => {\n                      setSelectedColor(color.value);\n                      if (color.value === NO_COLOR) {\n                        setOpacity(100);\n                      }\n                      onColorChange(color.value);\n                    }}\n                    title={t((color.name || 'color.unknown') as keyof Translations)}\n                    aria-label={t((color.name || 'color.unknown') as keyof Translations)}\n                  >\n                    {isNoColor(color.value) && NoColorIcon}\n                    {selectedColor === color.value && Check}\n                  </button>\n                );\n              })}\n            </Stack.Row>\n          ))}\n        </Stack.Col>\n      </Stack.Col>\n  );\n});\n"
  },
  {
    "path": "packages/drawnix/src/components/dialog/dialog.scss",
    "content": ".Dialog-overlay {\n    background: rgba(#121212, 0.2);\n    display: grid;\n    place-items: center;\n}\n\n.Dialog {\n    margin: 15px;\n    background-color: white;\n    padding: 15px;\n    border-radius: 4px;\n}"
  },
  {
    "path": "packages/drawnix/src/components/dialog/dialog.tsx",
    "content": "import * as React from 'react';\nimport {\n  useFloating,\n  useClick,\n  useDismiss,\n  useRole,\n  useInteractions,\n  useMergeRefs,\n  FloatingPortal,\n  FloatingFocusManager,\n  FloatingOverlay,\n  useId,\n} from '@floating-ui/react';\nimport './dialog.scss';\n\ninterface DialogOptions {\n  initialOpen?: boolean;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n}\n\nexport function useDialog({\n  initialOpen = false,\n  open: controlledOpen,\n  onOpenChange: setControlledOpen,\n}: DialogOptions = {}) {\n  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(initialOpen);\n  const [labelId, setLabelId] = React.useState<string | undefined>();\n  const [descriptionId, setDescriptionId] = React.useState<\n    string | undefined\n  >();\n\n  const open = controlledOpen ?? uncontrolledOpen;\n  const setOpen = setControlledOpen ?? setUncontrolledOpen;\n\n  const data = useFloating({\n    open,\n    onOpenChange: setOpen,\n  });\n\n  const context = data.context;\n\n  const click = useClick(context, {\n    enabled: controlledOpen == null,\n  });\n  const dismiss = useDismiss(context, { outsidePressEvent: 'mousedown' });\n  const role = useRole(context);\n\n  const interactions = useInteractions([click, dismiss, role]);\n\n  return React.useMemo(\n    () => ({\n      open,\n      setOpen,\n      ...interactions,\n      ...data,\n      labelId,\n      descriptionId,\n      setLabelId,\n      setDescriptionId,\n    }),\n    [open, setOpen, interactions, data, labelId, descriptionId]\n  );\n}\n\ntype ContextType =\n  | (ReturnType<typeof useDialog> & {\n      setLabelId: React.Dispatch<React.SetStateAction<string | undefined>>;\n      setDescriptionId: React.Dispatch<\n        React.SetStateAction<string | undefined>\n      >;\n    })\n  | null;\n\nconst DialogContext = React.createContext<ContextType>(null);\n\nexport const useDialogContext = () => {\n  const context = React.useContext(DialogContext);\n\n  if (context == null) {\n    throw new Error('Dialog components must be wrapped in <Dialog />');\n  }\n\n  return context;\n};\n\nexport function Dialog({\n  children,\n  ...options\n}: {\n  children: React.ReactNode;\n} & DialogOptions) {\n  const dialog = useDialog(options);\n  return (\n    <DialogContext.Provider value={dialog}>{children}</DialogContext.Provider>\n  );\n}\n\ninterface DialogTriggerProps {\n  children: React.ReactNode;\n  asChild?: boolean;\n}\n\nexport const DialogTrigger = React.forwardRef<\n  HTMLElement,\n  React.HTMLProps<HTMLElement> & DialogTriggerProps\n>(function DialogTrigger({ children, asChild = false, ...props }, propRef) {\n  const context = useDialogContext();\n  const childrenRef = (children as any).ref;\n  const ref = useMergeRefs([context.refs.setReference, propRef, childrenRef]);\n\n  // `asChild` allows the user to pass any element as the anchor\n  if (asChild && React.isValidElement(children)) {\n    return React.cloneElement(\n      children,\n      context.getReferenceProps({\n        ref,\n        ...props,\n        ...children.props,\n        'data-state': context.open ? 'open' : 'closed',\n      })\n    );\n  }\n\n  return (\n    <button\n      ref={ref}\n      // The user can style the trigger based on the state\n      data-state={context.open ? 'open' : 'closed'}\n      {...context.getReferenceProps(props)}\n    >\n      {children}\n    </button>\n  );\n});\n\nexport const DialogContent = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLProps<HTMLDivElement> & { container?: HTMLElement | null }\n>(function DialogContent(props, propRef) {\n  const { context: floatingContext, ...context } = useDialogContext();\n  const ref = useMergeRefs([context.refs.setFloating, propRef]);\n\n  if (!floatingContext.open) return null;\n\n  return (\n    <FloatingPortal root={props.container}>\n      <FloatingOverlay className=\"Dialog-overlay\" lockScroll>\n        <FloatingFocusManager context={floatingContext}>\n          <div\n            ref={ref}\n            aria-labelledby={context.labelId}\n            aria-describedby={context.descriptionId}\n            {...context.getFloatingProps(props)}\n          >\n            {props.children}\n          </div>\n        </FloatingFocusManager>\n      </FloatingOverlay>\n    </FloatingPortal>\n  );\n});\n\nexport const DialogHeading = React.forwardRef<\n  HTMLHeadingElement,\n  React.HTMLProps<HTMLHeadingElement>\n>(function DialogHeading({ children, ...props }, ref) {\n  const { setLabelId } = useDialogContext();\n  const id = useId();\n\n  // Only sets `aria-labelledby` on the Dialog root element\n  // if this component is mounted inside it.\n  React.useLayoutEffect(() => {\n    setLabelId(id);\n    return () => setLabelId(undefined);\n  }, [id, setLabelId]);\n\n  return (\n    <h2 {...props} ref={ref} id={id}>\n      {children}\n    </h2>\n  );\n});\n\nexport const DialogDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLProps<HTMLParagraphElement>\n>(function DialogDescription({ children, ...props }, ref) {\n  const { setDescriptionId } = useDialogContext();\n  const id = useId();\n\n  // Only sets `aria-describedby` on the Dialog root element\n  // if this component is mounted inside it.\n  React.useLayoutEffect(() => {\n    setDescriptionId(id);\n    return () => setDescriptionId(undefined);\n  }, [id, setDescriptionId]);\n\n  return (\n    <p {...props} ref={ref} id={id}>\n      {children}\n    </p>\n  );\n});\n\nexport const DialogClose = React.forwardRef<\n  HTMLButtonElement,\n  React.ButtonHTMLAttributes<HTMLButtonElement>\n>(function DialogClose(props, ref) {\n  const { setOpen } = useDialogContext();\n  return (\n    <button type=\"button\" {...props} ref={ref} onClick={() => setOpen(false)} />\n  );\n});\n"
  },
  {
    "path": "packages/drawnix/src/components/icons.tsx",
    "content": "import React from 'react';\n\nexport const createIcon = (svg: React.ReactNode) => {\n  return svg;\n};\n\nexport const HandIcon = createIcon(\n  <svg viewBox=\"0 0 16 16\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g id=\"Hand\" stroke=\"none\" fill=\"currentColor\">\n      <path d=\"M8.44583468,0.500225887 C9.07406934,0.510185679 9.54739531,0.839591366 9.86192311,1.34305279 C9.89696656,1.39914649 9.92878401,1.45492964 9.9576026,1.50991157 L9.9576026,1.50991157 L10.0210033,1.64201027 L10.061978,1.62350755 C10.1972891,1.56834247 10.3444107,1.53218464 10.5027907,1.51755353 L10.5027907,1.51755353 L10.6649031,1.51019133 C11.4883708,1.51019133 12.0208782,1.99343346 12.3023042,2.66393278 C12.3903714,2.87392911 12.4344191,3.10047818 12.4339446,3.3257952 L12.4339446,3.3257952 L12.4360033,3.80501027 L12.5160535,3.78341501 C12.6124478,3.76124046 12.7138812,3.74739854 12.820201,3.74250274 L12.820201,3.74250274 L12.9833264,3.74194533 C13.6121166,3.7657478 14.0645887,4.0801724 14.3087062,4.56112689 C14.4521117,4.8436609 14.4987984,5.11349437 14.4999262,5.33449618 L14.4999262,5.33449618 L14.3922653,12.049414 C14.3784752,12.909177 14.0717787,13.7360948 13.5212406,14.3825228 C13.4055676,14.5183496 13.2843697,14.643961 13.1582361,14.7596335 C12.4634771,15.3967716 11.755103,15.6538706 11.1897396,15.7000055 L11.1897396,15.7000055 L7.4723083,15.6798158 C7.14276373,15.634268 6.81580098,15.5154267 6.49455235,15.3472501 C6.25643701,15.2225944 6.06881706,15.0975452 5.88705731,14.9494308 L5.88705731,14.9494308 L2.55198782,11.500873 C2.39559475,11.3769079 2.17626793,11.1748532 1.9548636,10.9139403 C1.57867502,10.4706225 1.33501976,10.0139923 1.30330257,9.52833025 C1.28093191,9.18578476 1.37200912,8.85641102 1.5826788,8.56872564 C1.82538833,8.23725279 2.12881965,8.02107162 2.47470569,7.92957033 C2.95807982,7.80169771 3.42705723,7.92468989 3.86509644,8.18731167 C4.04431391,8.29475961 4.1816109,8.40304483 4.26225571,8.47866867 L4.26225571,8.47866867 L4.61400328,8.79701027 L4.57247249,3.59275349 L4.57628524,3.46204923 C4.5897691,3.23444442 4.64087578,2.95701848 4.75937106,2.66961597 C5.01017272,2.06131302 5.49670227,1.64692543 6.21363856,1.60818786 C6.44223508,1.59583681 6.65042099,1.62176802 6.83696985,1.68057551 L6.83696985,1.68057551 L6.86400328,1.69001027 C6.88501862,1.63593052 6.90764242,1.58175442 6.9331867,1.52672633 L6.9331867,1.52672633 L7.01883595,1.35955614 C7.31549194,0.832047939 7.79476072,0.48993549 8.44583468,0.500225887 Z M8.42684173,1.70001476 C8.26825412,1.69756905 8.16339456,1.77242008 8.06478367,1.94776814 C8.03967773,1.99241107 8.01831703,2.03811495 8.00083464,2.07855067 L8.00083464,2.07855067 L7.94879157,2.2035905 L7.94354455,2.20731401 L7.943,3.161 L7.97170661,3.16123746 L7.97170661,7.60991883 L6.77170661,7.60991883 L6.771,3.338 L6.74362358,3.33880359 C6.74284189,3.29064626 6.73014163,3.20282206 6.7002616,3.11094408 L6.66446012,3.01903385 C6.58982025,2.85766739 6.49843292,2.79455071 6.27838133,2.80644008 C6.07001018,2.81769881 5.95642108,2.91444507 5.86877664,3.12702089 C5.79792279,3.29887224 5.77228127,3.48655908 5.77246879,3.58977183 L5.77246879,3.58977183 L5.83613619,11.5252021 L3.41863956,9.33477657 L3.31637296,9.25979571 L3.24805011,9.21651224 C3.06096922,9.10434987 2.89279975,9.06024641 2.78159879,9.0896637 C2.71007735,9.10858411 2.63607367,9.1613084 2.55086305,9.27768211 C2.51020424,9.33320478 2.49638061,9.38319687 2.50075171,9.4501283 C2.51206889,9.62341997 2.64503022,9.87260054 2.86983366,10.1375191 C3.03268834,10.3294345 3.19762053,10.4813781 3.35554956,10.6131022 L3.35554956,10.6131022 L6.68454317,14.0569073 C6.71106575,14.0773808 6.74806086,14.1037158 6.79369091,14.1335929 L6.79369091,14.1335929 L6.95464838,14.2315311 L7.05111031,14.2841211 C7.25978123,14.3933622 7.46253523,14.4670573 7.55685495,14.4854708 L7.55685495,14.4854708 L11.1407985,14.5022108 C11.1503576,14.5013899 11.1627905,14.4997539 11.1779002,14.4971772 L11.1779002,14.4971772 L11.2991076,14.4694224 C11.3491682,14.4557375 11.4083624,14.437284 11.4751158,14.4130563 C11.769383,14.3062543 12.066676,14.1324596 12.3471758,13.8752234 C12.4371203,13.7927386 12.5240597,13.7026333 12.607654,13.6044743 C12.9760464,13.1719172 13.183059,12.6137678 13.1924195,12.030173 L13.1924195,12.030173 L13.3000132,5.32832551 C13.2997939,5.29016685 13.2826117,5.19085946 13.2386527,5.10425262 C13.1843838,4.99733326 13.1129774,4.94771265 12.9379578,4.94108739 C12.6814739,4.93138871 12.534132,5.11189595 12.4756792,5.39480062 L12.4768718,7.52734922 L11.2768718,7.52734922 L11.276,5.688 L11.2462883,5.6883208 L11.2339541,3.32771285 C11.2341,3.2560396 11.2209054,3.18817621 11.1957482,3.12818892 C11.0820579,2.85732094 10.9199288,2.71019133 10.6649031,2.71019133 C10.456829,2.71019133 10.3197487,2.87378067 10.2524297,3.11264939 L10.2530225,7.512783 L9.05302254,7.512783 L9.053,3.288 L9.01554331,3.28724203 L8.98800328,2.29901027 L8.9629175,2.22263368 C8.94515567,2.17417174 8.92167756,2.11937748 8.8924232,2.06330056 L8.8924232,2.06330056 L8.84420197,1.9788544 C8.72758855,1.79219249 8.59915015,1.70280728 8.42684173,1.70001476 Z\"></path>\n    </g>\n  </svg>\n);\n\nexport const SelectionIcon = createIcon(\n  <svg viewBox=\"0 0 16 16\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g id=\"selection\" stroke=\"none\" fill=\"currentColor\">\n      <path d=\"M1.38232686,2.38218266 L5.4143451,14.2246629 L5.45540179,14.3136477 C5.6738376,14.7029541 6.25143564,14.7273637 6.49230627,14.3232393 L8.11486037,11.5990854 L10.8833927,14.4351257 C11.1162256,14.673686 11.4988798,14.6767204 11.7354668,14.4418826 L14.1933351,12.0021862 L14.263123,11.9192708 C14.4260847,11.6858139 14.4039042,11.3621027 14.1959502,11.1531274 L11.3598604,8.30408543 L14.0003903,6.44278167 C14.4042341,6.15799031 14.3099422,5.5344405 13.8399491,5.38178897 L2.13023795,1.60291226 C1.65322163,1.44797961 1.20794286,1.91192855 1.38232686,2.38218266 Z M2.93689198,3.12556703 L12.3288604,6.15308543 L10.0883903,7.73315528 L10.0121747,7.79676991 C9.78025886,8.02517222 9.77056424,8.40723513 10.0088753,8.64671667 L12.9218604,11.5730854 L11.3198604,13.1630854 L8.42938714,10.2026992 L8.35682877,10.1391916 C8.07802132,9.93187508 7.66955488,10.0042813 7.48460396,10.3145856 L6.10286037,12.6310854 L2.93689198,3.12556703 Z\"></path>\n    </g>\n  </svg>\n);\n\nexport const MindIcon = createIcon(\n  <svg viewBox=\"0 0 16 16\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g id=\"Mind\" stroke=\"none\" fill=\"currentColor\">\n      <path d=\"M14.5,1.5 C15.3284271,1.5 16,2.17157288 16,3 L16,4.5 C16,5.32842712 15.3284271,6 14.5,6 L10.5,6 C9.70541385,6 9.05512881,5.38217354 9.00332687,4.60070262 L7.75,4.6 C6.70187486,4.6 5.75693372,5.0417832 5.09122946,5.7492967 L5.5,5.75 C6.32842712,5.75 7,6.42157288 7,7.25 L7,8.75 C7,9.57842712 6.32842712,10.25 5.5,10.25 L4.69703093,10.2512226 C5.3493111,11.2442937 6.47308134,11.9 7.75,11.9 L9.004,11.9 L9.00686658,11.85554 C9.07955132,11.0948881 9.72030388,10.5 10.5,10.5 L14.5,10.5 C15.3284271,10.5 16,11.1715729 16,12 L16,13.5 C16,14.3284271 15.3284271,15 14.5,15 L10.5,15 C9.67157288,15 9,14.3284271 9,13.5 L9,13.1 L7.75,13.1 C5.78479628,13.1 4.09258608,11.9311758 3.33061658,10.2507745 L1.5,10.25 C0.671572875,10.25 0,9.57842712 0,8.75 L0,7.25 C0,6.42157288 0.671572875,5.75 1.5,5.75 L3.5932906,5.74973863 C4.44206161,4.34167555 5.98606075,3.4 7.75,3.4 L9,3.4 L9,3 C9,2.17157288 9.67157288,1.5 10.5,1.5 L14.5,1.5 Z M14.5,11.7 L10.5,11.7 C10.3343146,11.7 10.2,11.8343146 10.2,12 L10.2,13.5 C10.2,13.6656854 10.3343146,13.8 10.5,13.8 L14.5,13.8 C14.6656854,13.8 14.8,13.6656854 14.8,13.5 L14.8,12 C14.8,11.8343146 14.6656854,11.7 14.5,11.7 Z M5.5,6.95 L1.5,6.95 C1.33431458,6.95 1.2,7.08431458 1.2,7.25 L1.2,8.75 C1.2,8.91568542 1.33431458,9.05 1.5,9.05 L5.5,9.05 C5.66568542,9.05 5.8,8.91568542 5.8,8.75 L5.8,7.25 C5.8,7.08431458 5.66568542,6.95 5.5,6.95 Z M14.5,2.7 L10.5,2.7 C10.3343146,2.7 10.2,2.83431458 10.2,3 L10.2,4.5 C10.2,4.66568542 10.3343146,4.8 10.5,4.8 L14.5,4.8 C14.6656854,4.8 14.8,4.66568542 14.8,4.5 L14.8,3 C14.8,2.83431458 14.6656854,2.7 14.5,2.7 Z\"></path>\n    </g>\n  </svg>\n);\n\nexport const ShapeIcon = createIcon(\n  <svg viewBox=\"0 0 16 16\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g id=\"geometry\" stroke=\"none\" fill=\"currentColor\">\n      <path d=\"M9.3,6.7 L1.7,6.7 L1.7,14.3 L9.3,14.3 L9.3,6.7 Z M10.5,9.8 C12.8748244,9.8 14.8,7.87482442 14.8,5.5 C14.8,3.12517558 12.8748244,1.2 10.5,1.2 C8.12517558,1.2 6.2,3.12517558 6.2,5.5 L9.5,5.5 C10.0522847,5.5 10.5,5.94771525 10.5,6.5 L10.5,9.8 Z M10.5,14.5 C10.5,15.0522847 10.0522847,15.5 9.5,15.5 L1.5,15.5 C0.94771525,15.5 0.5,15.0522847 0.5,14.5 L0.5,6.5 C0.5,5.94771525 0.94771525,5.5 1.5,5.5 L5,5.5 C5,2.46243388 7.46243388,0 10.5,0 C13.5375661,0 16,2.46243388 16,5.5 C16,8.53756612 13.5375661,11 10.5,11 L10.5,14.5 Z\"></path>\n    </g>\n  </svg>\n);\n\nexport const TextIcon = createIcon(\n  <svg viewBox=\"0 0 16 16\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g id=\"font\" stroke=\"none\" fill=\"currentColor\">\n      <path d=\"M4.75,14.5069828 C4.41862915,14.5069828 4.15,14.2383536 4.15,13.9069828 C4.15,13.5756119 4.41862915,13.3069828 4.75,13.3069828 L7.3993606,13.306 L7.3993606,2.7 L2.7113606,2.7 L2.7113606,4.10415313 C2.7113606,4.40238689 2.49377099,4.64979988 2.20868371,4.69630014 L2.1113606,4.70415313 C1.77998975,4.70415313 1.5113606,4.43552397 1.5113606,4.10415313 L1.5113606,2.1 C1.5113606,1.76862915 1.77998975,1.5 2.1113606,1.5 L13.8810378,1.5 C14.2124087,1.5 14.4810378,1.76862915 14.4810378,2.1 L14.4810378,4.10415313 C14.4810378,4.43552397 14.2124087,4.70415313 13.8810378,4.70415313 C13.549667,4.70415313 13.2810378,4.43552397 13.2810378,4.10415313 L13.2810378,2.7 L8.5993606,2.7 L8.5993606,13.306 L11.25,13.3069828 C11.5813708,13.3069828 11.85,13.5756119 11.85,13.9069828 C11.85,14.2383536 11.5813708,14.5069828 11.25,14.5069828 L4.75,14.5069828 Z\"></path>\n    </g>\n  </svg>\n);\n\nexport const EraseIcon = createIcon(\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth=\"2\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n  >\n    <path stroke=\"none\" d=\"M0 0h24v24H0z\" />\n    <path d=\"M19 20h-10.5l-4.21 -4.3a1 1 0 0 1 0 -1.41l10 -10a1 1 0 0 1 1.41 0l5 5a1 1 0 0 1 0 1.41l-9.2 9.3\" />\n    <path d=\"M18 13.3l-6.3 -6.3\" />\n  </svg>\n);\n\nexport const StraightArrowLineIcon = createIcon(\n  <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" version=\"1.1\">\n    <g id=\"straight-line\" stroke=\"none\" fill=\"currentColor\">\n      <path\n        d=\"M8.55595221,-1.5261864 C8.88741773,-1.5261864 9.15621426,-1.25765205 9.15653772,-0.926186684 L9.16739175,10.3828136 L10.9946787,10.3836977 C11.2708211,10.3836977 11.4946787,10.6075553 11.4946787,10.8836977 C11.4946787,10.9607525 11.4768694,11.0367648 11.4426413,11.1058002 L8.8378495,16.3594519 C8.7642512,16.5078936 8.58425218,16.5685662 8.43581043,16.4949679 C8.37895485,16.4667786 8.33250284,16.4212859 8.30313336,16.3650308 L5.56226325,11.1150985 C5.43446412,10.8703088 5.52930372,10.5682659 5.77409341,10.4404667 C5.84552557,10.4031736 5.92491301,10.3836977 6.0054942,10.3836977 L7.96739175,10.3828136 L7.95653772,-0.926186684 C7.95621467,-1.25723416 8.22431979,-1.52586306 8.55536727,-1.52618611 Z\"\n        id=\"\"\n        transform=\"translate(8.500035, 7.500035) rotate(-135.000000) translate(-8.500035, -7.500035) \"\n      />\n    </g>\n  </svg>\n);\n\nexport const RectangleIcon = createIcon(\n  <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\">\n    <path\n      d=\"M3 3h18v18H3z\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      fill=\"none\"\n    ></path>\n  </svg>\n);\n\nexport const TerminalIcon = createIcon(\n  <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\">\n    <g id=\"terminal\" stroke=\"none\" fill=\"currentColor\">\n      <path d=\"M11,3 C13.7614237,3 16,5.23857625 16,8 C16,10.7614237 13.7614237,13 11,13 L5,13 C2.23857625,13 0,10.7614237 0,8 C0,5.23857625 2.23857625,3 5,3 L11,3 Z M11,4.2 L5,4.2 C2.90131795,4.2 1.2,5.90131795 1.2,8 C1.2,10.0330982 2.79664702,11.6932796 4.8044525,11.7950555 L5,11.8 L11,11.8 C13.098682,11.8 14.8,10.098682 14.8,8 C14.8,5.96690176 13.203353,4.30672042 11.1955475,4.20494454 L11,4.2 Z\" />\n    </g>\n  </svg>\n);\n\nexport const EllipseIcon = createIcon(\n  <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" version=\"1.1\">\n    <g id=\"ellipse\" stroke=\"none\" fill=\"currentColor\">\n      <path d=\"M8,1 C11.8659932,1 15,4.13400675 15,8 C15,11.8659932 11.8659932,15 8,15 C4.13400675,15 1,11.8659932 1,8 C1,4.13400675 4.13400675,1 8,1 Z M8,2.2 C4.79674845,2.2 2.2,4.79674845 2.2,8 C2.2,11.2032515 4.79674845,13.8 8,13.8 C11.2032515,13.8 13.8,11.2032515 13.8,8 C13.8,4.79674845 11.2032515,2.2 8,2.2 Z\" />\n    </g>\n  </svg>\n);\n\nexport const TriangleIcon = createIcon(\n  <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" version=\"1.1\">\n    <g id=\"triangle\" stroke=\"none\" fill=\"currentColor\">\n      <path d=\"M8.23125547,1.21366135 C8.3114266,1.25857939 8.37766784,1.32472334 8.42270367,1.40482837 L15.6471754,14.2549655 C15.7825042,14.4956743 15.6970768,14.800513 15.456368,14.9358418 C15.3815505,14.977905 15.2971646,15 15.2113335,15 L0.787227066,15 C0.511084691,15 0.287227066,14.7761424 0.287227066,14.5 C0.287227066,14.414418 0.309194147,14.3302684 0.351025556,14.2556064 L7.55066033,1.40546924 C7.6856352,1.1645618 7.99034802,1.07868648 8.23125547,1.21366135 Z M7.98695902,3.07926294 L1.98095902,13.7992629 L14.014959,13.7992629 L7.98695902,3.07926294 Z\" />\n    </g>\n  </svg>\n);\n\nexport const DiamondIcon = createIcon(\n  <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" version=\"1.1\">\n    <g stroke=\"none\" fill=\"currentColor\">\n      <path\n        d=\"M13.7636471,2.6449804 C13.7716713,2.69552516 13.7718878,2.74700226 13.7642892,2.79761274 L12.3875778,11.9671885 C12.3550099,12.1841069 12.184864,12.3544698 11.9679874,12.3873141 L2.78433018,13.7781116 C2.511301,13.8194599 2.25644773,13.6316454 2.21509947,13.3586162 C2.20737253,13.307594 2.20759072,13.2556831 2.21574631,13.2047277 L3.67471119,4.08923146 C3.70888725,3.87570215 3.87646006,3.70834166 4.09003253,3.67443635 L13.1914362,2.22955927 C13.4641633,2.18626298 13.7203508,2.37225335 13.7636471,2.6449804 Z M12.4355704,3.5645263 L4.77957044,4.7795263 L3.55157044,12.4485263 L11.2775704,11.2775263 L12.4355704,3.5645263 Z\"\n        transform=\"translate(7.989647, 8.003560) rotate(-315.000000) translate(-7.989647, -8.003560) \"\n      />\n    </g>\n  </svg>\n);\n\nexport const ParallelogramIcon = createIcon(\n  <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" version=\"1.1\">\n    <g stroke=\"none\" fill=\"currentColor\">\n      <path d=\"M15.3062871,3.5 C15.5824294,3.5 15.8062871,3.72385763 15.8062871,4 C15.8062871,4.05374105 15.7976231,4.10713065 15.7806287,4.15811388 L13.113962,12.1581139 C13.045905,12.362285 12.8548356,12.5 12.6396204,12.5 L0.693712943,12.5 C0.417570568,12.5 0.193712943,12.2761424 0.193712943,12 C0.193712943,11.946259 0.202376883,11.8928694 0.219371294,11.8418861 L2.88603796,3.84188612 C2.95409498,3.63771505 3.14516441,3.5 3.36037961,3.5 L15.3062871,3.5 Z M14.335,4.7 L3.864,4.7 L1.664,11.3 L12.134,11.3 L14.335,4.7 Z\" />\n    </g>\n  </svg>\n);\n\nexport const RoundRectangleIcon = createIcon(\n  <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" version=\"1.1\">\n    <g stroke=\"none\" fill=\"currentColor\">\n      <path d=\"M11,3 C13.7614237,3 16,5.23857625 16,8 C16,10.7614237 13.7614237,13 11,13 L5,13 C2.23857625,13 0,10.7614237 0,8 C0,5.23857625 2.23857625,3 5,3 L11,3 Z M11,4.2 L5,4.2 C2.90131795,4.2 1.2,5.90131795 1.2,8 C1.2,10.0330982 2.79664702,11.6932796 4.8044525,11.7950555 L5,11.8 L11,11.8 C13.098682,11.8 14.8,10.098682 14.8,8 C14.8,5.96690176 13.203353,4.30672042 11.1955475,4.20494454 L11,4.2 Z\" />\n    </g>\n  </svg>\n);\n\nexport const StraightArrowIcon = createIcon(\n  <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" version=\"1.1\">\n    <g stroke=\"none\" fill=\"currentColor\">\n      <path\n        d=\"M8.55595221,-1.5261864 C8.88741773,-1.5261864 9.15621426,-1.25765205 9.15653772,-0.926186684 L9.16739175,10.3828136 L10.9946787,10.3836977 C11.2708211,10.3836977 11.4946787,10.6075553 11.4946787,10.8836977 C11.4946787,10.9607525 11.4768694,11.0367648 11.4426413,11.1058002 L8.8378495,16.3594519 C8.7642512,16.5078936 8.58425218,16.5685662 8.43581043,16.4949679 C8.37895485,16.4667786 8.33250284,16.4212859 8.30313336,16.3650308 L5.56226325,11.1150985 C5.43446412,10.8703088 5.52930372,10.5682659 5.77409341,10.4404667 C5.84552557,10.4031736 5.92491301,10.3836977 6.0054942,10.3836977 L7.96739175,10.3828136 L7.95653772,-0.926186684 C7.95621467,-1.25723416 8.22431979,-1.52586306 8.55536727,-1.52618611 Z\"\n        transform=\"translate(8.500035, 7.500035) rotate(-135.000000) translate(-8.500035, -7.500035) \"\n      />\n    </g>\n  </svg>\n);\n\nexport const ElbowArrowIcon = createIcon(\n  <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" version=\"1.1\">\n    <g stroke=\"none\" fill=\"currentColor\">\n      <path d=\"M10.0153197,2.75391207 C10.0923746,2.75391207 10.1683869,2.77172133 10.2374222,2.80594949 L15.4910739,5.41074126 C15.6395156,5.48433956 15.7001882,5.66433859 15.6265899,5.81278033 C15.5984006,5.86963592 15.5529079,5.91608792 15.4966529,5.9454574 L10.2467205,8.68632752 C10.0019308,8.81412664 9.69988791,8.71928704 9.57208878,8.47449735 C9.53479568,8.40306519 9.51531974,8.32367776 9.51531974,8.24309656 L9.51458753,6.62591207 L6.16858753,6.62651279 L6.16914066,12.0061269 C6.16914066,12.3043606 5.95155104,12.5517736 5.66646377,12.5982739 L5.56914066,12.6061269 L0.534587532,12.6061269 C0.203216682,12.6061269 -0.0654124678,12.3374977 -0.0654124678,12.0061269 C-0.0654124678,11.674756 0.203216682,11.4061269 0.534587532,11.4061269 L4.96858753,11.4055128 L4.96914066,6.02651279 C4.96914066,5.72827903 5.18673027,5.48086604 5.47181754,5.43436578 L5.56914066,5.42651279 L9.51458753,5.42591207 L9.51531974,3.25391207 C9.51531974,2.9777697 9.73917736,2.75391207 10.0153197,2.75391207 Z\" />\n    </g>\n  </svg>\n);\n\nexport const CurveArrowIcon = createIcon(\n  <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" version=\"1.1\">\n    <g stroke=\"none\" fill=\"currentColor\">\n      <path d=\"M10.0153197,2.75391207 C10.0923746,2.75391207 10.1683869,2.77172133 10.2374222,2.80594949 L15.4910739,5.41074126 C15.6395156,5.48433956 15.7001882,5.66433859 15.6265899,5.81278033 C15.5984006,5.86963592 15.5529079,5.91608792 15.4966529,5.9454574 L10.2467205,8.68632752 C10.0019308,8.81412664 9.69988791,8.71928704 9.57208878,8.47449735 C9.53479568,8.40306519 9.51531974,8.32367776 9.51531974,8.24309656 L9.51423005,6.39035523 C5.97984781,6.85936966 3.21691607,9.08498364 1.18879108,13.1285821 C1.04022695,13.4247836 0.679673152,13.5444674 0.383471635,13.3959033 C0.0872701176,13.2473391 -0.0324136308,12.8867853 0.116150501,12.5905838 C2.34388813,8.14900524 5.48945543,5.65776043 9.51468497,5.18078677 L9.51531974,3.25391207 C9.51531974,2.9777697 9.73917736,2.75391207 10.0153197,2.75391207 Z\" />\n    </g>\n  </svg>\n);\n\nexport const MenuIcon = createIcon(\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth=\"2\"\n  >\n    <g strokeWidth=\"1.5\">\n      <path stroke=\"none\" d=\"M0 0h24v24H0z\"></path>\n      <line x1=\"4\" y1=\"6\" x2=\"20\" y2=\"6\"></line>\n      <line x1=\"4\" y1=\"12\" x2=\"20\" y2=\"12\"></line>\n      <line x1=\"4\" y1=\"18\" x2=\"20\" y2=\"18\"></line>\n    </g>\n  </svg>\n);\n\nexport const GithubIcon = createIcon(\n  <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\">\n    <path\n      stroke=\"currentColor\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      fill=\"none\"\n      d=\"M7.5 15.833c-3.583 1.167-3.583-2.083-5-2.5m10 4.167v-2.917c0-.833.083-1.166-.417-1.666 2.334-.25 4.584-1.167 4.584-5a3.833 3.833 0 0 0-1.084-2.667 3.5 3.5 0 0 0-.083-2.667s-.917-.25-2.917 1.084a10.25 10.25 0 0 0-5.166 0C5.417 2.333 4.5 2.583 4.5 2.583a3.5 3.5 0 0 0-.083 2.667 3.833 3.833 0 0 0-1.084 2.667c0 3.833 2.25 4.75 4.584 5-.5.5-.5 1-.417 1.666V17.5\"\n      strokeWidth=\"1.25\"\n    ></path>\n  </svg>\n);\n\nexport const ExportImageIcon = createIcon(\n  <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\">\n    <g\n      strokeWidth=\"1.25\"\n      stroke=\"currentColor\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      fill=\"none\"\n    >\n      <path stroke=\"none\" d=\"M0 0h24v24H0z\"></path>\n      <path d=\"M15 8h.01\"></path>\n      <path d=\"M12 20h-5a3 3 0 0 1 -3 -3v-10a3 3 0 0 1 3 -3h10a3 3 0 0 1 3 3v5\"></path>\n      <path d=\"M4 15l4 -4c.928 -.893 2.072 -.893 3 0l4 4\"></path>\n      <path d=\"M14 14l1 -1c.617 -.593 1.328 -.793 2.009 -.598\"></path>\n      <path d=\"M19 16v6\"></path>\n      <path d=\"M22 19l-3 3l-3 -3\"></path>\n    </g>\n  </svg>\n);\n\nexport const ZoomOutIcon = createIcon(\n  <svg viewBox=\"0 0 16 16\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g id=\"zoom-out\" stroke=\"none\" fill=\"currentColor\" strokeWidth=\"1\">\n      <path\n        fillRule=\"nonzero\"\n        d=\"M6.85,2.73225886e-13 C10.6331505,2.73225886e-13 13.7,3.06684946 13.7,6.85 C13.7,8.54194045 13.0865836,10.0906098 12.0700142,11.2857448 L15.4201976,14.5717081 C15.6567367,14.8037768 15.6603607,15.1836585 15.4282919,15.4201976 C15.1962232,15.6567367 14.8163415,15.6603607 14.5798024,15.4282919 L14.5798024,15.4282919 L11.2163456,12.128262 C10.0309427,13.1099691 8.50937591,13.7 6.85,13.7 C3.06684946,13.7 4.58522109e-14,10.6331505 4.58522109e-14,6.85 C4.58522109e-14,3.06684946 3.06684946,2.73225886e-13 6.85,2.73225886e-13 Z M6.85,1.2 C3.72959116,1.2 1.2,3.72959116 1.2,6.85 C1.2,9.97040884 3.72959116,12.5 6.85,12.5 C8.31753357,12.5 9.65438791,11.9404957 10.6588859,11.0231643 C10.6855412,10.9625408 10.7245275,10.9050898 10.7743982,10.8542584 C10.8288931,10.7987137 10.8915387,10.7560124 10.9585649,10.7261903 C11.9144009,9.71595758 12.5,8.35136579 12.5,6.85 C12.5,3.72959116 9.97040884,1.2 6.85,1.2 Z M4.6,6.2 L9.12944565,6.2 C9.4608165,6.2 9.72944565,6.46862915 9.72944565,6.8 C9.72944565,7.09823376 9.51185604,7.34564675 9.22676876,7.39214701 L9.12944565,7.4 L4.6,7.4 C4.26862915,7.4 4,7.13137085 4,6.8 C4,6.50176624 4.21758961,6.25435325 4.50267688,6.20785299 L4.6,6.2 L9.12944565,6.2 Z\"\n      ></path>\n    </g>\n  </svg>\n);\n\nexport const ZoomInIcon = createIcon(\n  <svg viewBox=\"0 0 16 16\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g id=\"zoom-in\" stroke=\"none\" fill=\"currentColor\" strokeWidth=\"1\">\n      <path\n        fillRule=\"nonzero\"\n        d=\"M6.85,-1.81188398e-13 C10.6331505,-1.81188398e-13 13.7,3.06684946 13.7,6.85 C13.7,8.54194045 13.0865836,10.0906098 12.0700142,11.2857448 L15.4201976,14.5717081 C15.6567367,14.8037768 15.6603607,15.1836585 15.4282919,15.4201976 C15.1962232,15.6567367 14.8163415,15.6603607 14.5798024,15.4282919 L14.5798024,15.4282919 L11.2163456,12.128262 C10.0309427,13.1099691 8.50937591,13.7 6.85,13.7 C3.06684946,13.7 4.61852778e-14,10.6331505 4.61852778e-14,6.85 C4.61852778e-14,3.06684946 3.06684946,-1.81188398e-13 6.85,-1.81188398e-13 Z M6.85,1.2 C3.72959116,1.2 1.2,3.72959116 1.2,6.85 C1.2,9.97040884 3.72959116,12.5 6.85,12.5 C8.31753357,12.5 9.65438791,11.9404957 10.6588859,11.0231643 C10.6855412,10.9625408 10.7245275,10.9050898 10.7743982,10.8542584 C10.8288931,10.7987137 10.8915387,10.7560124 10.9585649,10.7261903 C11.9144009,9.71595758 12.5,8.35136579 12.5,6.85 C12.5,3.72959116 9.97040884,1.2 6.85,1.2 Z M6.86472282,3.93527718 C7.16295659,3.93527718 7.41036958,4.15286679 7.45686984,4.43795406 L7.46472282,4.53527718 L7.464,6.19927718 L9.12944565,6.2 C9.42767941,6.2 9.6750924,6.41758961 9.72159266,6.70267688 L9.72944565,6.8 C9.72944565,7.09823376 9.51185604,7.34564675 9.22676876,7.39214701 L9.12944565,7.4 L7.464,7.39927718 L7.46472282,9.06472282 C7.46472282,9.36295659 7.24713321,9.61036958 6.96204594,9.65686984 L6.86472282,9.66472282 C6.56648906,9.66472282 6.31907607,9.44713321 6.27257581,9.16204594 L6.26472282,9.06472282 L6.264,7.39927718 L4.6,7.4 C4.30176624,7.4 4.05435325,7.18241039 4.00785299,6.89732312 L4,6.8 C4,6.50176624 4.21758961,6.25435325 4.50267688,6.20785299 L4.6,6.2 L6.264,6.19927718 L6.26472282,4.53527718 C6.26472282,4.2701805 6.43664548,4.0452385 6.67507642,3.96586557 L6.76739971,3.94313016 L6.86472282,3.93527718 Z\"\n      ></path>\n    </g>\n  </svg>\n);\n\nexport const SaveFileIcon = createIcon(\n  <svg viewBox=\"0 0 18 18\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g id=\"save-file\" stroke=\"none\" fill=\"currentColor\">\n      <path\n        fillRule=\"nonzero\"\n        d=\"M11.064 9.1l2.645 2.595.03-.029.848.849-3.523 3.323-.848-.848 1.994-1.883H7.5v-1.2h4.712l-1.996-1.958.848-.849zM9.356.3L13.7 3.71V7.9h-1.2l-.001-2.633H8.5V1.5L3.1 1.5a.4.4 0 0 0-.392.32L2.7 1.9v12a.4.4 0 0 0 .32.392l.08.008h3.418v1.2H3.1a1.6 1.6 0 0 1-1.593-1.454L1.5 13.9v-12A1.6 1.6 0 0 1 2.954.307L3.1.3h6.256zM9.7 2.095v1.973l2.51-.001L9.7 2.095z\"\n      ></path>\n    </g>\n  </svg>\n);\n\nexport const OpenFileIcon = createIcon(\n  <svg viewBox=\"0 0 18 18\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g id=\"save-file\" stroke=\"currentColor\" fill=\"none\">\n      <path\n        d=\"m9.257 6.351.183.183H15.819c.34 0 .727.182 1.051.506.323.323.505.708.505 1.05v5.819c0 .316-.183.7-.52 1.035-.337.338-.723.522-1.037.522H4.182c-.352 0-.74-.181-1.058-.5-.318-.318-.499-.705-.499-1.057V5.182c0-.351.181-.736.5-1.054.32-.321.71-.503 1.057-.503H6.53l2.726 2.726Z\"\n        strokeWidth=\"1.25\"\n      />\n    </g>\n  </svg>\n);\n\nexport const BackgroundColorIcon = createIcon(\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    className=\"background-color-icon\"\n  >\n    <g transform=\"translate(1 1)\" fillRule=\"evenodd\" fill=\"#000\" stroke=\"none\">\n      <circle fillOpacity=\".04\" r=\"11\" cy=\"11\" cx=\"11\"></circle>\n      <path\n        d=\"M17 20.221V17h3.221A11.06 11.06 0 0 1 17 20.221zm-12 0A11.06 11.06 0 0 1 1.779 17H5v3.221zM20.221 5H17V1.779A11.06 11.06 0 0 1 20.221 5zM9 .181V1H6.411A10.919 10.919 0 0 1 9 .181zM15.589 1H13V.181c.907.167 1.775.445 2.589.819zM13 21.819V21h2.589c-.814.374-1.682.652-2.589.819zm-4 0A10.919 10.919 0 0 1 6.411 21H9v.819zm-8-6.23A10.919 10.919 0 0 1 .181 13H1v2.589zm0-9.178V9H.181C.348 8.093.626 7.225 1 6.411zM21.819 9H21V6.411c.374.814.652 1.682.819 2.589zM21 15.589V13h.819A10.919 10.919 0 0 1 21 15.589zM5 1.779V5H1.779A11.06 11.06 0 0 1 5 1.779zM5 13h4v4H5v-4zm8 0h4v4h-4v-4zM5 5h4v4H5V5zm8 0h4v4h-4V5zm0 12v4H9v-4h4zm8-8v4h-4V9h4zm-8 0v4H9V9h4zM5 9v4H1V9h4zm8-8v4H9V1h4z\"\n        fillOpacity=\".12\"\n      ></path>\n    </g>\n  </svg>\n);\n\nexport const NoColorIcon = createIcon(\n  <svg viewBox=\"0 0 32 32\" className=\"no-color-icon\">\n    <g\n      xmlns=\"http://www.w3.org/2000/svg\"\n      fillRule=\"nonzero\"\n      fill=\"currentColor\"\n      stroke=\"none\"\n    >\n      <path d=\"M2 16c0 7.733 6.267 14 14 14s14-6.267 14-14S23.733 2 16 2 2 8.267 2 16zm-1 0C1 7.716 7.714 1 16 1c8.284 0 15 6.714 15 15 0 8.284-6.714 15-15 15-8.284 0-15-6.714-15-15z\"></path>\n      <path d=\"M6.354 26.354l-.708-.708 20-20 .708.708z\"></path>\n    </g>\n  </svg>\n);\n\nexport const Check = createIcon(\n  <svg\n    className=\"selected-icon\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth=\"2\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n  >\n    <polyline points=\"20 6 9 17 4 12\"></polyline>\n  </svg>\n);\n\nexport const StrokeIcon = createIcon(\n  <svg viewBox=\"0 0 24 24\" className=\"stroke-icon\">\n    <g\n      xmlns=\"http://www.w3.org/2000/svg\"\n      stroke=\"none\"\n      fillRule=\"evenodd\"\n      fill=\"#000\"\n    >\n      <path\n        d=\"M12 5a7 7 0 1 0 0 14 7 7 0 0 0 0-14zm0-4c6.075 0 11 4.925 11 11s-4.925 11-11 11S1 18.075 1 12 5.925 1 12 1z\"\n        fillRule=\"nonzero\"\n        fillOpacity=\".04\"\n      ></path>\n      <path\n        d=\"M12 5V1c1.491 0 2.914.297 4.21.835L14.68 5.53A6.979 6.979 0 0 0 12 5zm4.95 2.048l2.828-2.828a11.016 11.016 0 0 1 2.388 3.568l-3.697 1.53a7.01 7.01 0 0 0-1.519-2.27zM19 12h4c0 1.491-.297 2.914-.835 4.21l-3.696-1.53c.342-.826.531-1.73.531-2.68zm-2.05 4.95l2.828 2.828a11.016 11.016 0 0 1-3.567 2.387l-1.532-3.696a7.01 7.01 0 0 0 2.27-1.52zM12 19v4c-1.491 0-2.914-.297-4.21-.835l1.53-3.696c.826.342 1.73.531 2.68.531zm-4.95-2.05l-2.828 2.828a11.016 11.016 0 0 1-2.387-3.567l3.696-1.532a7.01 7.01 0 0 0 1.52 2.27zM5 12H1c0-1.491.297-2.914.835-4.21L5.53 9.32A6.979 6.979 0 0 0 5 12zm2.05-4.95L4.222 4.222a11.016 11.016 0 0 1 3.567-2.387L9.321 5.53a7.01 7.01 0 0 0-2.27 1.52z\"\n        fillOpacity=\".12\"\n      ></path>\n    </g>\n  </svg>\n);\n\nexport const StrokeWhiteIcon = createIcon(\n  <svg viewBox=\"0 0 24 24\">\n    <g\n      xmlns=\"http://www.w3.org/2000/svg\"\n      id=\"icon-border-white\"\n      stroke=\"none\"\n      strokeWidth=\"1\"\n      fill=\"none\"\n      fillRule=\"evenodd\"\n      opacity=\"0.1\"\n    >\n      <g id=\"Group\">\n        <path\n          d=\"M12,22 C17.5228475,22 22,17.5228475 22,12 C22,6.4771525 17.5228475,2 12,2 C6.4771525,2 2,6.4771525 2,12 C2,17.5228475 6.4771525,22 12,22 Z M12,23 C5.92486775,23 1,18.0751322 1,12 C1,5.92486775 5.92486775,1 12,1 C18.0751322,1 23,5.92486775 23,12 C23,18.0751322 18.0751322,23 12,23 Z\"\n          fill=\"#000000\"\n          fillRule=\"nonzero\"\n        />\n        <path\n          d=\"M12,19 C15.8659932,19 19,15.8659932 19,12 C19,8.13400675 15.8659932,5 12,5 C8.13400675,5 5,8.13400675 5,12 C5,15.8659932 8.13400675,19 12,19 Z M12,20 C7.581722,20 4,16.418278 4,12 C4,7.581722 7.581722,4 12,4 C16.418278,4 20,7.581722 20,12 C20,16.418278 16.418278,20 12,20 Z\"\n          fill=\"#000000\"\n          fillRule=\"nonzero\"\n        />\n      </g>\n    </g>\n  </svg>\n);\n\nexport const StrokeStyleNormalIcon = createIcon(\n  <svg viewBox=\"0 0 24 32\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g transform=\"translate(0 14)\" fillRule=\"evenodd\" fill=\"none\">\n      <path d=\"M-18-19h60v40h-60z\"></path>\n      <path d=\"M0 0h24v2H0z\" fill=\"currentColor\"></path>\n    </g>\n  </svg>\n);\n\nexport const StrokeStyleDashedIcon = createIcon(\n  <svg viewBox=\"0 0 24 32\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g transform=\"translate(0 14)\" fillRule=\"evenodd\" fill=\"none\">\n      <g fill=\"currentColor\">\n        <path d=\"M0 0h6v2H0zM9 0h6v2H9zM18 0h6v2h-6z\"></path>\n      </g>\n    </g>\n  </svg>\n);\n\nexport const StrokeStyleDotedIcon = createIcon(\n  <svg viewBox=\"0 0 24 32\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g transform=\"translate(0 14)\" fillRule=\"evenodd\" fill=\"none\">\n      <g fill=\"currentColor\">\n        <rect rx=\"1\" height=\"2\" width=\"2\"></rect>\n        <rect rx=\"1\" x=\"4\" height=\"2\" width=\"2\"></rect>\n        <rect rx=\"1\" x=\"8\" height=\"2\" width=\"2\"></rect>\n        <rect rx=\"1\" x=\"12\" height=\"2\" width=\"2\"></rect>\n        <rect rx=\"1\" x=\"16\" height=\"2\" width=\"2\"></rect>\n        <rect rx=\"1\" x=\"20\" height=\"2\" width=\"2\"></rect>\n      </g>\n    </g>\n  </svg>\n);\n\nexport const FontColorIcon: React.FC<{ currentColor?: string }> = ({\n  currentColor,\n}) => {\n  return (\n    <svg\n      viewBox=\"0 0 16 16\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      className=\"font-color-icon\"\n    >\n      <g\n        id=\"font-color\"\n        strokeWidth=\"1\"\n        fillRule=\"evenodd\"\n        stroke=\"none\"\n        fill=\"currentColor\"\n      >\n        <path\n          id=\"secondary-color\"\n          d=\"M1.999 15.011h11.998V13.81H1.999z\"\n          fill={currentColor || '#333333'}\n        ></path>\n        <path\n          d=\"M6.034 7.59h4.104L8.086 2.297 6.034 7.59zm-.465 1.2l-1.437 3.707H2.845L7.301 1h1.287l-.001.004h.286l4.454 11.492h-1.288L10.603 8.79H5.569z\"\n          id=\"A\"\n        ></path>\n      </g>\n    </svg>\n  );\n};\n\nexport const UndoIcon = createIcon(\n  <svg viewBox=\"0 0 16 16\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g stroke=\"none\" fill=\"currentColor\">\n      <g id=\"undo-cion\" transform=\"translate(1 1)\">\n        <path\n          d=\"M3.84 5.825a.6.6 0 0 1 .063.774l-.064.075a.6.6 0 0 1-.774.063l-.074-.063L.176 3.859a.6.6 0 0 1-.064-.775l.064-.074L3.01.176a.6.6 0 0 1 .912.774l-.063.074-1.795 1.794h6.851a5.1 5.1 0 0 1 .216 10.196l-.216.004h-4a.6.6 0 0 1-.097-1.192l.097-.008h4a3.9 3.9 0 0 0 .201-7.795l-.2-.005H2.033l1.805 1.807z\"\n          id=\"undo-icon-path\"\n        ></path>\n      </g>\n    </g>\n  </svg>\n);\n\nexport const RedoIcon = createIcon(\n  <svg viewBox=\"0 0 16 16\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g stroke=\"none\" fill=\"currentColor\">\n      <g id=\"redo-cion\" transform=\"matrix(-1 0 0 1 15.015 1)\">\n        <path\n          d=\"M3.84 5.825a.6.6 0 0 1 .063.774l-.064.075a.6.6 0 0 1-.774.063l-.074-.063L.176 3.859a.6.6 0 0 1-.064-.775l.064-.074L3.01.176a.6.6 0 0 1 .912.774l-.063.074-1.795 1.794h6.851a5.1 5.1 0 0 1 .216 10.196l-.216.004h-4a.6.6 0 0 1-.097-1.192l.097-.008h4a3.9 3.9 0 0 0 .201-7.795l-.2-.005H2.033l1.805 1.807z\"\n          id=\"redo-icon-path\"\n        ></path>\n      </g>\n    </g>\n  </svg>\n);\n\nexport const TrashIcon = createIcon(\n  <svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\">\n    <path\n      strokeWidth=\"1.25\"\n      d=\"M3.333 5.833h13.334M8.333 9.167v5M11.667 9.167v5M4.167 5.833l.833 10c0 .92.746 1.667 1.667 1.667h6.666c.92 0 1.667-.746 1.667-1.667l.833-10M7.5 5.833v-2.5c0-.46.373-.833.833-.833h3.334c.46 0 .833.373.833.833v2.5\"\n    ></path>\n  </svg>\n);\n\nexport const DuplicateIcon = createIcon(\n  <svg\n    viewBox=\"0 0 20 20\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <g strokeWidth=\"1.25\">\n      <path d=\"M14.375 6.458H8.958a2.5 2.5 0 0 0-2.5 2.5v5.417a2.5 2.5 0 0 0 2.5 2.5h5.417a2.5 2.5 0 0 0 2.5-2.5V8.958a2.5 2.5 0 0 0-2.5-2.5Z\"></path>\n      <path d=\"M11.667 3.125c.517 0 .986.21 1.325.55.34.338.55.807.55 1.325v1.458H8.333c-.485 0-.927.185-1.26.487-.343.312-.57.75-.609 1.24l-.005 5.357H5a1.87 1.87 0 0 1-1.326-.55 1.87 1.87 0 0 1-.549-1.325V5c0-.518.21-.987.55-1.326.338-.34.807-.549 1.325-.549h6.667Z\"></path>\n    </g>\n  </svg>\n);\n\nexport const FeltTipPenIcon = createIcon(\n  <svg\n    viewBox=\"0 0 1024 1024\"\n    fill=\"currentColor\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path d=\"M170.794667 896c3.456 0 6.912-0.426667 10.325333-1.28l170.666667-42.666667c7.509333-1.877333 14.378667-5.76 19.84-11.221333L896.128 316.330667c16.128-16.128 25.002667-37.546667 25.002667-60.330667s-8.874667-44.202667-25.002667-60.330667L828.458667 128c-32.256-32.256-88.405333-32.256-120.661334 0L183.296 652.501333a42.794667 42.794667 0 0 0-11.221333 19.797334l-42.666667 170.666666A42.666667 42.666667 0 0 0 170.794667 896z m597.333333-707.669333L835.797333 256l-67.669333 67.669333L700.458667 256l67.669333-67.669333zM251.989333 704.469333l388.138667-388.138666L707.797333 384l-388.181333 388.138667-90.197333 22.528 22.570666-90.197334z\"></path>\n  </svg>\n);\n\nexport const ImageIcon = createIcon(\n  <svg viewBox=\"0 0 16 16\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g id=\"image\" stroke=\"none\" fill=\"currentColor\">\n      <path d=\"M10.496 7c-.824 0-1.572-.675-1.498-1.5 0-.825.674-1.5 1.498-1.5.823 0 1.497.675 1.497 1.5S11.319 7 10.496 7zM13.8 9.476V2.2H2.2v5.432l.1-.078C3.132 6.904 4.029 6.5 5 6.5c.823 0 1.552.27 2.342.778.226.145.449.304.735.518.06.045.546.413.69.52 1.634 1.21 2.833 1.6 4.798 1.207l.235-.047zm0 1.523V10.7c-5 1-6.3-3-8.8-3-1.5 0-2.8 1.6-2.8 1.6v4.6h11.6V11zM14 1c.6 0 1 .536 1 1.071v11.784c0 .642-.4 1.071-1 1.071H2c-.6 0-1-.429-1-1.07V2.07c0-.535.4-1.07 1-1.07h12z\"></path>\n    </g>\n  </svg>\n);\n\nexport const ExtraToolsIcon = createIcon(\n  <svg\n    stroke=\"currentColor\"\n    viewBox=\"0 0 24 24\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <g strokeWidth={1.8} fill=\"none\">\n      <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"></path>\n      <path d=\"M12 3l-4 7h8z\"></path>\n      <path d=\"M17 17m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0\"></path>\n      <path d=\"M4 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z\"></path>\n    </g>\n  </svg>\n);\n\nexport const MermaidLogoIcon = createIcon(\n  <svg\n    stroke=\"currentColor\"\n    viewBox=\"0 0 512 512\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path\n      stroke=\"none\"\n      fill=\"currentColor\"\n      d=\"M407.48,111.18C335.587,108.103 269.573,152.338 245.08,220C220.587,152.338 154.573,108.103 82.68,111.18C80.285,168.229 107.577,222.632 154.74,254.82C178.908,271.419 193.35,298.951 193.27,328.27L193.27,379.13L296.9,379.13L296.9,328.27C296.816,298.953 311.255,271.42 335.42,254.82C382.596,222.644 409.892,168.233 407.48,111.18Z\"\n    />\n  </svg>\n);\n\nexport const MarkdownLogoIcon = createIcon(\n  <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" version=\"1.1\">\n    <g stroke=\"none\" fill=\"currentColor\">\n      <path d=\"M14.85,2.5 C15.4851275,2.5 16,3.01487254 16,3.65 L16,12.35 C16,12.9851275 15.4851275,13.5 14.85,13.5 L1.15,13.5 C0.514872538,13.5 0,12.9851275 0,12.35 L0,3.65 C0,3.01487254 0.514872538,2.5 1.15,2.5 L14.85,2.5 Z M14.85,3.7 L1.15,3.7 C1.17735931,3.7 1.2,3.72264069 1.2,3.75 L1.2,12.25 C1.2,12.2773593 1.17735931,12.3 1.15,12.3 L14.85,12.3 C14.8226407,12.3 14.8,12.2773593 14.8,12.25 L14.8,3.75 C14.8,3.72264069 14.8226407,3.7 14.85,3.7 Z M3.5,10.5 L3.5,5.5 L5.25,5.5 L7,7.8 L8.75,5.5 L10.5,5.5 L10.5,10.5 L8.75,10.5 L8.75,7.5 L7,9.8 L5.25,7.5 L5.25,10.5 L3.5,10.5 Z M12.5,10.5 L11,8.5 L12.5,8.5 L12.5,5.5 L11,5.5 L12.5,5.5 L12.5,8.5 L14,8.5 L12.5,10.5 Z\" />\n    </g>\n  </svg>\n);\n\nexport const LinkIcon = createIcon(\n  <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\">\n    <g stroke=\"none\" fill=\"currentColor\">\n      <path\n        d=\"M12.253 4.13h-1.2v-1a2.8 2.8 0 0 0-5.6 0v4a2.8 2.8 0 0 0 2.8 2.8v1.2a4 4 0 0 1-4-4v-4a4 4 0 0 1 8 0v1zm-8 8h1.2v1a2.8 2.8 0 0 0 5.6 0v-4a2.8 2.8 0 0 0-2.8-2.8v-1.2a4 4 0 0 1 4 4v4a4 4 0 0 1-8 0v-1z\"\n        transform=\"rotate(46 8.253 8.13)\"\n      ></path>\n    </g>\n  </svg>\n);\n\n\nexport const ArrowIcon = createIcon(\n  <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\">\n    <g  stroke=\"none\">\n      <path d=\"M8.44521878,4.21103025 C8.58299906,3.97171622 8.8886944,3.88940684 9.12800843,4.02718711 L15.242109,7.54725833 C15.3194119,7.59176394 15.3834015,7.65613893 15.4274422,7.73370766 C15.5637831,7.97384463 15.4796398,8.27904026 15.2395028,8.41538118 L9.12748155,11.8855614 C9.0176214,11.947936 8.88822223,11.9664118 8.76529593,11.9372749 C8.4965984,11.8735862 8.33040588,11.604134 8.39409456,11.3354364 L9.018,8.69941945 L1.5,8.7 C1.22385763,8.7 1,8.47614237 1,8.2 L1,8 C1,7.72385763 1.22385763,7.5 1.5,7.5 L9.075,7.49941945 L8.39165922,4.57430951 C8.3700078,4.48168206 8.37536432,4.38547957 8.40609313,4.29679626 Z\"></path>\n    </g>\n  </svg>\n);\n\nexport const LineIcon = createIcon(\n  <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\">\n    <g stroke=\"none\">\n      <rect x=\"1\" y=\"7.5\" width=\"14\" height=\"1.2\" rx=\".5\"></rect>\n    </g>\n  </svg>\n);\n\nexport const StraightLineIcon = createIcon(\n  <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\">\n    <g stroke=\"none\">\n      <path d=\"M14.701408,3.54812055 C14.9888311,3.38321272 15.3555178,3.48253099 15.5204256,3.76995411 C15.6853334,4.05737723 15.5860152,4.42406391 15.298592,4.58897174 L1.29859203,12.6214138 C1.01116891,12.7863216 0.644482231,12.6870034 0.479574406,12.3995802 C0.314666581,12.1121571 0.41398485,11.7454704 0.701407969,11.5805626 L14.701408,3.54812055 Z\"></path>\n    </g>\n  </svg>\n);\n\nexport const NoteCurlyRightIcon = createIcon(\n  <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\">\n    <g stroke=\"none\" fill=\"currentColor\" fillRule=\"evenodd\">\n      <path d=\"M13,4 L13,5.2 L6,5.2 L6,4 L13,4 Z M14,7.4 L14,8.6 L6,8.6 L6,7.4 L14,7.4 Z M10,10.8 L10,12 L6,12 L6,10.8 L10,10.8 Z M1,15.0041595 L1,13.8041595 L2.79468336,13.8041595 L2.79468336,9.78041534 C2.79468336,9.50369117 2.86643344,9.23268025 3.0016431,8.99336795 L3.09031773,8.85379228 L3.67068336,8.03815953 L3.05107199,7.08070632 C2.91160731,6.86500725 2.82653611,6.61956432 2.80205305,6.36536742 L2.79468336,6.21196672 L2.79468336,2.20015953 L1,2.2 L1,1 L3.39468336,1 C3.72605421,1 3.99468365,1.26862915 3.99468365,1.6 L3.99468365,6.21196672 C3.99468365,6.28902439 4.01694112,6.3644419 4.05878052,6.42915162 L4.89853762,7.72793804 C5.03190909,7.93421321 5.02607838,8.20094898 4.88382047,8.40119903 L4.06859195,9.54875958 C4.02051339,9.61643761 3.99468365,9.69739809 3.99468365,9.78041534 L3.99468365,14.4041595 C3.99468365,14.7355304 3.72605421,15.0041595 3.39468336,15.0041595 L1,15.0041595 Z\" />\n    </g>\n  </svg>\n);\n\nexport const NoteCurlyLeftIcon = createIcon(\n  <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\">\n    <g stroke=\"none\" fill=\"currentColor\" fillRule=\"evenodd\">\n      <path d=\"M9,4 L9,5.2 L2,5.2 L2,4 L9,4 Z M10,7.4 L10,8.6 L2,8.6 L2,7.4 L10,7.4 Z M6,10.8 L6,12 L2,12 L2,10.8 L6,10.8 Z M15.0155409,1 L15.0155409,2.2 L13.2208576,2.2 L13.2208576,6.22374419 C13.2208576,6.50046836 13.1491075,6.77147928 13.0138978,7.01079158 L12.9252232,7.15036725 L12.3448576,7.966 L12.9644689,8.92345321 C13.1039336,9.13915228 13.1890048,9.38459521 13.2134879,9.63879211 L13.2208576,9.79219281 L13.2208576,13.804 L15.0155409,13.8041595 L15.0155409,15.0041595 L12.6208576,15.0041595 C12.2894867,15.0041595 12.0208573,14.7355304 12.0208573,14.4041595 L12.0208573,9.79219281 C12.0208573,9.71513514 11.9985998,9.63971763 11.9567604,9.57500791 L11.1170033,8.2762215 C10.9836318,8.06994632 10.9894625,7.80321055 11.1317204,7.6029605 L11.946949,6.45539995 C11.9950275,6.38772192 12.0208573,6.30676144 12.0208573,6.22374419 L12.0208573,1.6 C12.0208573,1.26862915 12.2894867,1 12.6208576,1 L15.0155409,1 Z\" />\n    </g>\n  </svg>\n);\n\nexport const ChevronDownIcon = createIcon(\n  <svg\n    width=\"15\"\n    height=\"15\"\n    viewBox=\"0 0 15 15\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path\n      d=\"M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z\"\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      clipRule=\"evenodd\"\n    ></path>\n  </svg>\n);\n\nexport const ThickCheckIcon = createIcon(\n  <svg\n    width=\"15\"\n    height=\"15\"\n    viewBox=\"0 0 15 15\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path\n      d=\"M11.4669 3.72684C11.7558 3.91574 11.8369 4.30308 11.648 4.59198L7.39799 11.092C7.29783 11.2452 7.13556 11.3467 6.95402 11.3699C6.77247 11.3931 6.58989 11.3355 6.45446 11.2124L3.70446 8.71241C3.44905 8.48022 3.43023 8.08494 3.66242 7.82953C3.89461 7.57412 4.28989 7.55529 4.5453 7.78749L6.75292 9.79441L10.6018 3.90792C10.7907 3.61902 11.178 3.53795 11.4669 3.72684Z\"\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      clipRule=\"evenodd\"\n    ></path>\n  </svg>\n);\n\nexport const FontSizeStepperUpIcon: React.FC<\n  React.SVGProps<SVGSVGElement>\n> = (props) => {\n  return (\n    <svg viewBox=\"0 0 16 16\" xmlns=\"http://www.w3.org/2000/svg\" {...props}>\n      <path\n        d=\"M4 10L8 6L12 10\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth=\"1.5\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n};\n\nexport const FontSizeStepperDownIcon: React.FC<\n  React.SVGProps<SVGSVGElement>\n> = (props) => {\n  return (\n    <svg viewBox=\"0 0 16 16\" xmlns=\"http://www.w3.org/2000/svg\" {...props}>\n      <path\n        d=\"M4 6L8 10L12 6\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth=\"1.5\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/drawnix/src/components/island.scss",
    "content": ".drawnix {\n  .island {\n    --padding: 0;\n    box-sizing: border-box;\n    background-color: var(--island-bg-color);\n    box-shadow: var(--shadow-island);\n    border-radius: var(--border-radius-md);\n    padding: calc(var(--padding) * var(--space-factor));\n    position: relative;\n    transition: box-shadow 0.5s ease-in-out;\n    border: 1px solid var(--island-border-color);\n\n    &.zen-mode {\n      box-shadow: none;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/drawnix/src/components/island.tsx",
    "content": "// Credits to excalidraw\nimport classNames from 'classnames';\nimport './island.scss';\n\nimport React from 'react';\n\ntype IslandProps = {\n  children: React.ReactNode;\n  padding?: number;\n  className?: string | boolean;\n  style?: object;\n} & React.HTMLAttributes<HTMLDivElement>;\n\nexport const Island = React.forwardRef<HTMLDivElement, IslandProps>(\n  ({ children, padding, className, style, ...restProps }, ref) => (\n    <div\n      className={classNames('island', className)}\n      style={{ '--padding': padding, ...style }}\n      ref={ref}\n      {...restProps}\n    >\n      {children}\n    </div>\n  )\n);\n"
  },
  {
    "path": "packages/drawnix/src/components/menu/common.ts",
    "content": "import React, { useContext } from 'react';\nimport { EVENT } from '../../constants';\nimport { composeEventHandlers } from '../../utils/common';\n\nexport const MenuContentPropsContext = React.createContext<{\n  onSelect?: (event: Event) => void;\n}>({});\n\nexport const getMenuItemClassName = (\n  className = '',\n  active = false,\n) => {\n  return `menu-item menu-item-base ${className} ${\n    active ? 'menu-item--active' : ''\n  }`.trim();\n};\n\nexport const useHandleMenuItemClick = (\n  origOnClick:\n    | React.MouseEventHandler<HTMLAnchorElement | HTMLButtonElement>\n    | undefined,\n  onSelect: ((event: Event) => void) | undefined\n) => {\n  const menuContentProps = useContext(MenuContentPropsContext);\n\n  return composeEventHandlers(origOnClick, (event) => {\n    const itemSelectEvent = new CustomEvent(EVENT.MENU_ITEM_SELECT, {\n      bubbles: true,\n      cancelable: true,\n    });\n    onSelect?.(itemSelectEvent);\n    if (!itemSelectEvent.defaultPrevented) {\n      menuContentProps.onSelect?.(itemSelectEvent);\n    }\n  });\n};\n"
  },
  {
    "path": "packages/drawnix/src/components/menu/menu-group.tsx",
    "content": "import React from 'react';\n\nconst MenuGroup = ({\n  children,\n  className = '',\n  style,\n  title,\n}: {\n  children: React.ReactNode;\n  className?: string;\n  style?: React.CSSProperties;\n  title?: string;\n}) => {\n  return (\n    <div className={`menu-group ${className}`} style={style}>\n      {title && <p className=\"menu-group-title\">{title}</p>}\n      {children}\n    </div>\n  );\n};\n\nexport default MenuGroup;\nMenuGroup.displayName = 'MenuGroup';\n"
  },
  {
    "path": "packages/drawnix/src/components/menu/menu-item-content-radio.tsx",
    "content": "import { RadioGroup } from '../radio-group';\n\ntype Props<T> = {\n  value: T;\n  shortcut?: string;\n  choices: {\n    value: T;\n    label: React.ReactNode;\n    ariaLabel?: string;\n  }[];\n  onChange: (value: T) => void;\n  children: React.ReactNode;\n  name: string;\n};\n\nconst MenuItemContentRadio = <T,>({\n  value,\n  shortcut,\n  onChange,\n  choices,\n  children,\n  name,\n}: Props<T>) => {\n  return (\n    <>\n      <div className=\"menu-item-base menu-item-bare\">\n        <label className=\"menu-item__text\" htmlFor={name}>\n          {children}\n        </label>\n        <RadioGroup\n          name={name}\n          value={value}\n          onChange={onChange}\n          choices={choices}\n        />\n      </div>\n      {shortcut && (\n        <div className=\"menu-item__shortcut menu-item__shortcut--orphaned\">\n          {shortcut}\n        </div>\n      )}\n    </>\n  );\n};\n\nMenuItemContentRadio.displayName = 'MenuItemContentRadio';\n\nexport default MenuItemContentRadio;\n"
  },
  {
    "path": "packages/drawnix/src/components/menu/menu-item-content.tsx",
    "content": "import React from 'react';\n\nconst MenuItemContent = ({\n  icon,\n  shortcut,\n  children,\n}: {\n  icon?: React.ReactNode;\n  shortcut?: string;\n  children: React.ReactNode;\n}) => {\n  return (\n    <>\n      {icon && <div className=\"menu-item__icon\">{icon}</div>}\n      <div className=\"menu-item__text\">{children}</div>\n      {shortcut && (\n        <div className=\"menu-item__shortcut\">{shortcut}</div>\n      )}\n    </>\n  );\n};\nexport default MenuItemContent;\n"
  },
  {
    "path": "packages/drawnix/src/components/menu/menu-item-custom.tsx",
    "content": "import React from 'react';\n\nconst MenuItemCustom = ({\n  children,\n  className = '',\n  selected,\n  ...rest\n}: {\n  children: React.ReactNode;\n  className?: string;\n  selected?: boolean;\n} & React.HTMLAttributes<HTMLDivElement>) => {\n  return (\n    <div\n      {...rest}\n      className={`menu-item-base menu-item-custom ${className} ${\n        selected ? `menu-item--selected` : ``\n      }`.trim()}\n    >\n      {children}\n    </div>\n  );\n};\n\nexport default MenuItemCustom;\n"
  },
  {
    "path": "packages/drawnix/src/components/menu/menu-item-link.tsx",
    "content": "import React from 'react';\nimport { getMenuItemClassName, useHandleMenuItemClick } from './common';\nimport MenuItemContent from './menu-item-content';\n\nconst MenuItemLink = ({\n  icon,\n  shortcut,\n  href,\n  children,\n  onSelect,\n  className = '',\n  selected,\n  ...rest\n}: {\n  href: string;\n  icon?: React.ReactNode;\n  children: React.ReactNode;\n  shortcut?: string;\n  className?: string;\n  selected?: boolean;\n  onSelect?: (event: Event) => void;\n} & React.AnchorHTMLAttributes<HTMLAnchorElement>) => {\n  const handleClick = useHandleMenuItemClick(rest.onClick, onSelect);\n\n  return (\n    <a\n      {...rest}\n      href={href}\n      target=\"_blank\"\n      rel=\"noreferrer\"\n      className={getMenuItemClassName(className, selected)}\n      title={rest.title ?? rest['aria-label']}\n      onClick={handleClick}\n    >\n      <MenuItemContent icon={icon} shortcut={shortcut}>\n        {children}\n      </MenuItemContent>\n    </a>\n  );\n};\n\nexport default MenuItemLink;\nMenuItemLink.displayName = 'MenuItemLink';\n"
  },
  {
    "path": "packages/drawnix/src/components/menu/menu-item.tsx",
    "content": "import React, { useState, useRef } from 'react';\nimport {\n  getMenuItemClassName,\n  useHandleMenuItemClick,\n} from './common';\nimport MenuItemContent from './menu-item-content';\nimport { Popover, PopoverContent, PopoverTrigger } from '../popover/popover';\n\nconst MenuItem = ({\n  icon,\n  onSelect,\n  children,\n  shortcut,\n  className,\n  selected,\n  submenu,\n  ...rest\n}: {\n  icon?: React.ReactNode;\n  onSelect: (event: Event) => void;\n  children: React.ReactNode;\n  shortcut?: string;\n  selected?: boolean;\n  className?: string;\n  submenu?: React.ReactNode;\n} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onSelect'>) => {\n  const [isOpen, setIsOpen] = useState(false);\n  const closeTimeoutRef = useRef<number>();\n  const handleClick = useHandleMenuItemClick(rest.onClick, onSelect);\n  \n  const menuItemContent = (\n    <MenuItemContent icon={icon} shortcut={shortcut}>\n      {children}\n    </MenuItemContent>\n  );\n\n  const handleMouseEnter = () => {\n    if (closeTimeoutRef.current) {\n      window.clearTimeout(closeTimeoutRef.current);\n    }\n    setIsOpen(true);\n  };\n\n  const handleMouseLeave = () => {\n    closeTimeoutRef.current = window.setTimeout(() => {\n      setIsOpen(false);\n    }, 100);\n  };\n\n  const handleMenuItemClick = (event: React.MouseEvent<HTMLButtonElement>) => {\n    if (submenu) {\n      setIsOpen(!isOpen);\n      rest.onClick?.(event as any);\n    } else {\n      handleClick(event as any);\n    }\n  };\n\n  if (submenu) {\n    return (\n      <Popover \n        open={isOpen}\n        onOpenChange={setIsOpen}\n        placement=\"right-start\"\n      >\n        <PopoverTrigger asChild>\n          <button\n            {...rest}\n            type=\"button\"\n            className={getMenuItemClassName(className, selected || isOpen)}\n            title={rest.title ?? rest['aria-label']}\n            onClick={handleMenuItemClick}\n            onMouseEnter={handleMouseEnter}\n            onMouseLeave={handleMouseLeave}\n          >\n            {menuItemContent}\n          </button>\n        </PopoverTrigger>\n        <PopoverContent onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>\n          {submenu}\n        </PopoverContent>\n      </Popover>\n    );\n  }\n\n  return (\n    <button\n      {...rest}\n      onClick={handleClick}\n      type=\"button\"\n      className={getMenuItemClassName(className, selected)}\n      title={rest.title ?? rest['aria-label']}\n    >\n      {menuItemContent}\n    </button>\n  );\n};\nMenuItem.displayName = 'MenuItem';\n\nexport const DropDownMenuItemBadge = ({\n  children,\n}: {\n  children: React.ReactNode;\n}) => {\n  return (\n    <div\n      style={{\n        display: 'inline-flex',\n        marginLeft: 'auto',\n        padding: '2px 4px',\n        background: 'var(--color-promo)',\n        color: 'var(--color-surface-lowest)',\n        borderRadius: 6,\n        fontSize: 9,\n        fontFamily: 'Cascadia, monospace',\n      }}\n    >\n      {children}\n    </div>\n  );\n};\nDropDownMenuItemBadge.displayName = 'MenuItemBadge';\n\nMenuItem.Badge = DropDownMenuItemBadge;\n\nexport default MenuItem;\n"
  },
  {
    "path": "packages/drawnix/src/components/menu/menu-separator.tsx",
    "content": "const MenuSeparator = () => (\n  <div\n    style={{\n      height: '1px',\n      backgroundColor: 'var(--color-gray-10)',\n      margin: '.5rem 0',\n    }}\n  />\n);\n\nexport default MenuSeparator;\nMenuSeparator.displayName = 'MenuSeparator';\n"
  },
  {
    "path": "packages/drawnix/src/components/menu/menu.scss",
    "content": "@import \"../../styles/variables.module.scss\";\n\n.drawnix {\n  .menu {\n    &--mobile {\n      left: 0;\n      width: 100%;\n      row-gap: 0.75rem;\n\n      .menu-container {\n        padding: 8px 8px;\n        box-sizing: border-box;\n        box-shadow: var(--shadow-island);\n        border-radius: var(--border-radius-lg);\n        position: relative;\n        transition: box-shadow 0.5s ease-in-out;\n\n        &.zen-mode {\n          box-shadow: none;\n        }\n      }\n    }\n\n    .menu-container {\n      background-color: var(--island-bg-color);\n      max-height: calc(100vh - 150px);\n      overflow-y: auto;\n      --gap: 2;\n    }\n\n    .menu-item-base {\n      display: flex;\n      padding: 0 0.625rem;\n      column-gap: 0.625rem;\n      font-size: 0.875rem;\n      color: var(--color-gray-90);\n      width: 100%;\n      box-sizing: border-box;\n      font-weight: normal;\n      font-family: inherit;\n    }\n\n    .menu-item {\n      background-color: transparent;\n      border: 1px solid transparent;\n      align-items: center;\n      height: 2rem;\n      margin-top: 4px;\n      cursor: pointer;\n      border-radius: var(--border-radius-md);\n\n      @media screen and (min-width: 1921px) {\n        height: 2.25rem;\n      }\n\n      &--active {\n        background-color: var(--color-surface-primary-container);\n        text-decoration: none;\n      }\n\n      &__text {\n        display: flex;\n        align-items: center;\n        width: 100%;\n        text-overflow: ellipsis;\n        overflow: hidden;\n        white-space: nowrap;\n        gap: 0.75rem;\n      }\n\n      &__shortcut {\n        margin-inline-start: auto;\n        opacity: 0.5;\n\n        &--orphaned {\n          text-align: right;\n          font-size: 0.875rem;\n          padding: 0 0.625rem;\n        }\n      }\n\n      &:hover {\n        background-color: var(--color-surface-primary-container);\n        text-decoration: none;\n      }\n\n      &:active {\n        background-color: var(--color-surface-primary-container);\n        border-color: var(--color-brand-active);\n      }\n\n      svg {\n        width: 1rem;\n        height: 1rem;\n        display: block;\n      }\n    }\n\n    .menu-item-bare {\n      align-items: center;\n      height: 2rem;\n      justify-content: space-between;\n\n      @media screen and (min-width: 1921px) {\n        height: 2.25rem;\n      }\n\n      svg {\n        width: 1rem;\n        height: 1rem;\n        display: block;\n      }\n    }\n\n    .menu-item-custom {\n      margin-top: 0.5rem;\n    }\n\n    .menu-group-title {\n      font-size: 14px;\n      text-align: left;\n      margin: 10px 0;\n      font-weight: 500;\n    }\n  }\n\n  .menu-button {\n    @include outlineButtonStyles;\n    width: var(--lg-button-size);\n    height: var(--lg-button-size);\n\n    @at-root .drawnix.theme--dark#{&} {\n      --background: var(--color-surface-high);\n      &:hover {\n        --background: #363541;\n      }\n    }\n\n    svg {\n      width: var(--lg-icon-size);\n      height: var(--lg-icon-size);\n    }\n\n    &--mobile {\n      border: none;\n      margin: 0;\n      padding: 0;\n      width: var(--default-button-size);\n      height: var(--default-button-size);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/drawnix/src/components/menu/menu.tsx",
    "content": "import { Island } from '../island';\nimport React from 'react';\nimport { MenuContentPropsContext } from './common';\nimport classNames from 'classnames';\nimport './menu.scss';\n\nconst Menu = ({\n  children,\n  className = '',\n  onSelect,\n  style,\n}: {\n  children?: React.ReactNode;\n  className?: string;\n  /**\n   * Called when any menu item is selected (clicked on).\n   */\n  onSelect?: (event: Event) => void;\n  style?: React.CSSProperties;\n}) => {\n  const newClassName = classNames(`menu ${className}`).trim();\n\n  return (\n    <MenuContentPropsContext.Provider value={{ onSelect }}>\n      <div className={newClassName} style={style} data-testid=\"menu\">\n        {\n          <Island className=\"menu-container\" padding={2}>\n            {children}\n          </Island>\n        }\n      </div>\n    </MenuContentPropsContext.Provider>\n  );\n};\nMenu.displayName = 'Menu';\n\nexport default Menu;\n"
  },
  {
    "path": "packages/drawnix/src/components/popover/popover.tsx",
    "content": "import * as React from 'react';\nimport {\n  useFloating,\n  autoUpdate,\n  offset,\n  flip,\n  shift,\n  useClick,\n  useDismiss,\n  useRole,\n  useInteractions,\n  useMergeRefs,\n  Placement,\n  FloatingPortal,\n  FloatingFocusManager,\n} from '@floating-ui/react';\n\ninterface PopoverOptions {\n  initialOpen?: boolean;\n  placement?: Placement;\n  modal?: boolean;\n  open?: boolean;\n  sideOffset?: number;\n  onOpenChange?: (open: boolean) => void;\n}\n\nexport function usePopover({\n  initialOpen = false,\n  placement = 'bottom',\n  modal,\n  sideOffset,\n  open: controlledOpen,\n  onOpenChange: setControlledOpen,\n}: PopoverOptions = {}) {\n  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(initialOpen);\n  const [labelId, setLabelId] = React.useState<string | undefined>();\n  const [descriptionId, setDescriptionId] = React.useState<\n    string | undefined\n  >();\n\n  const open = controlledOpen ?? uncontrolledOpen;\n  const setOpen = setControlledOpen ?? setUncontrolledOpen;\n\n  const data = useFloating({\n    placement,\n    open,\n    onOpenChange: setOpen,\n    whileElementsMounted: autoUpdate,\n    middleware: [\n      offset(sideOffset || 4),\n      flip({\n        crossAxis: placement.includes('-'),\n        fallbackAxisSideDirection: 'end',\n        padding: 5,\n      }),\n      shift({ padding: 5 }),\n    ],\n  });\n\n  const context = data.context;\n\n  const click = useClick(context, {\n    enabled: controlledOpen == null,\n  });\n  const dismiss = useDismiss(context);\n  const role = useRole(context);\n\n  const interactions = useInteractions([click, dismiss, role]);\n\n  return React.useMemo(\n    () => ({\n      open,\n      setOpen,\n      ...interactions,\n      ...data,\n      modal,\n      labelId,\n      descriptionId,\n      setLabelId,\n      setDescriptionId,\n    }),\n    [open, setOpen, interactions, data, modal, labelId, descriptionId]\n  );\n}\n\ntype ContextType =\n  | (ReturnType<typeof usePopover> & {\n      setLabelId: React.Dispatch<React.SetStateAction<string | undefined>>;\n      setDescriptionId: React.Dispatch<\n        React.SetStateAction<string | undefined>\n      >;\n    })\n  | null;\n\nconst PopoverContext = React.createContext<ContextType>(null);\n\nexport const usePopoverContext = () => {\n  const context = React.useContext(PopoverContext);\n\n  if (context == null) {\n    throw new Error('Popover components must be wrapped in <Popover />');\n  }\n\n  return context;\n};\n\nexport function Popover({\n  children,\n  modal = false,\n  ...restOptions\n}: {\n  children: React.ReactNode;\n} & PopoverOptions) {\n  // This can accept any props as options, e.g. `placement`,\n  // or other positioning options.\n  const popover = usePopover({ modal, ...restOptions });\n  return (\n    <PopoverContext.Provider value={popover}>\n      {children}\n    </PopoverContext.Provider>\n  );\n}\n\ninterface PopoverTriggerProps {\n  children: React.ReactNode;\n  asChild?: boolean;\n}\n\nexport const PopoverTrigger = React.forwardRef<\n  HTMLElement,\n  React.HTMLProps<HTMLElement> & PopoverTriggerProps\n>(function PopoverTrigger({ children, asChild = false, ...props }, propRef) {\n  const context = usePopoverContext();\n  const childrenRef = (children as any).ref;\n  const ref = useMergeRefs([context.refs.setReference, propRef, childrenRef]);\n\n  // `asChild` allows the user to pass any element as the anchor\n  if (asChild && React.isValidElement(children)) {\n    return React.cloneElement(\n      children,\n      context.getReferenceProps({\n        ref,\n        ...props,\n        ...children.props,\n        'data-state': context.open ? 'open' : 'closed',\n      })\n    );\n  }\n\n  return (\n    <button\n      ref={ref}\n      type=\"button\"\n      // The user can style the trigger based on the state\n      data-state={context.open ? 'open' : 'closed'}\n      {...context.getReferenceProps(props)}\n    >\n      {children}\n    </button>\n  );\n});\n\nexport const PopoverContent = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLProps<HTMLDivElement> & { container?: HTMLElement | null }\n>(function PopoverContent({ container, style, ...props }, propRef) {\n  const { context: floatingContext, ...context } = usePopoverContext();\n  const ref = useMergeRefs([context.refs.setFloating, propRef]);\n\n  if (!floatingContext.open) return null;\n\n  return (\n    <FloatingPortal root={container}>\n      <FloatingFocusManager context={floatingContext} modal={context.modal}>\n        <div\n          ref={ref}\n          style={{ ...context.floatingStyles, ...style }}\n          aria-labelledby={context.labelId}\n          aria-describedby={context.descriptionId}\n          {...context.getFloatingProps(props)}\n        >\n          {props.children}\n        </div>\n      </FloatingFocusManager>\n    </FloatingPortal>\n  );\n});\n"
  },
  {
    "path": "packages/drawnix/src/components/popup/link-popup/link-popup.scss",
    "content": ".drawnix {\n  .link-popup {\n    padding-left: 8px;\n\n    &__link {\n      font-size: 14px;\n    }\n\n    .link-popup__link {\n      display: inline-block;\n      width: 18rem;\n      white-space: nowrap;\n      overflow: hidden;\n      text-overflow: ellipsis;\n    }\n\n    &__input {\n      padding: 10px 0px;\n      width: 328px;\n      border: none;\n      border-radius: 4px;\n      font-size: 14px;\n      outline: none;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/drawnix/src/components/popup/link-popup/link-popup.tsx",
    "content": "import { useEffect, useState, useRef } from 'react';\nimport { Island } from '../../island';\nimport Stack from '../../stack';\nimport { ToolButton } from '../../tool-button';\nimport classNames from 'classnames';\nimport './link-popup.scss';\nimport { flip, offset, useFloating } from '@floating-ui/react';\nimport { useDrawnix } from '../../../hooks/use-drawnix';\nimport { FeltTipPenIcon, TrashIcon } from '../../icons';\nimport { Transforms } from 'slate';\nimport { ReactEditor } from 'slate-react';\nimport { LinkEditor } from '@plait/text-plugins';\nimport { LinkElement } from '@plait/common';\nimport { useBoard } from '@plait-board/react-board';\nimport { useI18n } from '../../../i18n';\n\nexport const LinkPopup = () => {\n  const { t } = useI18n();\n  const [url, setUrl] = useState('');\n\n  const { appState, setAppState } = useDrawnix();\n\n  const board = useBoard();\n\n  const { refs, floatingStyles } = useFloating({\n    placement: 'top',\n    middleware: [offset(20), flip()],\n  });\n\n  const linkState = appState.linkState;\n  const target = appState.linkState?.targetDom || null;\n  const isEditing = appState.linkState?.isEditing || false;\n  const isHoveringOrigin = appState.linkState?.isHoveringOrigin || false;\n  const isHovering = appState.linkState?.isHovering || false;\n  const isOpening = isEditing || isHoveringOrigin || isHovering;\n\n  const linkStateRef = useRef(appState.linkState);\n\n  useEffect(() => {\n    linkStateRef.current = appState.linkState;\n    if (appState.linkState) {\n      setUrl(appState.linkState.targetElement.url);\n    } else {\n      setUrl('');\n    }\n  }, [appState.linkState]);\n\n  useEffect(() => {\n    if (target) {\n      const rect = target.getBoundingClientRect();\n      refs.setPositionReference({\n        getBoundingClientRect() {\n          return {\n            x: rect.x,\n            y: rect.y,\n            width: rect.width,\n            height: rect.height,\n            top: rect.y,\n            left: rect.x,\n            right: rect.x + rect.width,\n            bottom: rect.y + rect.height,\n          };\n        },\n      });\n    }\n  }, [board.viewport, target]);\n\n  useEffect(() => {\n    const handleClickOutside = (event: MouseEvent) => {\n      if (\n        refs.floating.current &&\n        !refs.floating.current.contains(event.target as Node)\n      ) {\n        if (linkStateRef.current) {\n          const linkElement = LinkEditor.getLinkElement(\n            linkStateRef.current.editor\n          );\n          if (linkElement && !(linkElement[0] as LinkElement).url.trim()) {\n            LinkEditor.unwrapLink(linkStateRef.current.editor);\n          }\n        }\n        setAppState({\n          ...appState,\n          linkState: null,\n        });\n      }\n    };\n\n    document.addEventListener('mousedown', handleClickOutside);\n    return () => {\n      document.removeEventListener('mousedown', handleClickOutside);\n    };\n  }, []);\n\n  const saveUrlAndExitEditing = () => {\n    if (url !== linkState!.targetElement.url) {\n      const editor = linkState!.editor;\n      const node = linkState!.targetElement;\n      const path = ReactEditor.findPath(editor, node);\n      Transforms.setNodes(editor, { url: url }, { at: path });\n    }\n    const linkElement = LinkEditor.getLinkElement(linkState!.editor);\n    setAppState({\n      ...appState,\n      linkState: {\n        ...appState.linkState!,\n        targetElement: linkElement[0] as LinkElement,\n        isEditing: false,\n        isHoveringOrigin: true,\n      },\n    });\n  };\n\n  return (\n    isOpening && (\n      <Island\n        ref={refs.setFloating}\n        style={floatingStyles}\n        padding={1}\n        className={classNames('link-popup')}\n        onPointerEnter={() => {\n          if (!isHovering) {\n            setAppState({\n              ...appState,\n              linkState: {\n                ...appState.linkState!,\n                isHovering: true,\n              },\n            });\n          }\n        }}\n        onPointerLeave={() => {\n          if (!isEditing) {\n            setAppState({\n              ...appState,\n              linkState: {\n                ...appState.linkState!,\n                isHovering: false,\n              },\n            });\n          }\n        }}\n      >\n        <Stack.Row gap={1} align=\"center\">\n          {isEditing ? (\n            <>\n              <input\n                type=\"text\"\n                value={url}\n                onChange={(e) => {\n                  setUrl(e.target.value);\n                }}\n                onKeyDown={(e) => {\n                  if (e.key === 'Enter') {\n                    saveUrlAndExitEditing();\n                  }\n                }}\n                className=\"link-popup__input\"\n                autoFocus\n              />\n              <ToolButton\n                type=\"icon\"\n                visible={true}\n                icon={TrashIcon}\n                title={t('popupLink.delLink')}\n                aria-label={t('popupLink.delLink')}\n                onPointerDown={() => {\n                  const editor = linkState!.editor;\n                  const targetElement = linkState!.targetElement;\n                  const path = ReactEditor.findPath(editor, targetElement);\n                  Transforms.unwrapNodes(editor, {\n                    at: path,\n                  });\n                  setAppState({\n                    ...appState,\n                    linkState: null,\n                  });\n                }}\n              ></ToolButton>\n            </>\n          ) : (\n            <>\n              <a\n                href={url}\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n                className=\"link-popup__link\"\n              >\n                {url}\n              </a>\n              <ToolButton\n                className=\"link-popup__edit\"\n                type=\"icon\"\n                visible={true}\n                icon={FeltTipPenIcon}\n                title={`Edit link`}\n                aria-label={`Edit link`}\n                onPointerDown={({ event }) => {\n                  event.preventDefault();\n                  setAppState({\n                    ...appState,\n                    linkState: {\n                      ...appState.linkState!,\n                      isEditing: true,\n                    },\n                  });\n                }}\n              ></ToolButton>\n              <ToolButton\n                type=\"icon\"\n                visible={true}\n                icon={TrashIcon}\n                title={t('popupLink.delLink')}\n                aria-label={t('popupLink.delLink')}\n                onPointerDown={() => {\n                  const editor = linkState!.editor;\n                  const targetElement = linkState!.targetElement;\n                  const path = ReactEditor.findPath(editor, targetElement);\n                  Transforms.unwrapNodes(editor, {\n                    at: path,\n                  });\n                  setAppState({\n                    ...appState,\n                    linkState: null,\n                  });\n                }}\n              ></ToolButton>\n            </>\n          )}\n        </Stack.Row>\n      </Island>\n    )\n  );\n};\n"
  },
  {
    "path": "packages/drawnix/src/components/radio-group.scss",
    "content": "@import '../styles/variables.module.scss';\n\n.drawnix {\n  --RadioGroup-background: var(--island-bg-color);\n  --RadioGroup-border: var(--color-surface-high);\n\n  --RadioGroup-choice-color-off: var(--color-primary);\n  --RadioGroup-choice-color-off-hover: var(--color-brand-hover);\n  --RadioGroup-choice-background-off: var(--island-bg-color);\n  --RadioGroup-choice-background-off-active: var(--color-surface-high);\n\n  --RadioGroup-choice-color-on: var(--color-surface-lowest);\n  --RadioGroup-choice-background-on: var(--color-primary);\n  --RadioGroup-choice-background-on-hover: var(--color-brand-hover);\n  --RadioGroup-choice-background-on-active: var(--color-brand-active);\n\n  .RadioGroup {\n    box-sizing: border-box;\n    display: flex;\n    flex-direction: row;\n    align-items: flex-start;\n\n    padding: 3px;\n    border-radius: 10px;\n\n    background: var(--RadioGroup-background);\n    border: 1px solid var(--RadioGroup-border);\n\n    &__choice {\n      position: relative;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      width: 32px;\n      height: 24px;\n\n      color: var(--RadioGroup-choice-color-off);\n      background: var(--RadioGroup-choice-background-off);\n\n      border-radius: 8px;\n\n      font-family: \"Assistant\";\n      font-style: normal;\n      font-weight: 600;\n      font-size: 0.75rem;\n      line-height: 100%;\n      user-select: none;\n      letter-spacing: 0.4px;\n\n      transition: all 75ms ease-out;\n\n      &:hover {\n        color: var(--RadioGroup-choice-color-off-hover);\n      }\n\n      &:active {\n        background: var(--RadioGroup-choice-background-off-active);\n      }\n\n      &.active {\n        color: var(--RadioGroup-choice-color-on);\n        background: var(--RadioGroup-choice-background-on);\n\n        &:hover {\n          background: var(--RadioGroup-choice-background-on-hover);\n        }\n\n        &:active {\n          background: var(--RadioGroup-choice-background-on-active);\n        }\n      }\n\n      & input {\n        z-index: 1;\n        position: absolute;\n        width: 100%;\n        height: 100%;\n        margin: 0;\n        padding: 0;\n\n        border-radius: 8px;\n\n        -webkit-appearance: none;\n        -moz-appearance: none;\n        appearance: none;\n\n        cursor: pointer;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/drawnix/src/components/radio-group.tsx",
    "content": "import classNames from 'classnames';\nimport './radio-group.scss';\n\nexport type RadioGroupChoice<T> = {\n  value: T;\n  label: React.ReactNode;\n  ariaLabel?: string;\n};\n\nexport type RadioGroupProps<T> = {\n  choices: RadioGroupChoice<T>[];\n  value: T;\n  onChange: (value: T) => void;\n  name: string;\n};\n\nexport const RadioGroup = function <T>({\n  onChange,\n  value,\n  choices,\n  name,\n}: RadioGroupProps<T>) {\n  return (\n    <div className=\"RadioGroup\">\n      {choices.map((choice) => (\n        <div\n          className={classNames('RadioGroup__choice', {\n            active: choice.value === value,\n          })}\n          key={String(choice.value)}\n          title={choice.ariaLabel}\n        >\n          <input\n            name={name}\n            type=\"radio\"\n            checked={choice.value === value}\n            onChange={() => onChange(choice.value)}\n            aria-label={choice.ariaLabel}\n          />\n          {choice.label}\n        </div>\n      ))}\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/drawnix/src/components/select/select.scss",
    "content": ".drawnix {\n  /* Define local variables mapped to project theme or defaults */\n  --dx-select-trigger-height: 2rem;\n  --dx-select-content-padding: 4px;\n  --dx-select-item-height: 2rem;\n  --dx-select-item-indicator-width: 20px;\n  --dx-select-separator-margin-right: 8px;\n\n  /* Trigger Styles */\n  .dx-SelectTrigger {\n    display: inline-flex;\n    align-items: center;\n    justify-content: space-between;\n    flex-shrink: 0;\n    user-select: none;\n    vertical-align: top;\n    height: var(--dx-select-trigger-height);\n    box-sizing: border-box;\n    font-family: inherit;\n    font-size: 14px;\n    line-height: 1;\n    text-align: left;\n    cursor: default;\n    background-color: var(--island-bg-color);\n    color: var(--color-text-primary);\n    border: 1px solid var(--island-border-color);\n    border-radius: var(--border-radius-md);\n    padding: 0 var(--dx-space-2);\n    gap: var(--dx-space-2);\n    outline: none;\n\n    &:hover {\n      background-color: var(--color-gray-10);\n      border-color: var(--color-gray-40);\n    }\n\n    &[data-state='open'] {\n      border-color: var(--color-primary);\n      box-shadow: 0 0 0 1px var(--color-primary);\n    }\n\n    &:focus-visible {\n      border-color: var(--color-primary);\n      box-shadow: 0 0 0 1px var(--color-primary);\n    }\n\n    &[data-disabled] {\n      color: var(--color-text-disabled);\n      background-color: var(--color-bg-disabled);\n      cursor: not-allowed;\n    }\n\n    &.dx-variant-ghost {\n      background-color: transparent;\n      border-color: transparent;\n      &:hover {\n        background-color: var(--color-gray-10);\n      }\n    }\n  }\n\n  .dx-SelectTriggerInner {\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    flex: 1;\n  }\n\n  .dx-SelectIcon {\n    flex-shrink: 0;\n    color: var(--color-text-secondary);\n    display: flex;\n    align-items: center;\n    \n    svg {\n      display: block;\n    }\n  }\n\n  /* Content Styles */\n  .dx-SelectContent {\n    box-sizing: border-box;\n    background-color: var(--island-bg-color);\n    border: 1px solid var(--island-border-color);\n    box-shadow: var(--shadow-island);\n    border-radius: var(--border-radius-lg);\n    padding: var(--dx-select-content-padding);\n    min-width: var(--radix-select-trigger-width); /* Note: Floating UI might handle width */\n    max-height: 300px;\n    overflow: hidden;\n    z-index: 1000;\n    display: flex;\n    flex-direction: column;\n\n    &:focus-visible {\n      outline: 2px solid var(--color-primary);\n      outline-offset: 2px;\n    }\n  }\n\n  .dx-SelectContent[data-hide-selected-indicator] {\n    .dx-SelectItem {\n      padding-left: var(--dx-space-2);\n    }\n\n    .dx-SelectItemIndicator {\n      display: none;\n    }\n  }\n\n  .dx-SelectViewport {\n    overflow-y: auto;\n    padding: 0;\n    flex: 1;\n  }\n\n  /* Item Styles */\n  .dx-SelectItem {\n    display: flex;\n    align-items: center;\n    width: 100%;\n    height: var(--dx-select-item-height);\n    padding: 0 var(--dx-space-2) 0 var(--dx-select-item-indicator-width);\n    position: relative;\n    box-sizing: border-box;\n    outline: none;\n    user-select: none;\n    cursor: pointer;\n    border: none;\n    background: transparent;\n    color: var(--color-text-primary);\n    font-size: 14px;\n    border-radius: var(--dx-radius-2);\n    text-align: center;\n\n    &[data-highlighted] {\n      background-color: var(--color-primary);\n      color: var(--color-icon-white);\n      \n      .dx-SelectItemIndicator {\n        color: var(--color-icon-white);\n      }\n    }\n\n    &[data-disabled] {\n      color: var(--color-text-disabled);\n      cursor: not-allowed;\n    }\n  }\n\n  .dx-SelectItemIndicator {\n    position: absolute;\n    left: 0;\n    width: var(--dx-select-item-indicator-width);\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    color: var(--color-primary);\n  }\n  \n  .dx-SelectItemText {\n    flex: 1;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    text-align: inherit;\n  }\n\n  /* Label, Group, Separator */\n  .dx-SelectLabel {\n    display: flex;\n    align-items: center;\n    height: var(--dx-select-item-height);\n    padding-left: var(--dx-select-item-indicator-width);\n    padding-right: var(--dx-space-2);\n    color: var(--color-text-secondary);\n    font-size: 12px;\n    font-weight: 500;\n    user-select: none;\n    cursor: default;\n  }\n\n  .dx-SelectSeparator {\n    height: 1px;\n    background-color: var(--island-border-color);\n    margin: var(--dx-space-1) 0;\n  }\n\n  /* Size Variants */\n  .dx-r-size-1 {\n    --dx-select-trigger-height: 24px;\n    --dx-select-item-height: 24px;\n    font-size: 12px;\n    \n    &.dx-SelectItem {\n       font-size: 12px;\n    }\n  }\n  \n  .dx-r-size-2 {\n    --dx-select-trigger-height: 32px;\n    --dx-select-item-height: 28px; /* Radix often uses slightly smaller items */\n    font-size: 14px;\n  }\n  \n  .dx-r-size-3 {\n    --dx-select-trigger-height: 40px;\n    --dx-select-item-height: 32px;\n    font-size: 16px;\n    \n    &.dx-SelectItem {\n       font-size: 16px;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/drawnix/src/components/select/select.tsx",
    "content": "import * as React from 'react';\nimport classNames from 'classnames';\nimport {\n  autoUpdate,\n  flip,\n  FloatingFocusManager,\n  FloatingList,\n  FloatingPortal,\n  offset,\n  Placement,\n  shift,\n  useClick,\n  useDismiss,\n  useFloating,\n  useInteractions,\n  useListItem,\n  useListNavigation,\n  useMergeRefs,\n  useRole,\n  useTypeahead,\n} from '@floating-ui/react';\nimport { ChevronDownIcon, ThickCheckIcon } from '../icons';\nimport './select.scss';\n\ntype SelectValueType = string;\n\ninterface SelectContextType {\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  value: SelectValueType | undefined;\n  setValue: (value: SelectValueType) => void;\n  activeIndex: number | null;\n  setActiveIndex: (index: number | null) => void;\n  selectedIndex: number | null;\n  elementsRef: React.MutableRefObject<Array<HTMLElement | null>>;\n  labelsRef: React.MutableRefObject<Array<string | null>>;\n  valuesRef: React.MutableRefObject<Array<SelectValueType | null>>;\n  getReferenceProps: ReturnType<typeof useInteractions>['getReferenceProps'];\n  getFloatingProps: ReturnType<typeof useInteractions>['getFloatingProps'];\n  getItemProps: ReturnType<typeof useInteractions>['getItemProps'];\n  refs: ReturnType<typeof useFloating>['refs'];\n  floatingStyles: React.CSSProperties;\n  floatingContext: ReturnType<typeof useFloating>['context'];\n  size: '1' | '2' | '3';\n  hideSelectedIndicator: boolean;\n  disableItemHoverHighlight: boolean;\n}\n\nconst SelectContext = React.createContext<SelectContextType | null>(null);\n\nconst useSelectContext = () => {\n  const context = React.useContext(SelectContext);\n  if (!context) {\n    throw new Error('Select components must be wrapped in <Select.Root />');\n  }\n  return context;\n};\n\ninterface SelectRootProps {\n  children: React.ReactNode;\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  size?: '1' | '2' | '3';\n  disabled?: boolean;\n  placement?: Placement;\n  sideOffset?: number;\n  hideSelectedIndicator?: boolean;\n  disableItemHoverHighlight?: boolean;\n  disableInitialHighlight?: boolean;\n  disableTypeahead?: boolean;\n}\n\nconst SelectRoot: React.FC<SelectRootProps> = ({\n  children,\n  value: controlledValue,\n  defaultValue,\n  onValueChange,\n  open: controlledOpen,\n  defaultOpen = false,\n  onOpenChange: setControlledOpen,\n  size = '2',\n  disabled = false,\n  placement = 'bottom-start',\n  sideOffset = 4,\n  hideSelectedIndicator = false,\n  disableItemHoverHighlight = false,\n  disableInitialHighlight = false,\n  disableTypeahead = false,\n}) => {\n  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen);\n  const open = controlledOpen ?? uncontrolledOpen;\n  const setOpen = setControlledOpen ?? setUncontrolledOpen;\n\n  const [uncontrolledValue, setUncontrolledValue] = React.useState<\n    string | undefined\n  >(defaultValue);\n  const value = controlledValue ?? uncontrolledValue;\n  const setValue = React.useCallback(\n    (nextValue: string) => {\n      if (controlledValue === undefined) {\n        setUncontrolledValue(nextValue);\n      }\n      onValueChange?.(nextValue);\n    },\n    [controlledValue, onValueChange]\n  );\n\n  const elementsRef = React.useRef<Array<HTMLElement | null>>([]);\n  const labelsRef = React.useRef<Array<string | null>>([]);\n  const valuesRef = React.useRef<Array<SelectValueType | null>>([]);\n  const [activeIndex, setActiveIndex] = React.useState<number | null>(null);\n\n  const selectedIndex = React.useMemo(() => {\n    if (value == null) return null;\n    const index = valuesRef.current.findIndex((v) => v === value);\n    return index >= 0 ? index : null;\n  }, [value]);\n\n  const navigationSelectedIndex = disableInitialHighlight ? null : selectedIndex;\n\n  React.useEffect(() => {\n    if (!open) return;\n    if (disableInitialHighlight) {\n      setActiveIndex(null);\n      return;\n    }\n    setActiveIndex(selectedIndex ?? 0);\n  }, [open, selectedIndex, disableInitialHighlight]);\n\n  const { refs, floatingStyles, context } = useFloating({\n    placement,\n    open,\n    onOpenChange: setOpen,\n    whileElementsMounted: autoUpdate,\n    middleware: [\n      offset(sideOffset),\n      flip({ padding: 5 }),\n      shift({ padding: 5 }),\n    ],\n  });\n\n  const click = useClick(context, { enabled: !disabled && controlledOpen === undefined });\n  const dismiss = useDismiss(context);\n  const role = useRole(context, { role: 'listbox' });\n\n  const listNavigation = useListNavigation(context, {\n    listRef: elementsRef,\n    activeIndex,\n    selectedIndex: navigationSelectedIndex,\n    onNavigate: setActiveIndex,\n    loop: true,\n    focusItemOnHover: !disableItemHoverHighlight,\n  });\n\n  const typeahead = useTypeahead(context, {\n    listRef: labelsRef,\n    activeIndex,\n    selectedIndex: navigationSelectedIndex,\n    onMatch: setActiveIndex,\n  });\n\n  const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([\n    click,\n    dismiss,\n    role,\n    listNavigation,\n    disableTypeahead ? ({} as any) : typeahead,\n  ]);\n\n  const contextValue = React.useMemo(\n    () => ({\n      open,\n      setOpen,\n      value,\n      setValue,\n      activeIndex,\n      setActiveIndex,\n      selectedIndex,\n      elementsRef,\n      labelsRef,\n      valuesRef,\n      getReferenceProps,\n      getFloatingProps,\n      getItemProps,\n      refs,\n      floatingStyles,\n      floatingContext: context,\n      size,\n      hideSelectedIndicator,\n      disableItemHoverHighlight,\n    }),\n    [\n      open,\n      setOpen,\n      value,\n      setValue,\n      activeIndex,\n      selectedIndex,\n      getReferenceProps,\n      getFloatingProps,\n      getItemProps,\n      refs,\n      floatingStyles,\n      context,\n      size,\n      hideSelectedIndicator,\n      disableItemHoverHighlight,\n    ]\n  );\n\n  return (\n    <SelectContext.Provider value={contextValue}>\n      {children}\n    </SelectContext.Provider>\n  );\n};\nSelectRoot.displayName = 'Select.Root';\n\ninterface SelectTriggerProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n  variant?: 'classic' | 'surface' | 'soft' | 'ghost';\n  color?: string;\n  radius?: 'none' | 'small' | 'medium' | 'large' | 'full';\n  placeholder?: string;\n  asChild?: boolean;\n}\n\nconst SelectTrigger = React.forwardRef<HTMLButtonElement, SelectTriggerProps>(\n  (\n    {\n      children,\n      className,\n      variant = 'surface',\n      color,\n      radius,\n      placeholder,\n      asChild,\n      ...props\n    },\n    forwardedRef\n  ) => {\n    const context = useSelectContext();\n    const mergedRef = useMergeRefs([context.refs.setReference, forwardedRef]);\n\n    if (asChild && React.isValidElement(children)) {\n      return React.cloneElement(children, {\n        ref: mergedRef,\n        ...context.getReferenceProps(props),\n        'data-state': context.open ? 'open' : 'closed',\n      } as any);\n    }\n\n    // Value rendering logic\n    let content = children;\n    if (!content && context.value) {\n       // Find label for value\n       const index = context.valuesRef.current.indexOf(context.value);\n       if (index !== -1) {\n         content = context.labelsRef.current[index];\n       } else {\n         content = context.value;\n       }\n    }\n    \n    const shouldShowPlaceholder = !content && placeholder;\n    const displayContent = shouldShowPlaceholder ? placeholder : content;\n\n    return (\n      <button\n        type=\"button\"\n        ref={mergedRef}\n        className={classNames(\n          'dx-reset',\n          'dx-SelectTrigger',\n          `dx-r-size-${context.size}`,\n          `dx-variant-${variant}`,\n          className\n        )}\n        data-state={context.open ? 'open' : 'closed'}\n        data-placeholder={shouldShowPlaceholder ? '' : undefined}\n        {...context.getReferenceProps(props)}\n      >\n        <span className=\"dx-SelectTriggerInner\">{displayContent}</span>\n        <span className=\"dx-SelectIcon\">\n          {ChevronDownIcon}\n        </span>\n      </button>\n    );\n  }\n);\nSelectTrigger.displayName = 'Select.Trigger';\n\ninterface SelectContentProps extends React.HTMLAttributes<HTMLDivElement> {\n  variant?: 'solid' | 'soft';\n  color?: string;\n  highContrast?: boolean;\n  container?: HTMLElement | null;\n}\n\nconst SelectContent = React.forwardRef<HTMLDivElement, SelectContentProps>(\n  (\n    {\n      children,\n      className,\n      variant = 'solid',\n      color,\n      highContrast,\n      container,\n      style,\n      ...props\n    },\n    forwardedRef\n  ) => {\n    const context = useSelectContext();\n    const mergedRef = useMergeRefs([context.refs.setFloating, forwardedRef]);\n\n    if (!context.open) return null;\n\n    return (\n      <FloatingPortal root={container}>\n        <FloatingFocusManager context={context.floatingContext} initialFocus={-1}>\n          <div\n            ref={mergedRef}\n            className={classNames(\n              'dx-SelectContent',\n              `dx-r-size-${context.size}`,\n              `dx-variant-${variant}`,\n              className\n            )}\n            data-hide-selected-indicator={context.hideSelectedIndicator ? '' : undefined}\n            style={{ ...context.floatingStyles, ...style }}\n            {...context.getFloatingProps(props)}\n          >\n            <FloatingList elementsRef={context.elementsRef} labelsRef={context.labelsRef}>\n              <div className=\"dx-SelectViewport\">\n                 {children}\n              </div>\n            </FloatingList>\n          </div>\n        </FloatingFocusManager>\n      </FloatingPortal>\n    );\n  }\n);\nSelectContent.displayName = 'Select.Content';\n\ninterface SelectItemProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n  value: string;\n  textValue?: string;\n}\n\nconst SelectItem = React.forwardRef<HTMLButtonElement, SelectItemProps>(\n  ({ children, className, value, textValue, disabled, ...props }, forwardedRef) => {\n    const context = useSelectContext();\n    const { ref: itemRef, index } = useListItem({\n      label: textValue ?? (typeof children === 'string' ? children : value),\n    });\n    const mergedRef = useMergeRefs([itemRef, forwardedRef]);\n\n    const isActive = context.activeIndex === index;\n    const isSelected = context.value === value;\n\n    React.useEffect(() => {\n        if (index !== null) {\n            context.valuesRef.current[index] = value;\n            // Best effort to get label\n            context.labelsRef.current[index] = textValue ?? (typeof children === 'string' ? children : value);\n        }\n    }, [index, value, textValue, children, context.valuesRef, context.labelsRef]);\n\n    const handleSelect = () => {\n      context.setValue(value);\n      context.setOpen(false);\n    };\n\n    const mergedItemProps = context.getItemProps({\n      ...props,\n      onClick: (e: React.MouseEvent<HTMLElement>) => {\n        props.onClick?.(e as React.MouseEvent<HTMLButtonElement>);\n        handleSelect();\n      },\n      onKeyDown: (e: React.KeyboardEvent<HTMLElement>) => {\n        props.onKeyDown?.(e as React.KeyboardEvent<HTMLButtonElement>);\n        if (e.key === 'Enter') {\n          e.preventDefault();\n          handleSelect();\n        }\n      },\n    });\n\n    const {\n      onPointerMove,\n      onMouseMove,\n      onMouseEnter,\n      onMouseLeave,\n      ...restMergedItemProps\n    } = mergedItemProps as React.ButtonHTMLAttributes<HTMLButtonElement>;\n\n    return (\n      <button\n        ref={mergedRef}\n        type=\"button\"\n        role=\"option\"\n        aria-selected={isSelected}\n        data-highlighted={isActive ? '' : undefined}\n        data-state={isSelected ? 'checked' : 'unchecked'}\n        data-disabled={disabled ? '' : undefined}\n        tabIndex={isActive ? 0 : -1}\n        className={classNames('dx-SelectItem', className)}\n        disabled={disabled}\n        {...restMergedItemProps}\n        {...(context.disableItemHoverHighlight\n          ? {}\n          : { onPointerMove, onMouseMove, onMouseEnter, onMouseLeave })}\n      >\n        {!context.hideSelectedIndicator && (\n          <span className=\"dx-SelectItemIndicator\">\n            {isSelected && ThickCheckIcon}\n          </span>\n        )}\n        <span className=\"dx-SelectItemText\">{children}</span>\n      </button>\n    );\n  }\n);\nSelectItem.displayName = 'Select.Item';\n\ntype SelectGroupProps = React.HTMLAttributes<HTMLDivElement>\nconst SelectGroup = React.forwardRef<HTMLDivElement, SelectGroupProps>(\n  ({ className, ...props }, ref) => (\n    <div ref={ref} className={classNames('dx-SelectGroup', className)} {...props} />\n  )\n);\nSelectGroup.displayName = 'Select.Group';\n\ntype SelectLabelProps = React.HTMLAttributes<HTMLDivElement>\nconst SelectLabel = React.forwardRef<HTMLDivElement, SelectLabelProps>(\n  ({ className, ...props }, ref) => (\n    <div ref={ref} className={classNames('dx-SelectLabel', className)} {...props} />\n  )\n);\nSelectLabel.displayName = 'Select.Label';\n\ntype SelectSeparatorProps = React.HTMLAttributes<HTMLDivElement>\nconst SelectSeparator = React.forwardRef<HTMLDivElement, SelectSeparatorProps>(\n  ({ className, ...props }, ref) => (\n    <div ref={ref} className={classNames('dx-SelectSeparator', className)} {...props} />\n  )\n);\nSelectSeparator.displayName = 'Select.Separator';\n\nexport const Select = {\n  Root: SelectRoot,\n  Trigger: SelectTrigger,\n  Content: SelectContent,\n  Item: SelectItem,\n  Group: SelectGroup,\n  Label: SelectLabel,\n  Separator: SelectSeparator,\n};\n"
  },
  {
    "path": "packages/drawnix/src/components/shape-picker.tsx",
    "content": "import classNames from 'classnames';\nimport { Island } from './island';\nimport Stack from './stack';\nimport { ToolButton } from './tool-button';\nimport {\n  RectangleIcon,\n  EllipseIcon,\n  TriangleIcon,\n  DiamondIcon,\n  ParallelogramIcon,\n  RoundRectangleIcon,\n  TerminalIcon,\n  NoteCurlyLeftIcon,\n  NoteCurlyRightIcon,\n} from './icons';\nimport { BoardTransforms, PlaitBoard } from '@plait/core';\nimport React from 'react';\nimport { BoardCreationMode, setCreationMode } from '@plait/common';\nimport { BasicShapes, DrawPointerType, FlowchartSymbols } from '@plait/draw';\nimport { useBoard } from '@plait-board/react-board';\nimport { Translations, useI18n } from '../i18n';\nimport { splitRows } from '../utils/common';\n\nexport interface ShapeProps {\n  icon: React.ReactNode;\n  title: string;\n  pointer: DrawPointerType;\n}\n\nexport const SHAPES: ShapeProps[] = [\n  {\n    icon: RectangleIcon,\n    title: 'toolbar.shape.rectangle',\n    pointer: BasicShapes.rectangle,\n  },\n  {\n    icon: EllipseIcon,\n    title: 'toolbar.shape.ellipse',\n    pointer: BasicShapes.ellipse,\n  },\n  {\n    icon: TriangleIcon,\n    title: 'toolbar.shape.triangle',\n    pointer: BasicShapes.triangle,\n  },\n  {\n    icon: TerminalIcon,\n    title: 'toolbar.shape.terminal',\n    pointer: FlowchartSymbols.terminal,\n  },\n  {\n    icon: NoteCurlyRightIcon,\n    title: 'toolbar.shape.noteCurlyRight',\n    pointer: FlowchartSymbols.noteCurlyRight,\n  },\n  {\n    icon: NoteCurlyLeftIcon,\n    title: 'toolbar.shape.noteCurlyLeft',\n    pointer: FlowchartSymbols.noteCurlyLeft,\n  },\n  {\n    icon: DiamondIcon,\n    title: 'toolbar.shape.diamond',\n    pointer: BasicShapes.diamond,\n  },\n  {\n    icon: ParallelogramIcon,\n    title: 'toolbar.shape.parallelogram',\n    pointer: BasicShapes.parallelogram,\n  },\n  {\n    icon: RoundRectangleIcon,\n    title: 'toolbar.shape.roundRectangle',\n    pointer: BasicShapes.roundRectangle,\n  },\n];\n\nconst ROW_SHAPES = splitRows(SHAPES, 5);\n\nexport type ShapePickerProps = {\n  onPointerUp: (pointer: DrawPointerType) => void;\n};\n\nexport const ShapePicker: React.FC<ShapePickerProps> = ({ onPointerUp }) => {\n  const board = useBoard();\n  const { t } = useI18n();\n  return (\n    <Island padding={1}>\n      <Stack.Col gap={1}>\n        {ROW_SHAPES.map((rowShapes, rowIndex) => {\n          return (\n            <Stack.Row gap={1} key={rowIndex}>\n              {rowShapes.map((shape, index) => {\n                return (\n                  <ToolButton\n                    key={index}\n                    className={classNames({ fillable: false })}\n                    type=\"icon\"\n                    size={'small'}\n                    visible={true}\n                    selected={PlaitBoard.isPointer(board, shape.pointer)}\n                    icon={shape.icon}\n                    title={t(\n                      (shape.title || 'toolbar.shape') as keyof Translations\n                    )}\n                    aria-label={t(\n                      (shape.title || 'toolbar.shape') as keyof Translations\n                    )}\n                    onPointerDown={() => {\n                      setCreationMode(board, BoardCreationMode.dnd);\n                      BoardTransforms.updatePointerType(board, shape.pointer);\n                    }}\n                    onPointerUp={() => {\n                      setCreationMode(board, BoardCreationMode.drawing);\n                      onPointerUp(shape.pointer);\n                    }}\n                  />\n                );\n              })}\n            </Stack.Row>\n          );\n        })}\n      </Stack.Col>\n    </Island>\n  );\n};\n"
  },
  {
    "path": "packages/drawnix/src/components/size-slider.scss",
    "content": "@import \"open-color/open-color.scss\";\n\n.slider-container {\n    padding: 10px 0px;\n    &.disabled {\n        opacity: 50%;\n        cursor: not-allowed;\n    \n        .slider-track,.slider-thumb {\n            cursor: not-allowed;\n        }\n    }\n    .slider-track {\n        position: relative;\n        height: 4px;\n        background-color: var(--color-gray-20);\n        border-radius: 2px;\n        cursor: pointer;\n    }\n    .slider-range {\n        position: absolute;\n        height: 100%;\n        background-color: var(--color-primary);\n        border-radius: 3px;\n    }\n    .slider-thumb {\n        position: absolute;\n        width: 12px;\n        height: 12px;\n        background-color: $oc-white;\n        border: 2px solid var(--color-primary);\n        border-radius: 50%;\n        top: 50%;\n        transform: translate(-50%, -50%);\n        cursor: grab;\n        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1),\n    }\n}\n"
  },
  {
    "path": "packages/drawnix/src/components/size-slider.tsx",
    "content": "import React, {\n  useState,\n  useRef,\n  useCallback,\n  useEffect,\n  useMemo,\n} from 'react';\nimport { toFixed } from '@plait/core';\nimport './size-slider.scss';\nimport classNames from 'classnames';\nimport { throttle } from 'lodash';\n\ninterface SliderProps {\n  min?: number;\n  max?: number;\n  step?: number;\n  defaultValue?: number;\n  disabled?: boolean;\n  title?: string;\n  onChange?: (value: number) => void;\n  beforeStart?: () => void;\n  afterEnd?: () => void;\n}\n\nexport const SizeSlider: React.FC<SliderProps> = ({\n  min = 0,\n  max = 100,\n  step = 1,\n  defaultValue = 100,\n  disabled = false,\n  onChange,\n  beforeStart,\n  afterEnd,\n  title,\n}) => {\n  const [isDragging, setIsDragging] = useState(false);\n  const [value, setValue] = useState(defaultValue);\n  const sliderRef = useRef<HTMLDivElement>(null);\n  const thumbRef = useRef<HTMLDivElement>(null);\n  const percentage = ((value - min) / (max - min)) * 100;\n\n  const handleSliderChange = useCallback(\n    throttle(\n      (event: React.MouseEvent<HTMLDivElement> | MouseEvent) => {\n        if (sliderRef.current && thumbRef.current) {\n          const sliderRect = sliderRef.current.getBoundingClientRect();\n          const thumbRect = thumbRef.current.getBoundingClientRect();\n          const x = event.clientX - sliderRect.left;\n          const thumbPercentage = toFixed(\n            (thumbRect.width / 2 / sliderRect.width) * 100\n          );\n          let percentage = Math.min(Math.max(x / sliderRect.width, 0), 1);\n          if (percentage >= (100 - thumbPercentage) / 100) {\n            percentage = 1;\n          } else if (percentage <= thumbPercentage / 100) {\n            percentage = 0;\n          }\n          const newValue =\n            Math.round((percentage * (max - min)) / step) * step + min;\n          setValue(newValue);\n          onChange && onChange(newValue);\n        }\n      },\n      50,\n      { leading: true, trailing: true }\n    ),\n    [min, max, step, onChange]\n  );\n\n  const handlePointerDown = useCallback(() => {\n    const handleMouseMove = (e: MouseEvent) => {\n      setIsDragging(true);\n      handleSliderChange(e);\n    };\n    const handleMouseUp = () => {\n      document.removeEventListener('pointermove', handleMouseMove);\n      document.removeEventListener('pointerup', handleMouseUp);\n      afterEnd && afterEnd();\n      setTimeout(() => {\n        setIsDragging(false);\n      }, 0);\n    };\n\n    document.addEventListener('pointermove', handleMouseMove);\n    document.addEventListener('pointerup', handleMouseUp);\n  }, [handleSliderChange]);\n\n  useEffect(()=>{\n    setValue(defaultValue)\n  },[defaultValue])\n\n  return (\n    <div\n      data-tooltip\n      title={title}\n      className={classNames('slider-container', { disabled: disabled })}\n    >\n      <div\n        ref={sliderRef}\n        className=\"slider-track\"\n        onClick={(event) => {\n          if (disabled || isDragging) {\n            return;\n          }\n          handleSliderChange(event);\n        }}\n        onPointerDown={(event) => {\n          event.preventDefault();\n          if (disabled) {\n            return;\n          }\n          beforeStart && beforeStart();\n          handlePointerDown();\n        }}\n      >\n        <div\n          className=\"slider-range\"\n          style={{\n            width: `${percentage}%`,\n          }}\n        />\n        <div\n          ref={thumbRef}\n          className=\"slider-thumb\"\n          style={{\n            left: `${percentage}%`,\n          }}\n        />\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/drawnix/src/components/stack.scss",
    "content": ".drawnix {\n  .stack {\n    --gap: 0;\n    display: grid;\n    gap: calc(var(--space-factor) * var(--gap));\n  }\n\n  .stack_vertical {\n    grid-template-columns: auto;\n    grid-auto-flow: row;\n    grid-auto-rows: min-content;\n  }\n\n  .stack_horizontal {\n    grid-template-rows: auto;\n    grid-auto-flow: column;\n    grid-auto-columns: min-content;\n  }\n}\n"
  },
  {
    "path": "packages/drawnix/src/components/stack.tsx",
    "content": "// Credits to excalidraw\nimport \"./stack.scss\";\n\nimport React, { forwardRef } from \"react\";\nimport clsx from \"classnames\";\n\ntype StackProps = {\n  children: React.ReactNode;\n  gap?: number;\n  align?: \"start\" | \"center\" | \"end\" | \"baseline\";\n  justifyContent?: \"center\" | \"space-around\" | \"space-between\";\n  className?: string | boolean;\n  style?: React.CSSProperties;\n  ref: React.RefObject<HTMLDivElement>;\n};\n\nconst RowStack = forwardRef(\n  (\n    { children, gap, align, justifyContent, className, style }: StackProps,\n    ref: React.ForwardedRef<HTMLDivElement>,\n  ) => {\n    return (\n      <div\n        className={clsx(\"stack stack_horizontal\", className)}\n        style={{\n          \"--gap\": gap,\n          alignItems: align,\n          justifyContent,\n          ...style,\n        }}\n        ref={ref}\n      >\n        {children}\n      </div>\n    );\n  },\n);\n\nconst ColStack = forwardRef(\n  (\n    { children, gap, align, justifyContent, className, style }: StackProps,\n    ref: React.ForwardedRef<HTMLDivElement>,\n  ) => {\n    return (\n      <div\n        className={clsx(\"stack stack_vertical\", className)}\n        style={{\n          \"--gap\": gap,\n          justifyItems: align,\n          justifyContent,\n          ...style,\n        }}\n        ref={ref}\n      >\n        {children}\n      </div>\n    );\n  },\n);\n\nexport default {\n  Row: RowStack,\n  Col: ColStack,\n};\n"
  },
  {
    "path": "packages/drawnix/src/components/tool-button.tsx",
    "content": "// Credits to excalidraw\nimport './tool-icon.scss';\n\nimport type { CSSProperties } from 'react';\nimport React, { useEffect, useRef, useState } from 'react';\nimport { AbortError } from '../errors';\nimport { isPromiseLike } from '../utils/common';\nimport classNames from 'classnames';\nimport { EventPointerType } from '../types';\n\nexport type ToolButtonSize = 'small' | 'medium';\n\ntype ToolButtonBaseProps = {\n  icon?: React.ReactNode;\n  'aria-label': string;\n  'aria-keyshortcuts'?: string;\n  'data-testid'?: string;\n  label?: string;\n  title?: string;\n  name?: string;\n  id?: string;\n  size?: ToolButtonSize;\n  keyBindingLabel?: string | null;\n  showAriaLabel?: boolean;\n  hidden?: boolean;\n  visible?: boolean;\n  selected?: boolean;\n  disabled?: boolean;\n  className?: string;\n  style?: CSSProperties;\n  onPointerDown?(data: {\n    pointerType: EventPointerType;\n    event: React.PointerEvent<HTMLElement>;\n  }): void;\n  onPointerUp?(data: { pointerType: EventPointerType }): void;\n};\n\ntype ToolButtonProps =\n  | (ToolButtonBaseProps & {\n      type: 'button';\n      children?: React.ReactNode;\n      onClick?(event: React.MouseEvent): void;\n    })\n  | (ToolButtonBaseProps & {\n      type: 'submit';\n      children?: React.ReactNode;\n      onClick?(event: React.MouseEvent): void;\n    })\n  | (ToolButtonBaseProps & {\n      type: 'icon';\n      children?: React.ReactNode;\n      onClick?(): void;\n    })\n  | (ToolButtonBaseProps & {\n      type: 'radio';\n      checked: boolean;\n      onChange?(data: { pointerType: EventPointerType | null }): void;\n    });\n\nexport const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {\n  const { id: drawnixId } = { id: 'drawnix' };\n  const innerRef = React.useRef(null);\n  React.useImperativeHandle(ref, () => innerRef.current);\n  const sizeCn = `tool-icon_size_${props.size || 'medium'}`;\n\n  const [isLoading, setIsLoading] = useState(false);\n\n  const isMountedRef = useRef(true);\n\n  const onClick = async (event: React.MouseEvent) => {\n    const ret = 'onClick' in props && props.onClick?.(event);\n\n    if (isPromiseLike(ret)) {\n      try {\n        setIsLoading(true);\n        await ret;\n      } catch (error: any) {\n        if (!(error instanceof AbortError)) {\n          throw error;\n        } else {\n          console.warn(error);\n        }\n      } finally {\n        if (isMountedRef.current) {\n          setIsLoading(false);\n        }\n      }\n    }\n  };\n\n  useEffect(() => {\n    isMountedRef.current = true;\n    return () => {\n      isMountedRef.current = false;\n    };\n  }, []);\n\n  const lastPointerTypeRef = useRef<EventPointerType | null>(null);\n\n  if (\n    props.type === 'button' ||\n    props.type === 'icon' ||\n    props.type === 'submit'\n  ) {\n    const type = (props.type === 'icon' ? 'button' : props.type) as\n      | 'button'\n      | 'submit';\n    return (\n      <button\n        className={classNames(\n          'tool-icon_type_button',\n          sizeCn,\n          props.className,\n          props.visible && !props.hidden\n            ? 'tool-icon_type_button--show'\n            : 'tool-icon_type_button--hide',\n          {\n            'tool-icon': !props.hidden,\n            'tool-icon--selected': props.selected,\n          }\n        )}\n        style={props.style}\n        data-testid={props['data-testid']}\n        hidden={props.hidden}\n        title={props.title}\n        aria-label={props['aria-label']}\n        type={type}\n        onClick={onClick}\n        onPointerDown={(event) => {\n          props.onPointerDown?.({\n            pointerType: event.pointerType || null,\n            event,\n          });\n        }}\n        onPointerUp={(event) => {\n          props.onPointerUp?.({ pointerType: event.pointerType || null });\n        }}\n        ref={innerRef}\n        disabled={isLoading || !!props.disabled}\n      >\n        {(props.icon || props.label) && (\n          <div\n            className=\"tool-icon__icon\"\n            aria-hidden=\"true\"\n            aria-disabled={!!props.disabled}\n          >\n            {props.icon || props.label}\n            {props.keyBindingLabel && (\n              <span className=\"tool-icon__keybinding\">\n                {props.keyBindingLabel}\n              </span>\n            )}\n          </div>\n        )}\n        {props.showAriaLabel && (\n          <div className=\"tool-icon__label\">{props['aria-label']}</div>\n        )}\n        {props.children && (\n          <div className=\"tool-icon__icon\">{props.children}</div>\n        )}\n      </button>\n    );\n  }\n\n  return (\n    <label\n      className={classNames('tool-icon', props.className)}\n      title={props.title}\n      onPointerDown={(event) => {\n        lastPointerTypeRef.current = event.pointerType || null;\n        props.onPointerDown?.({\n          pointerType: event.pointerType || null,\n          event,\n        });\n      }}\n      onPointerUp={(event) => {\n        props.onPointerUp?.({ pointerType: event.pointerType || null });\n        requestAnimationFrame(() => {\n          lastPointerTypeRef.current = null;\n        });\n      }}\n    >\n      <input\n        className={`tool-icon_type_radio ${sizeCn}`}\n        type=\"radio\"\n        name={props.name}\n        aria-label={props['aria-label']}\n        aria-keyshortcuts={props['aria-keyshortcuts']}\n        data-testid={props['data-testid']}\n        id={`${drawnixId}-${props.id}`}\n        onChange={() => {\n          props.onChange?.({ pointerType: lastPointerTypeRef.current });\n        }}\n        checked={props.checked}\n        ref={innerRef}\n      />\n      <div className=\"tool-icon__icon\">\n        {props.icon}\n        {props.keyBindingLabel && (\n          <span className=\"tool-icon__keybinding\">{props.keyBindingLabel}</span>\n        )}\n      </div>\n    </label>\n  );\n});\n\nToolButton.displayName = 'ToolButton';\n"
  },
  {
    "path": "packages/drawnix/src/components/tool-icon.scss",
    "content": "@import \"open-color/open-color.scss\";\n@import \"../styles/variables.module.scss\";\n\n.drawnix {\n  .tool-icon {\n    border-radius: var(--border-radius-md);\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    position: relative;\n    cursor: pointer;\n    -webkit-tap-highlight-color: transparent;\n    user-select: none;\n\n    &__hidden {\n      display: none !important;\n    }\n\n    @include toolbarButtonColorStates;\n  }\n\n  .tool-icon--plain {\n    background-color: transparent;\n    .tool-icon__icon {\n      width: 2rem;\n      height: 2rem;\n    }\n  }\n\n  .tool-icon_type_radio,\n  .tool-icon_type_checkbox {\n    position: absolute;\n    opacity: 0;\n    pointer-events: none;\n  }\n\n  .tool-icon__icon {\n    box-sizing: border-box;\n    width: var(--lg-button-size);\n    height: var(--lg-button-size);\n    color: var(--icon-fill-color);\n\n    display: flex;\n    justify-content: center;\n    align-items: center;\n\n    border-radius: var(--border-radius-md);\n\n    & + .tool-icon__label {\n      margin-inline-start: 0;\n    }\n\n    svg {\n      stroke: currentColor;\n      position: relative;\n      width: var(--lg-icon-size);\n      height: var(--lg-icon-size);\n      outline: none;\n    }\n  }\n\n  .tool-icon_type_button {\n    padding: 0;\n    border: none;\n    margin: 0;\n    font-size: inherit;\n    background-color: initial;\n\n    &:focus-visible {\n      box-shadow: 0 0 0 2px var(--color-primary);\n      outline: none;\n    }\n\n    &.tool-icon--selected {\n      background: var(--color-surface-primary-container);\n      svg {\n        color: var(--color-on-primary-container);\n      }\n    }\n\n    &:active {\n      background-color: var(--button-gray-3);\n    }\n\n    &:disabled {\n      cursor: default;\n\n      &:active,\n      &:focus-visible,\n      &:hover {\n        background-color: initial;\n        border: none;\n        box-shadow: none;\n      }\n\n      svg {\n        color: var(--color-disabled);\n      }\n    }\n\n    &--show {\n      visibility: visible;\n    }\n\n    &--hide {\n      display: none !important;\n    }\n  }\n\n  .tool-icon__label {\n    display: flex;\n    align-items: center;\n    color: var(--icon-fill-color);\n    font-family: var(--ui-font);\n    margin: 0 0.8em;\n    text-overflow: ellipsis;\n  }\n\n  .tool-icon_size_small .tool-icon__icon {\n    width: 2rem;\n    height: 2rem;\n    font-size: 0.8em;\n    svg {\n      width: var(--default-icon-size);\n      height: var(--default-icon-size);\n    }\n  }\n\n  .tool-icon__keybinding {\n    position: absolute;\n    bottom: 2px;\n    right: 3px;\n    font-size: 0.625rem;\n    color: var(--keybinding-color);\n    font-family: var(--ui-font);\n    user-select: none;\n  }\n}\n"
  },
  {
    "path": "packages/drawnix/src/components/toolbar/app-toolbar/app-menu-items.tsx",
    "content": "import {\n  ExportImageIcon,\n  GithubIcon,\n  OpenFileIcon,\n  SaveFileIcon,\n  TrashIcon,\n} from '../../icons';\nimport { useBoard, useListRender } from '@plait-board/react-board';\nimport {\n  BoardTransforms,\n  PlaitBoard,\n  PlaitElement,\n  PlaitTheme,\n  ThemeColorMode,\n  Viewport,\n} from '@plait/core';\nimport { loadFromJSON, saveAsJSON } from '../../../data/json';\nimport MenuItem from '../../menu/menu-item';\nimport MenuItemLink from '../../menu/menu-item-link';\nimport { saveAsImage, saveAsSvg } from '../../../utils/image';\nimport { useDrawnix } from '../../../hooks/use-drawnix';\nimport { useI18n } from '../../../i18n';\nimport Menu from '../../menu/menu';\nimport { useContext } from 'react';\nimport { MenuContentPropsContext } from '../../menu/common';\nimport { EVENT } from '../../../constants';\nimport { getShortcutKey } from '../../../utils/common';\n\nexport const SaveToFile = () => {\n  const board = useBoard();\n  const { t } = useI18n();\n  return (\n    <MenuItem\n      data-testid=\"save-button\"\n      onSelect={() => {\n        saveAsJSON(board);\n      }}\n      icon={SaveFileIcon}\n      aria-label={t('menu.saveFile')}\n      shortcut={getShortcutKey('CtrlOrCmd+S')}\n    >{t('menu.saveFile')}</MenuItem>\n  );\n};\nSaveToFile.displayName = 'SaveToFile';\n\nexport const OpenFile = () => {\n  const board = useBoard();\n  const listRender = useListRender();\n  const { t } = useI18n();\n  const clearAndLoad = (\n    value: PlaitElement[],\n    viewport?: Viewport,\n    theme?: PlaitTheme\n  ) => {\n    board.children = value;\n    board.viewport = viewport || { zoom: 1 };\n    if (theme) {\n      board.theme = theme;\n    }\n    listRender.update(board.children, {\n      board: board,\n      parent: board,\n      parentG: PlaitBoard.getElementHost(board),\n    });\n    BoardTransforms.fitViewport(board);\n  };\n  return (\n    <MenuItem\n      data-testid=\"open-button\"\n      onSelect={() => {\n        loadFromJSON(board).then((data) => {\n          clearAndLoad(data.elements, data.viewport, data.theme);\n        });\n      }}\n      icon={OpenFileIcon}\n      aria-label={t('menu.open')}\n    >{t('menu.open')}</MenuItem>\n  );\n};\nOpenFile.displayName = 'OpenFile';\n\nexport const SaveAsImage = () => {\n  const board = useBoard();\n  const menuContentProps = useContext(MenuContentPropsContext);\n  const { t } = useI18n();\n  return (\n    <MenuItem\n      icon={ExportImageIcon}\n      data-testid=\"image-export-button\"\n      onSelect={() => {\n        saveAsImage(board, true);\n      }}\n      submenu={\n        <Menu onSelect={() => {\n          const itemSelectEvent = new CustomEvent(EVENT.MENU_ITEM_SELECT, {\n            bubbles: true,\n            cancelable: true,\n          });\n          menuContentProps.onSelect?.(itemSelectEvent);\n        }}>\n          <MenuItem\n            onSelect={() => {\n              saveAsSvg(board);\n            }}\n            aria-label={t('menu.exportImage.svg')}\n          >\n            {t('menu.exportImage.svg')}\n          </MenuItem>\n          <MenuItem\n            onSelect={() => {\n              saveAsImage(board, true);\n            }}\n            aria-label={t('menu.exportImage.png')}\n          >\n            {t('menu.exportImage.png')}\n          </MenuItem>\n          <MenuItem\n            onSelect={() => {\n              saveAsImage(board, false);\n            }}\n            aria-label={t('menu.exportImage.jpg')}\n          >\n            {t('menu.exportImage.jpg')}\n          </MenuItem>\n        </Menu>\n      }\n      shortcut={getShortcutKey('CtrlOrCmd+Shift+E')}\n      aria-label={t('menu.exportImage')}\n    >\n      {t('menu.exportImage')}\n    </MenuItem>\n  );\n};\nSaveAsImage.displayName = 'SaveAsImage';\n\nexport const CleanBoard = () => {\n  const { appState, setAppState } = useDrawnix();\n  const { t } = useI18n();\n  return (\n    <MenuItem\n      icon={TrashIcon}\n      data-testid=\"reset-button\"\n      onSelect={() => {\n        setAppState({\n          ...appState,\n          openCleanConfirm: true,\n        });\n      }}\n      shortcut={getShortcutKey('CtrlOrCmd+Backspace')}\n      aria-label={t('menu.cleanBoard')}\n    >\n      {t('menu.cleanBoard')}\n    </MenuItem>\n  );\n};\nCleanBoard.displayName = 'CleanBoard';\n\nexport const Socials = () => {\n  return (\n    <MenuItemLink\n      icon={GithubIcon}\n      href=\"https://github.com/plait-board/drawnix\"\n      aria-label=\"GitHub\"\n    >\n      GitHub\n    </MenuItemLink>\n  );\n};\nSocials.displayName = 'Socials';\n"
  },
  {
    "path": "packages/drawnix/src/components/toolbar/app-toolbar/app-toolbar.tsx",
    "content": "import { useBoard } from '@plait-board/react-board';\nimport Stack from '../../stack';\nimport { ToolButton } from '../../tool-button';\nimport {\n  DuplicateIcon,\n  MenuIcon,\n  RedoIcon,\n  TrashIcon,\n  UndoIcon,\n} from '../../icons';\nimport classNames from 'classnames';\nimport {\n  ATTACHED_ELEMENT_CLASS_NAME,\n  deleteFragment,\n  duplicateElements,\n  getSelectedElements,\n  PlaitBoard,\n} from '@plait/core';\nimport { Island } from '../../island';\nimport { Popover, PopoverContent, PopoverTrigger } from '../../popover/popover';\nimport { useState } from 'react';\nimport { CleanBoard, OpenFile, SaveAsImage, SaveToFile, Socials } from './app-menu-items';\nimport { LanguageSwitcherMenu } from './language-switcher-menu';\nimport Menu from '../../menu/menu';\nimport MenuSeparator from '../../menu/menu-separator';\nimport { useI18n } from '../../../i18n';\n\nexport const AppToolbar = () => {\n  const board = useBoard();\n  const { t } = useI18n();\n  const container = PlaitBoard.getBoardContainer(board);\n  const selectedElements = getSelectedElements(board);\n  const [appMenuOpen, setAppMenuOpen] = useState(false);\n  const isUndoDisabled = board.history.undos.length <= 0;\n  const isRedoDisabled = board.history.redos.length <= 0;\n  return (\n    <Island\n      padding={1}\n      className={classNames('app-toolbar', ATTACHED_ELEMENT_CLASS_NAME)}\n    >\n      <Stack.Row gap={1}>\n        <Popover\n          key={0}\n          sideOffset={12}\n          open={appMenuOpen}\n          onOpenChange={(open) => {\n            setAppMenuOpen(open);\n          }}\n          placement=\"bottom-start\"\n        >\n          <PopoverTrigger asChild>\n            <ToolButton\n              type=\"icon\"\n              visible={true}\n              selected={appMenuOpen}\n              icon={MenuIcon}\n              title={t('general.menu')}\n              aria-label={t('general.menu')}\n              onPointerDown={() => {\n                setAppMenuOpen(!appMenuOpen);\n              }}\n            />\n          </PopoverTrigger>\n          <PopoverContent container={container}>\n            <Menu\n              onSelect={() => {\n                setAppMenuOpen(false);\n              }}\n            >\n              <OpenFile></OpenFile>\n              <SaveToFile></SaveToFile>\n              <SaveAsImage></SaveAsImage>\n              <CleanBoard></CleanBoard>\n              <MenuSeparator />\n              <LanguageSwitcherMenu />\n              <Socials />\n            </Menu>\n          </PopoverContent>\n        </Popover>\n        <ToolButton\n          key={1}\n          type=\"icon\"\n          icon={UndoIcon}\n          visible={true}\n          title={t('general.undo')}\n          aria-label={t('general.undo')}\n          onPointerUp={() => {\n            board.undo();\n          }}\n          disabled={isUndoDisabled}\n        />\n        <ToolButton\n          key={2}\n          type=\"icon\"\n          icon={RedoIcon}\n          visible={true}\n          title={t('general.redo')}\n          aria-label={t('general.redo')}\n          onPointerUp={() => {\n            board.redo();\n          }}\n          disabled={isRedoDisabled}\n        />\n        {selectedElements.length > 0 && (\n          <ToolButton\n            className=\"duplicate\"\n            key={3}\n            type=\"icon\"\n            icon={DuplicateIcon}\n            visible={true}\n            title={t('general.duplicate')}\n            aria-label={t('general.duplicate')}\n            onPointerUp={() => {\n              duplicateElements(board);\n            }}\n          />\n        )}\n        {selectedElements.length > 0 && (\n          <ToolButton\n            className=\"trash\"\n            key={4}\n            type=\"icon\"\n            icon={TrashIcon}\n            visible={true}\n            title={t('general.delete')}\n            aria-label={t('general.delete')}\n            onPointerUp={() => {\n              deleteFragment(board);\n            }}\n          />\n        )}\n        \n      </Stack.Row>\n    </Island>\n  );\n};\n"
  },
  {
    "path": "packages/drawnix/src/components/toolbar/app-toolbar/language-switcher-menu.tsx",
    "content": "import { useContext } from 'react';\nimport { MenuIcon } from '../../icons';\nimport { useI18n } from '../../../i18n';\nimport Menu from '../../menu/menu';\nimport MenuItem from '../../menu/menu-item';\nimport { MenuContentPropsContext } from '../../menu/common';\nimport { EVENT } from '../../../constants';\n\nexport const LanguageSwitcherMenu = () => {\n  const { language, setLanguage, t } = useI18n();\n  const menuContentProps = useContext(MenuContentPropsContext);\n  \n  return (\n    <MenuItem\n      icon={MenuIcon}\n      data-testid=\"language-switcher-button\"\n      onSelect={() => {\n        // This will be handled by the submenu\n      }}\n      submenu={\n        <Menu onSelect={() => {\n          const itemSelectEvent = new CustomEvent(EVENT.MENU_ITEM_SELECT, {\n            bubbles: true,\n            cancelable: true,\n          });\n          menuContentProps.onSelect?.(itemSelectEvent);\n        }}>\n          <MenuItem\n            onSelect={() => {\n              setLanguage('zh');\n            }}\n            aria-label={t('language.chinese')}\n            selected={language === 'zh'}\n          >\n            {t('language.chinese')}\n          </MenuItem>\n          <MenuItem\n            onSelect={() => {\n              setLanguage('en');\n            }}\n            aria-label={t('language.english')}\n            selected={language === 'en'}\n          >\n            {t('language.english')}\n          </MenuItem>\n            <MenuItem\n             onSelect={() => {\n              setLanguage('ru');\n            }}\n            aria-label={t('language.russian')}\n            selected={language === 'ru'}\n          >\n            {t('language.russian')}\n          </MenuItem>\n          <MenuItem\n            onSelect={() => {\n              setLanguage('ar');\n            }}\n            aria-label={t('language.arabic')}\n            selected={language === 'ar'}\n          >{t('language.arabic')} \n            </MenuItem>\n          <MenuItem\n            onSelect={() => {\n              setLanguage('vi');\n            }}\n            aria-label={t('language.vietnamese')}\n            selected={language === 'vi'}\n          >{t('language.vietnamese')}\n          </MenuItem>\n        </Menu>\n      }\n      aria-label={t('language.switcher')}\n    >\n      {t('language.switcher')}\n    </MenuItem>\n  );\n};\n\nLanguageSwitcherMenu.displayName = 'LanguageSwitcherMenu';\n"
  },
  {
    "path": "packages/drawnix/src/components/toolbar/creation-toolbar.tsx",
    "content": "import classNames from 'classnames';\nimport { Island } from '../island';\nimport Stack from '../stack';\nimport { ToolButton } from '../tool-button';\nimport {\n  HandIcon,\n  MindIcon,\n  SelectionIcon,\n  ShapeIcon,\n  TextIcon,\n  EraseIcon,\n  StraightArrowLineIcon,\n  FeltTipPenIcon,\n  ImageIcon,\n  ExtraToolsIcon,\n} from '../icons';\nimport { useBoard } from '@plait-board/react-board';\nimport {\n  ATTACHED_ELEMENT_CLASS_NAME,\n  BoardTransforms,\n  PlaitBoard,\n  PlaitPointerType,\n} from '@plait/core';\nimport { MindPointerType } from '@plait/mind';\nimport { BoardCreationMode, setCreationMode } from '@plait/common';\nimport {\n  ArrowLineShape,\n  BasicShapes,\n  DrawPointerType,\n  FlowchartSymbols,\n} from '@plait/draw';\nimport { FreehandPanel , FREEHANDS } from './freehand-panel/freehand-panel';\nimport { ShapePicker } from '../shape-picker';\nimport { ArrowPicker } from '../arrow-picker';\nimport { useState } from 'react';\nimport { Popover, PopoverContent, PopoverTrigger } from '../popover/popover';\nimport { FreehandShape } from '../../plugins/freehand/type';\nimport {\n  DrawnixPointerType,\n  useDrawnix,\n  useSetPointer,\n} from '../../hooks/use-drawnix';\nimport { ExtraToolsButton } from './extra-tools/extra-tools-button';\nimport { addImage } from '../../utils/image';\nimport { useI18n } from '../../i18n';\nimport { SHAPES } from '../shape-picker';\nimport { ARROWS } from '../arrow-picker';\n\nexport enum PopupKey {\n  'shape' = 'shape',\n  'arrow' = 'arrow',\n  'freehand' = 'freehand',\n}\n\ntype AppToolButtonProps = {\n  titleKey?: keyof typeof import('../../i18n').Translations;\n  name?: string;\n  icon: React.ReactNode;\n  pointer?: DrawnixPointerType;\n  key?: PopupKey | 'image' | 'extra-tools';\n};\n\nconst isBasicPointer = (pointer: string) => {\n  return (\n    pointer === PlaitPointerType.hand || pointer === PlaitPointerType.selection\n  );\n};\n\nexport const BUTTONS: AppToolButtonProps[] = [\n  {\n    icon: HandIcon,\n    pointer: PlaitPointerType.hand,\n    titleKey: 'toolbar.hand',\n  },\n  {\n    icon: SelectionIcon,\n    pointer: PlaitPointerType.selection,\n    titleKey: 'toolbar.selection',\n  },\n  {\n    icon: MindIcon,\n    pointer: MindPointerType.mind,\n    titleKey: 'toolbar.mind',\n  },\n  {\n    icon: TextIcon,\n    pointer: BasicShapes.text,\n    titleKey: 'toolbar.text',\n  },\n  {\n    icon: FeltTipPenIcon,\n    pointer: FreehandShape.feltTipPen,\n    titleKey: 'toolbar.pen',\n    key: PopupKey.freehand,\n  },\n  {\n    icon: StraightArrowLineIcon,\n    titleKey: 'toolbar.arrow',\n    key: PopupKey.arrow,\n    pointer: ArrowLineShape.straight,\n  },\n  {\n    icon: ShapeIcon,\n    titleKey: 'toolbar.shape',\n    key: PopupKey.shape,\n    pointer: BasicShapes.rectangle,\n  },\n  {\n    icon: ImageIcon,\n    titleKey: 'toolbar.image',\n    key: 'image',\n  },\n  {\n    icon: ExtraToolsIcon,\n    titleKey: 'toolbar.extraTools',\n    key: 'extra-tools',\n  },\n];\n\n// TODO provider by plait/draw\nexport const isArrowLinePointer = (board: PlaitBoard) => {\n  return Object.keys(ArrowLineShape).includes(board.pointer);\n};\n\nexport const isShapePointer = (board: PlaitBoard) => {\n  return (\n    Object.keys(BasicShapes).includes(board.pointer) ||\n    Object.keys(FlowchartSymbols).includes(board.pointer)\n  );\n};\n\nexport const CreationToolbar = () => {\n  const board = useBoard();\n  const { appState } = useDrawnix();\n  const { t } = useI18n();\n  const setPointer = useSetPointer();\n  const container = PlaitBoard.getBoardContainer(board);\n\n  const [freehandOpen, setFreehandOpen] = useState(false);\n  const [arrowOpen, setArrowOpen] = useState(false);\n  const [shapeOpen, setShapeOpen] = useState(false);\n  const [lastFreehandButton, setLastFreehandButton] =\n    useState<AppToolButtonProps>(\n      BUTTONS.find((button) => button.key === PopupKey.freehand)!\n    );\n  const [lastShapePointer, setLastShapePointer] = useState<string | undefined>(SHAPES[0].pointer);\n  const [lastArrowPointer, setLastArrowPointer] = useState<string | undefined>(ARROWS[0].pointer);\n\n  const onPointerDown = (pointer: DrawnixPointerType) => {\n    setCreationMode(board, BoardCreationMode.dnd);\n    BoardTransforms.updatePointerType(board, pointer);\n    setPointer(pointer);\n  };\n\n  const onPointerUp = () => {\n    setCreationMode(board, BoardCreationMode.drawing);\n  };\n\n  const isChecked = (button: AppToolButtonProps) => {\n    return (\n      PlaitBoard.isPointer(board, button.pointer) && !arrowOpen && !shapeOpen && !freehandOpen\n    );\n  };\n\n  const checkCurrentPointerIsFreehand = (board: PlaitBoard) => {\n    return PlaitBoard.isInPointer(board, [\n      FreehandShape.feltTipPen, \n      FreehandShape.eraser,\n    ]);\n  };\n\n\n  return (\n    <Island\n      padding={1}\n      className={classNames('draw-toolbar', ATTACHED_ELEMENT_CLASS_NAME)}\n    >\n      <Stack.Row gap={1}>\n        {BUTTONS.map((button, index) => {\n          if (appState.isMobile && button.pointer === PlaitPointerType.hand) {\n            return <></>;\n          }\n          if (button.key === PopupKey.freehand) {\n            return (\n              <Popover\n                key={index}\n                open={freehandOpen || checkCurrentPointerIsFreehand(board)}\n                sideOffset={12}\n                onOpenChange={(open) => {\n                  setFreehandOpen(open);\n                }}\n              >\n                <PopoverTrigger asChild>\n                  <ToolButton\n                    type=\"icon\"\n                    visible={true}\n                    selected={\n                      freehandOpen ||\n                      checkCurrentPointerIsFreehand(board)\n                    }\n                    icon={lastFreehandButton.icon}\n                    title={lastFreehandButton.titleKey ? t(lastFreehandButton.titleKey) : 'Freehand'}\n                    aria-label={lastFreehandButton.titleKey ? t(lastFreehandButton.titleKey) : 'Freehand'}\n                    onPointerDown={() => {\n                      setFreehandOpen(!freehandOpen);\n                      onPointerDown(lastFreehandButton.pointer!);\n                    }}\n                    onPointerUp={() => {\n                      onPointerUp();\n                    }}\n                  />\n                </PopoverTrigger>\n                <PopoverContent container={container}>\n                  <FreehandPanel\n                    onPointerUp={(pointer: DrawnixPointerType) => {\n                      setPointer(pointer);\n                      setLastFreehandButton(\n                        FREEHANDS.find((button) => button.pointer === pointer)!\n                      );\n                    }}\n                  ></FreehandPanel>\n                </PopoverContent>\n              </Popover>\n            );\n          }\n          if (button.key === PopupKey.shape) {\n            return (\n              <Popover\n                key={index}\n                open={shapeOpen}\n                sideOffset={12}\n                onOpenChange={(open) => {\n                  setShapeOpen(open);\n                }}\n              >\n                <PopoverTrigger asChild>\n                  <ToolButton\n                    type=\"icon\"\n                    visible={true}\n                    selected={\n                      shapeOpen ||\n                      (isShapePointer(board) &&\n                        !PlaitBoard.isPointer(board, BasicShapes.text))\n                    }\n                    icon={button.icon}\n                    title={button.titleKey ? t(button.titleKey) : 'Shape'}\n                    aria-label={button.titleKey ? t(button.titleKey) : 'Shape'}\n                    onPointerDown={() => {\n                      setShapeOpen(!shapeOpen);\n                      if (isShapePointer(board)) {\n                        BoardTransforms.updatePointerType(board, board.pointer);\n                      } else {\n                        setPointer(lastShapePointer || SHAPES[0].pointer)\n                        setCreationMode(board, BoardCreationMode.drawing);\n                        BoardTransforms.updatePointerType(board, lastShapePointer || SHAPES[0].pointer);\n                      } \n                    }}\n                  />\n                </PopoverTrigger>\n                <PopoverContent container={container}>\n                  <ShapePicker\n                    onPointerUp={(pointer: DrawPointerType) => {\n                      setShapeOpen(false);\n                      setPointer(pointer);\n                      setLastShapePointer(pointer);\n                    }}\n                  ></ShapePicker>\n                </PopoverContent>\n              </Popover>\n            );\n          }\n          if (button.key === PopupKey.arrow) {\n            return (\n              <Popover\n                key={index}\n                open={arrowOpen}\n                sideOffset={12}\n                onOpenChange={(open) => {\n                  setArrowOpen(open);\n                }}\n              >\n                <PopoverTrigger asChild>\n                  <ToolButton\n                    type=\"icon\"\n                    visible={true}\n                    selected={arrowOpen || isArrowLinePointer(board)}\n                    icon={button.icon}\n                    title={button.titleKey ? t(button.titleKey) : ''}\n                    aria-label={button.titleKey ? t(button.titleKey) : ''}\n                    onPointerDown={() => {\n                      setArrowOpen(!arrowOpen);\n                      if (isArrowLinePointer(board)) {\n                        BoardTransforms.updatePointerType(board, board.pointer);\n                      } else {\n                        setCreationMode(board, BoardCreationMode.drawing);\n                        BoardTransforms.updatePointerType(board, lastArrowPointer || ARROWS[0].pointer);\n                        setPointer(lastArrowPointer || ARROWS[0].pointer);\n                      }\n                    }}\n                  />\n                </PopoverTrigger>\n                <PopoverContent container={container}>\n                  <ArrowPicker\n                    onPointerUp={(pointer: DrawPointerType) => {\n                      setArrowOpen(false);\n                      setPointer(pointer);\n                      setLastArrowPointer(pointer);\n                    }}\n                  ></ArrowPicker>\n                </PopoverContent>\n              </Popover>\n            );\n          }\n          if (button.key === 'extra-tools') {\n            return <ExtraToolsButton key={index}></ExtraToolsButton>;\n          }\n          return (\n            <ToolButton\n              key={index}\n              type=\"radio\"\n              icon={button.icon}\n              checked={isChecked(button)}\n              title={button.titleKey ? t(button.titleKey) : ''}\n              aria-label={button.titleKey ? t(button.titleKey) : ''}\n              onPointerDown={() => {\n                if (button.pointer && !isBasicPointer(button.pointer)) {\n                  onPointerDown(button.pointer);\n                }\n              }}\n              onPointerUp={() => {\n                if (button.pointer && !isBasicPointer(button.pointer)) {\n                  onPointerUp();\n                } else if (button.pointer && isBasicPointer(button.pointer)) {\n                  BoardTransforms.updatePointerType(board, button.pointer);\n                  setPointer(button.pointer);\n                }\n                if (button.key === 'image') {\n                  addImage(board);\n                }\n              }}\n            />\n          );\n        })}\n      </Stack.Row>\n    </Island>\n  );\n};\n"
  },
  {
    "path": "packages/drawnix/src/components/toolbar/extra-tools/extra-tools-button.tsx",
    "content": "import { useBoard } from \"@plait-board/react-board\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../../popover/popover\";\nimport { PlaitBoard } from \"@plait/core\";\nimport { useState } from \"react\";\nimport { ToolButton } from \"../../tool-button\";\nimport { ExtraToolsIcon } from \"../../icons\";\nimport Menu from \"../../menu/menu\";\nimport { MarkdownToDrawnixItem, MermaidToDrawnixItem } from \"./menu-items\";\nimport { useI18n } from \"../../../i18n\";\n\nexport const ExtraToolsButton = () => {\n  const board = useBoard();\n  const { t } = useI18n();\n  const container = PlaitBoard.getBoardContainer(board);\n  const [appMenuOpen, setAppMenuOpen] = useState(false);\n  return (\n    <Popover\n      key={0}\n      sideOffset={12}\n      open={appMenuOpen}\n      onOpenChange={(open) => {\n        setAppMenuOpen(open);\n      }}\n      placement=\"bottom-start\"\n    >\n      <PopoverTrigger asChild>\n        <ToolButton\n          type=\"icon\"\n          visible={true}\n          selected={appMenuOpen}\n          icon={ExtraToolsIcon}\n          title={t('toolbar.extraTools')}\n          aria-label={t('toolbar.extraTools')}\n          onPointerDown={() => {\n            setAppMenuOpen(!appMenuOpen);\n          }}\n        />\n      </PopoverTrigger>\n      <PopoverContent container={container}>\n        <Menu\n          onSelect={() => {\n            setAppMenuOpen(false);\n          }}\n        >\n          <MermaidToDrawnixItem></MermaidToDrawnixItem>\n          <MarkdownToDrawnixItem></MarkdownToDrawnixItem>\n        </Menu>\n      </PopoverContent>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "packages/drawnix/src/components/toolbar/extra-tools/menu-items.tsx",
    "content": "import MenuItem from '../../menu/menu-item';\nimport { MarkdownLogoIcon, MermaidLogoIcon } from '../../icons';\nimport { DialogType, useDrawnix } from '../../../hooks/use-drawnix';\nimport { useI18n } from '../../../i18n';\n\nexport const MermaidToDrawnixItem = () => {\n  const { appState, setAppState } = useDrawnix();\n  const { t } = useI18n();\n  return (\n    <MenuItem\n      data-testid=\"marmaid-to-drawnix-button\"\n      onSelect={() => {\n        setAppState({\n          ...appState,\n          openDialogType: DialogType.mermaidToDrawnix,\n        });\n      }}\n      icon={MermaidLogoIcon}\n      aria-label={t('extraTools.mermaidToDrawnix')}\n    >\n      {t('extraTools.mermaidToDrawnix')}\n    </MenuItem>\n  );\n};\n\nMermaidToDrawnixItem.displayName = 'MermaidToDrawnix';\n\nexport const MarkdownToDrawnixItem = () => {\n  const { appState, setAppState } = useDrawnix();\n  const { t } = useI18n();\n  return (\n    <MenuItem\n      data-testid=\"markdown-to-drawnix-button\"\n      onSelect={() => {\n        setAppState({\n          ...appState,\n          openDialogType: DialogType.markdownToDrawnix,\n        });\n      }}\n      icon={MarkdownLogoIcon}\n      aria-label={t('extraTools.markdownToDrawnix')}\n    >\n      {t('extraTools.markdownToDrawnix')}\n    </MenuItem>\n  );\n};\n\nMarkdownToDrawnixItem.displayName = 'MarkdownToDrawnix';\n"
  },
  {
    "path": "packages/drawnix/src/components/toolbar/freehand-panel/freehand-panel.tsx",
    "content": "import classNames from 'classnames';\nimport { Island } from '../../island';\nimport Stack from '../../stack';\nimport { ToolButton } from '../../tool-button';\nimport {\n  EraseIcon,\n  FeltTipPenIcon,\n} from '../../icons';\nimport { BoardTransforms } from '@plait/core';\nimport React from 'react';\nimport { BoardCreationMode, setCreationMode } from '@plait/common';\nimport { FreehandShape } from '../../../plugins/freehand/type';\nimport { useBoard } from '@plait-board/react-board';\nimport { splitRows } from '../../../utils/common';\nimport {\n    DrawnixPointerType,\n} from '../../../hooks/use-drawnix';\nimport { useI18n } from '../../../i18n';\n\nexport interface FreehandProps {\n    titleKey: string;\n    icon: React.ReactNode;\n    pointer: DrawnixPointerType;\n}\n\nexport const FREEHANDS: FreehandProps[] = [\n  {\n      icon: FeltTipPenIcon,\n      pointer: FreehandShape.feltTipPen,\n      titleKey: 'toolbar.pen',\n    },\n    {\n      icon: EraseIcon,\n      pointer: FreehandShape.eraser,\n      titleKey: 'toolbar.eraser',\n    },\n];\n\nconst ROW_FREEHANDS = splitRows(FREEHANDS, 5);\n\nexport type FreehandPickerProps = {\n  onPointerUp: (pointer: DrawnixPointerType) => void;\n};\n\nexport const FreehandPanel: React.FC<FreehandPickerProps> = ({\n  onPointerUp,\n}) => {\n  const { t } = useI18n();\n  const board = useBoard();\n  return (\n    <Island padding={1}>\n      <Stack.Col gap={1}>\n        {ROW_FREEHANDS.map((rowFreehands, rowIndex) => {\n          return (\n            <Stack.Row gap={1} key={rowIndex}>\n              {rowFreehands.map((freehand, index) => {\n                return (\n                  <ToolButton\n                    key={index}\n                    className={classNames({ fillable: false })}\n                    selected={board.pointer === freehand.pointer}\n                    type=\"icon\"\n                    size={'small'}\n                    visible={true}\n                    icon={freehand.icon}\n                    title={t(freehand.titleKey)}\n                    aria-label={t(freehand.titleKey)}\n                    onPointerDown={() => {\n                      setCreationMode(board, BoardCreationMode.dnd);\n                      BoardTransforms.updatePointerType(board, freehand.pointer);\n                    }}\n                    onPointerUp={() => {\n                      setCreationMode(board, BoardCreationMode.drawing);\n                      onPointerUp(freehand.pointer);\n                    }}\n                  />\n                );\n              })}\n            </Stack.Row>\n          );\n        })}\n      </Stack.Col>\n    </Island>\n  );\n};\n"
  },
  {
    "path": "packages/drawnix/src/components/toolbar/pencil-mode-toolbar.tsx",
    "content": "import { ToolButton } from '../tool-button';\nimport { useBoard } from '@plait-board/react-board';\nimport { useDrawnix } from '../../hooks/use-drawnix';\nimport { setIsPencilMode } from '../../plugins/with-pencil';\n\nexport const ClosePencilToolbar = () => {\n  const board = useBoard();\n  const { appState, setAppState } = useDrawnix();\n  return (\n    <>\n      {appState.isPencilMode && (\n        <div className=\"pencil-mode-toolbar\">\n          <ToolButton\n            type=\"button\"\n            visible={true}\n            title={`X Pencil`}\n            aria-label={`Arrow`}\n            label=\"Pencil X\"\n            onPointerDown={() => {\n              setAppState({ ...appState, isPencilMode: false });\n              setIsPencilMode(board, false);\n            }}\n          />\n        </div>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/drawnix/src/components/toolbar/popup-toolbar/arrow-mark-button.tsx",
    "content": "import React, { useState } from 'react';\nimport { ToolButton } from '../../tool-button';\nimport classNames from 'classnames';\nimport { PlaitBoard } from '@plait/core';\nimport { ArrowIcon, LineIcon } from '../../icons';\nimport { Popover, PopoverContent, PopoverTrigger } from '../../popover/popover';\nimport { ArrowLineHandle } from '@plait/draw';\nimport { ArrowMarkerPicker } from '../../arrow-mark-picker';\nimport { useI18n } from '../../../i18n';\nimport type { Translations } from '../../../i18n';\n\nexport type ArrowMarkButtonProps = {\n  board: PlaitBoard;\n  endProperty?: ArrowLineHandle;\n  children?: React.ReactNode;\n  end: 'source' | 'target';\n};\n\nexport const ArrowMarkButton: React.FC<ArrowMarkButtonProps> = ({\n  board,\n  end,\n  endProperty,\n}) => {\n  const [isPopoverOpen, setIsPopoverrOpen] = useState(false);\n  const container = PlaitBoard.getBoardContainer(board);\n  const { t } = useI18n();\n  if (!endProperty) {\n    return null;\n  }\n  const marker = endProperty.marker ?? 'none';\n  const endLabelKey: keyof Translations = end === 'source' ? 'line.source' : 'line.target';\n  const markerLabelKey: keyof Translations =\n    marker === 'none' ? 'line.none' : 'line.arrow';\n  const title = `${t(endLabelKey)} — ${t(markerLabelKey)}`;\n\n  return (\n    <Popover\n      sideOffset={12}\n      open={isPopoverOpen}\n      onOpenChange={(open) => {\n        setIsPopoverrOpen(open);\n      }}\n      placement={'top'}\n    >\n      <PopoverTrigger asChild>\n        <ToolButton\n          className={classNames(\n            `property-button  ${end === 'source' ? 'source-arrow-button' : ''}`\n          )}\n          visible={true}\n          icon={marker === 'none' ? LineIcon : ArrowIcon}\n          type=\"button\"\n          title={title}\n          aria-label={title}\n          selected={isPopoverOpen}\n          onPointerUp={() => {\n            setIsPopoverrOpen(!isPopoverOpen);\n          }}\n        ></ToolButton>\n      </PopoverTrigger>\n      <PopoverContent container={container}>\n        <ArrowMarkerPicker end={end} property={endProperty} />\n      </PopoverContent>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "packages/drawnix/src/components/toolbar/popup-toolbar/fill-button.tsx",
    "content": "import React, { useState } from 'react';\nimport { ToolButton } from '../../tool-button';\nimport classNames from 'classnames';\nimport { ATTACHED_ELEMENT_CLASS_NAME, PlaitBoard } from '@plait/core';\nimport { Island } from '../../island';\nimport { ColorPicker } from '../../color-picker';\nimport {\n  hexAlphaToOpacity,\n  isFullyTransparent,\n  removeHexAlpha,\n} from '../../../utils/color';\nimport { BackgroundColorIcon } from '../../icons';\nimport { Popover, PopoverContent, PopoverTrigger } from '../../popover/popover';\nimport {\n  setFillColor,\n  setFillColorOpacity,\n} from '../../../transforms/property';\n\nexport type PopupFillButtonProps = {\n  board: PlaitBoard;\n  currentColor: string | undefined;\n  title: string;\n  children?: React.ReactNode;\n};\n\nexport const PopupFillButton: React.FC<PopupFillButtonProps> = ({\n  board,\n  currentColor,\n  title,\n  children,\n}) => {\n  const [isFillPropertyOpen, setIsFillPropertyOpen] = useState(false);\n  const hexColor = currentColor && removeHexAlpha(currentColor);\n  const opacity = currentColor ? hexAlphaToOpacity(currentColor) : 100;\n  const container = PlaitBoard.getBoardContainer(board);\n  const icon =\n    !hexColor || isFullyTransparent(opacity) ? BackgroundColorIcon : undefined;\n\n  return (\n    <Popover\n      sideOffset={12}\n      open={isFillPropertyOpen}\n      onOpenChange={(open) => {\n        setIsFillPropertyOpen(open);\n      }}\n      placement={'top'}\n    >\n      <PopoverTrigger asChild>\n        <ToolButton\n          className={classNames(`property-button`)}\n          visible={true}\n          selected={isFillPropertyOpen}\n          icon={icon}\n          type=\"button\"\n          title={title}\n          aria-label={title}\n          onPointerUp={() => {\n            setIsFillPropertyOpen(!isFillPropertyOpen);\n          }}\n        >\n          {!icon && children}\n        </ToolButton>\n      </PopoverTrigger>\n      <PopoverContent container={container}>\n        <Island\n          padding={4}\n          className={classNames(`${ATTACHED_ELEMENT_CLASS_NAME}`)}\n        >\n          <ColorPicker\n            onColorChange={(selectedColor: string) => {\n              setFillColor(board, selectedColor);\n            }}\n            onOpacityChange={(opacity: number) => {\n              setFillColorOpacity(board, opacity);\n            }}\n            currentColor={currentColor}\n          ></ColorPicker>\n        </Island>\n      </PopoverContent>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "packages/drawnix/src/components/toolbar/popup-toolbar/font-color-button.tsx",
    "content": "import React, { ReactNode, useState } from 'react';\nimport { ColorPicker } from '../../color-picker';\nimport { ToolButton } from '../../tool-button';\nimport classNames from 'classnames';\nimport { Island } from '../../island';\nimport { ATTACHED_ELEMENT_CLASS_NAME, PlaitBoard } from '@plait/core';\nimport { Popover, PopoverContent, PopoverTrigger } from '../../popover/popover';\nimport {\n  setTextColor,\n  setTextColorOpacity,\n} from '../../../transforms/property';\n\nexport type PopupFontColorButtonProps = {\n  board: PlaitBoard;\n  currentColor: string | undefined;\n  fontColorIcon: ReactNode;\n  title: string;\n};\n\nexport const PopupFontColorButton: React.FC<PopupFontColorButtonProps> = ({\n  board,\n  currentColor,\n  fontColorIcon,\n  title,\n}) => {\n  const [isFontColorPropertyOpen, setIsFontColorPropertyOpen] = useState(false);\n  const container = PlaitBoard.getBoardContainer(board);\n\n  return (\n    <Popover\n      sideOffset={12}\n      open={isFontColorPropertyOpen}\n      onOpenChange={(open) => {\n        setIsFontColorPropertyOpen(open);\n      }}\n      placement={'top'}\n    >\n      <PopoverTrigger asChild>\n        <ToolButton\n          className={classNames(`property-button`)}\n          selected={isFontColorPropertyOpen}\n          visible={true}\n          icon={fontColorIcon}\n          type=\"button\"\n          title={title}\n          aria-label={title}\n          onPointerUp={() => {\n            setIsFontColorPropertyOpen(!isFontColorPropertyOpen);\n          }}\n        ></ToolButton>\n      </PopoverTrigger>\n      <PopoverContent container={container}>\n        <Island\n          padding={4}\n          className={classNames(`${ATTACHED_ELEMENT_CLASS_NAME}`)}\n        >\n          <ColorPicker\n            onColorChange={(selectedColor: string) => {\n              setTextColor(\n                board,\n                currentColor ? currentColor : selectedColor,\n                selectedColor\n              );\n            }}\n            onOpacityChange={(opacity: number) => {\n              if (currentColor) {\n                setTextColorOpacity(board, currentColor, opacity);\n              }\n            }}\n            currentColor={currentColor}\n          ></ColorPicker>\n        </Island>\n      </PopoverContent>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "packages/drawnix/src/components/toolbar/popup-toolbar/font-size-control.tsx",
    "content": "import React, { useEffect, useMemo, useRef, useState } from 'react';\nimport { PlaitBoard } from '@plait/core';\nimport { setTextFontSize } from '../../../transforms/property';\nimport { FontSizeStepperDownIcon, FontSizeStepperUpIcon } from '../../icons';\nimport { Select } from '../../select/select';\nimport { DEFAULT_FONT_SIZE } from '@plait/text-plugins';\n\nexport type PopupFontSizeControlProps = {\n  board: PlaitBoard;\n  currentFontSize?: number;\n  title: string;\n  options?: number[];\n};\n\nconst DEFAULT_OPTIONS = [10, 12, 14, 18, 24, 36, 48];\nconst MIN_FONT_SIZE = 8;\nconst MAX_FONT_SIZE = 78;\n\nexport const PopupFontSizeControl: React.FC<PopupFontSizeControlProps> = ({\n  board,\n  currentFontSize,\n  title,\n  options = DEFAULT_OPTIONS,\n}) => {\n  const [open, setOpen] = useState(false);\n  const inputRef = useRef<HTMLInputElement | null>(null);\n  const normalizedCurrent = useMemo(() => {\n    return Number.isFinite(currentFontSize as number) &&\n      (currentFontSize as number) > 0\n      ? (currentFontSize as number)\n      : undefined;\n  }, [currentFontSize]);\n\n  const [draft, setDraft] = useState<string>(\n    normalizedCurrent ? String(normalizedCurrent) : String(DEFAULT_FONT_SIZE)\n  );\n\n  useEffect(() => {\n    setDraft(normalizedCurrent ? String(normalizedCurrent) : String(DEFAULT_FONT_SIZE));\n  }, [normalizedCurrent]);\n\n  const apply = (value: string) => {\n    if (!value) {\n      setDraft('');\n      return;\n    }\n    const next = Number(value);\n    if (!Number.isFinite(next)) {\n      return;\n    }\n    const clamped = Math.min(\n      MAX_FONT_SIZE,\n      Math.max(MIN_FONT_SIZE, Math.round(next))\n    );\n    const nextValue = String(clamped);\n    setDraft(nextValue);\n    setTextFontSize(board, clamped);\n  };\n\n  const getBaseValue = () => {\n    const parsed = Number(draft);\n    if (Number.isFinite(parsed)) {\n      return Math.min(MAX_FONT_SIZE, Math.max(MIN_FONT_SIZE, Math.round(parsed)));\n    }\n    if (typeof normalizedCurrent === 'number' && normalizedCurrent > 0) {\n      return Math.min(\n        MAX_FONT_SIZE,\n        Math.max(MIN_FONT_SIZE, Math.round(normalizedCurrent))\n      );\n    }\n    return DEFAULT_FONT_SIZE;\n  };\n\n  const stepBy = (delta: number) => {\n    const base = getBaseValue();\n    const next = Math.min(\n      MAX_FONT_SIZE,\n      Math.max(MIN_FONT_SIZE, Math.round(base + delta))\n    );\n    const value = String(next);\n    setDraft(value);\n    apply(value);\n  };\n\n  const container = PlaitBoard.getBoardContainer(board);\n\n  return (\n    <Select.Root\n      open={open}\n      onOpenChange={setOpen}\n      placement={'top-start'}\n      sideOffset={12}\n      hideSelectedIndicator\n      disableInitialHighlight\n      disableItemHoverHighlight\n      disableTypeahead\n    >\n      <Select.Trigger asChild>\n        <div\n          className=\"popup-font-size\"\n          title={title}\n          aria-label={title}\n          onPointerDown={(event) => {\n            event.stopPropagation();\n          }}\n          onPointerUp={(event) => {\n            event.stopPropagation();\n          }}\n        >\n          <input\n            ref={inputRef}\n            className=\"popup-font-size__input\"\n            type=\"number\"\n            inputMode=\"numeric\"\n            min={MIN_FONT_SIZE}\n            max={MAX_FONT_SIZE}\n            step={1}\n            value={draft}\n            placeholder={''}\n            onChange={(event) => setDraft(event.target.value)}\n            onBlur={(event) => apply(event.target.value)}\n            onPointerUp={(event) => {\n              event.stopPropagation();\n              setOpen(true);\n            }}\n            onKeyDown={(event) => {\n              if (event.key === 'Enter') {\n                apply(draft);\n              }\n            }}\n          />\n          <div className=\"popup-font-size__stepper\" aria-hidden=\"false\">\n            <button\n              type=\"button\"\n              className=\"popup-font-size__stepper-button\"\n              aria-label={`${title} +`}\n              onPointerDown={(event) => {\n                event.preventDefault();\n                event.stopPropagation();\n              }}\n              onPointerUp={(event) => {\n                event.stopPropagation();\n                stepBy(1);\n                inputRef.current?.focus();\n              }}\n            >\n              <FontSizeStepperUpIcon\n                className=\"popup-font-size__stepper-icon\"\n                aria-hidden=\"true\"\n              />\n            </button>\n            <button\n              type=\"button\"\n              className=\"popup-font-size__stepper-button\"\n              aria-label={`${title} -`}\n              onPointerDown={(event) => {\n                event.preventDefault();\n                event.stopPropagation();\n              }}\n              onPointerUp={(event) => {\n                event.stopPropagation();\n                stepBy(-1);\n                inputRef.current?.focus();\n              }}\n            >\n              <FontSizeStepperDownIcon\n                className=\"popup-font-size__stepper-icon\"\n                aria-hidden=\"true\"\n              />\n            </button>\n          </div>\n        </div>\n      </Select.Trigger>\n      <Select.Content\n        container={container}\n        style={{ minWidth: '4.5rem' }}\n        onPointerDown={(event) => {\n          event.preventDefault();\n          event.stopPropagation();\n        }}\n        onPointerUp={(event) => {\n          event.stopPropagation();\n        }}\n      >\n        {options.map((size) => {\n          const value = String(size);\n          return (\n            <Select.Item\n              key={value}\n              value={value}\n              textValue={value}\n              onPointerUp={() => {\n                setDraft(value);\n                apply(value);\n              }}\n            >\n              {size}\n            </Select.Item>\n          );\n        })}\n      </Select.Content>\n    </Select.Root>\n  );\n};\n"
  },
  {
    "path": "packages/drawnix/src/components/toolbar/popup-toolbar/link-button.tsx",
    "content": "import React from 'react';\nimport { ToolButton } from '../../tool-button';\nimport classNames from 'classnames';\nimport { useI18n } from '../../../i18n';\nimport { getSelectedElements, PlaitBoard } from '@plait/core';\nimport { LinkIcon } from '../../icons';\nimport { useDrawnix } from '../../../hooks/use-drawnix';\nimport { getFirstTextEditor, LinkElement } from '@plait/common';\nimport { ReactEditor } from 'slate-react';\nimport { LinkEditor } from '@plait/text-plugins';\n\nexport type PopupLinkButtonProps = {\n  board: PlaitBoard;\n  title: string;\n};\n\nexport const PopupLinkButton: React.FC<PopupLinkButtonProps> = ({\n  board,\n  title,\n}) => {\n  const { t } = useI18n();\n  const { appState, setAppState } = useDrawnix();\n  return (\n    <ToolButton\n      className={classNames(`property-button`)}\n      visible={true}\n      selected={\n        appState.linkState?.isEditing ||\n        appState.linkState?.isHovering ||\n        appState.linkState?.isHoveringOrigin\n      }\n      icon={LinkIcon}\n      type=\"button\"\n      title={title}\n      aria-label={title}\n      onPointerUp={() => {\n        const pbElement = getSelectedElements(board)[0];\n        const editor = getFirstTextEditor(pbElement);\n        const linkElementEntry = LinkEditor.getLinkElement(editor);\n        if (!linkElementEntry) {\n          LinkEditor.wrapLink(editor, t('textPlaceholders.link'), '');\n        }\n        setTimeout(() => {\n          const linkElementEntry = LinkEditor.getLinkElement(editor);\n          const linkElement = linkElementEntry[0] as LinkElement;\n          const targetDom = ReactEditor.toDOMNode(editor, linkElement);\n          setAppState({\n            ...appState,\n            linkState: {\n              editor,\n              targetDom: targetDom,\n              targetElement: linkElement,\n              isEditing: true,\n              isHovering: false,\n              isHoveringOrigin: false,\n            },\n          });\n        }, 0);\n      }}\n    ></ToolButton>\n  );\n};\n"
  },
  {
    "path": "packages/drawnix/src/components/toolbar/popup-toolbar/popup-toolbar.scss",
    "content": ".popup-toolbar {\n    .popup-font-size {\n        height: var(--lg-button-size);\n        display: flex;\n        align-items: center;\n        gap: 2px;\n        padding: 0 4px 0 4px;\n        border-radius: var(--border-radius-sm);\n        background-color: var(--color-surface-secondary-container);\n        color: var(--color-on-surface);\n        &:not([data-disable-hover]):hover {\n            background-color: var(--color-surface-primary-container);\n        }\n        .popup-font-size__input {\n            width: 36px;\n            height: 100%;\n            border: none;\n            outline: none;\n            padding: 0;\n            background: transparent;\n            color: inherit;\n            font-size: 14px;\n            text-align: center;\n            appearance: textfield;\n            -moz-appearance: textfield;\n            &::-webkit-outer-spin-button,\n            &::-webkit-inner-spin-button {\n                -webkit-appearance: none;\n                margin: 0;\n            }\n        }\n        .popup-font-size__stepper {\n            height: 100%;\n            width: 16px;\n            display: flex;\n            flex-direction: column;\n            align-items: stretch;\n            justify-content: stretch;\n        }\n        .popup-font-size__stepper-button {\n            flex: 1;\n            border: 0;\n            outline: none;\n            padding: 0;\n            background: transparent;\n            color: inherit;\n            cursor: pointer;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n        }\n        .popup-font-size__stepper-icon {\n            width: 12px;\n            height: 12px;\n            display: block;\n        }\n        .popup-font-size__stepper-button:first-child .popup-font-size__stepper-icon {\n            transform: translateY(0.5px);\n        }\n        .popup-font-size__stepper-button:last-child .popup-font-size__stepper-icon {\n            transform: translateY(-0.5px);\n        }\n    }\n    .property-button {\n        height: var(--lg-button-size);\n        width: var(--lg-button-size);\n        .color-label {\n            cursor: pointer;\n        }\n        .fill-label {\n            display: inline-block;\n            width: var(--popup-label-size);\n            height: var(--popup-label-size);\n            border-radius: 50%;\n            &.color-white {\n                border: 1px solid var(--color-gray-30);\n            }\n        }\n        .stroke-label {\n            border-radius: 50%;\n            width: calc(var(--popup-label-size) - var(--border-radius-lg));\n            height: calc(var(--popup-label-size) - var(--border-radius-lg));\n            border-width: var(--border-radius-sm);\n            border-style: solid;\n        }\n        .tool-icon__icon {\n            svg {\n                width: var(--xlg-icon-size);\n                height: var(--xlg-icon-size);\n            }\n        }\n    }\n}\n.stroke-setting {\n    &.has-stroke-style {\n        padding-top: 8px !important;\n    }\n    .stroke-style-picker {\n        justify-content: space-between;\n        padding: 0 8px;\n    }\n}\n\n.source-arrow-island .property-button ,.source-arrow-button{\n   transform: rotateY(180deg);\n}\n"
  },
  {
    "path": "packages/drawnix/src/components/toolbar/popup-toolbar/popup-toolbar.tsx",
    "content": "import Stack from '../../stack';\nimport { FontColorIcon } from '../../icons';\nimport {\n  ATTACHED_ELEMENT_CLASS_NAME,\n  getRectangleByElements,\n  getSelectedElements,\n  isDragging,\n  isMovingElements,\n  isSelectionMoving,\n  PlaitBoard,\n  PlaitElement,\n  RectangleClient,\n  toHostPointFromViewBoxPoint,\n  toScreenPointFromHostPoint,\n} from '@plait/core';\nimport { useEffect, useRef, useState } from 'react';\nimport { useBoard } from '@plait-board/react-board';\nimport { flip, offset, useFloating } from '@floating-ui/react';\nimport { Island } from '../../island';\nimport classNames from 'classnames';\nimport { useI18n } from '../../../i18n';\nimport {\n  getStrokeColorByElement as getStrokeColorByMindElement,\n  MindElement,\n} from '@plait/mind';\nimport './popup-toolbar.scss';\nimport {\n  ArrowLineHandle,\n  getStrokeColorByElement as getStrokeColorByDrawElement,\n  getStrokeStyleByElement,\n  isClosedCustomGeometry,\n  isClosedDrawElement,\n  isDrawElementsIncludeText,\n  PlaitDrawElement,\n} from '@plait/draw';\nimport { CustomText, StrokeStyle } from '@plait/common';\nimport { getTextMarksByElement } from '@plait/text-plugins';\nimport { PopupFontColorButton } from './font-color-button';\nimport { PopupFontSizeControl } from './font-size-control';\nimport { PopupStrokeButton } from './stroke-button';\nimport { PopupFillButton } from './fill-button';\nimport { isWhite, removeHexAlpha } from '../../../utils/color';\nimport { NO_COLOR } from '../../../constants/color';\nimport { Freehand } from '../../../plugins/freehand/type';\nimport { PopupLinkButton } from './link-button';\nimport { ArrowMarkButton } from './arrow-mark-button';\n\nexport const PopupToolbar = () => {\n  const board = useBoard();\n  const { t } = useI18n();\n  const selectedElements = getSelectedElements(board);\n  const [movingOrDragging, setMovingOrDragging] = useState(false);\n  const movingOrDraggingRef = useRef(movingOrDragging);\n  const open =\n    selectedElements.length > 0 &&\n    !isSelectionMoving(board) &&\n    !selectedElements.some(PlaitDrawElement.isImage);\n  const { viewport, selection, children } = board;\n  const { refs, floatingStyles } = useFloating({\n    placement: 'right-start',\n    middleware: [offset(32), flip()],\n  });\n  let state: {\n    fill: string | undefined;\n    strokeColor?: string;\n    strokeStyle?: StrokeStyle;\n    hasFill?: boolean;\n    hasText?: boolean;\n    fontColor?: string;\n    hasFontColor?: boolean;\n    hasStroke?: boolean;\n    hasStrokeStyle?: boolean;\n    marks?: Omit<CustomText, 'text'>;\n    // Line state\n    isLine?: boolean;\n    source?: ArrowLineHandle;\n    target?: ArrowLineHandle;\n  } = {\n    fill: 'red',\n  };\n  if (open && !movingOrDragging) {\n    const hasFill =\n      selectedElements.some((value) => hasFillProperty(board, value)) &&\n      !PlaitBoard.hasBeenTextEditing(board);\n    const hasText = selectedElements.some((value) =>\n      hasTextProperty(board, value)\n    );\n    const hasStroke =\n      selectedElements.some((value) => hasStrokeProperty(board, value)) &&\n      !PlaitBoard.hasBeenTextEditing(board);\n    const hasStrokeStyle =\n      selectedElements.some((value) => hasStrokeStyleProperty(board, value)) &&\n      !PlaitBoard.hasBeenTextEditing(board);\n    const isLine = selectedElements.every((value) =>\n      PlaitDrawElement.isArrowLine(value)\n    );\n    state = {\n      ...getElementState(board),\n      hasFill,\n      hasFontColor: hasText,\n      hasStroke,\n      hasStrokeStyle,\n      hasText,\n      isLine,\n    };\n  }\n  useEffect(() => {\n    if (open) {\n      const hasSelected = selectedElements.length > 0;\n      if (!movingOrDragging && hasSelected) {\n        const elements = getSelectedElements(board);\n        const rectangle = getRectangleByElements(board, elements, false);\n        const [start, end] = RectangleClient.getPoints(rectangle);\n        const screenStart = toScreenPointFromHostPoint(\n          board,\n          toHostPointFromViewBoxPoint(board, start)\n        );\n        const screenEnd = toScreenPointFromHostPoint(\n          board,\n          toHostPointFromViewBoxPoint(board, end)\n        );\n        const width = screenEnd[0] - screenStart[0];\n        const height = screenEnd[1] - screenStart[1];\n        refs.setPositionReference({\n          getBoundingClientRect() {\n            return {\n              width,\n              height,\n              x: screenStart[0],\n              y: screenStart[1],\n              top: screenStart[1],\n              left: screenStart[0],\n              right: screenStart[0] + width,\n              bottom: screenStart[1] + height,\n            };\n          },\n        });\n      }\n    }\n  }, [viewport, selection, children, movingOrDragging]);\n\n  useEffect(() => {\n    movingOrDraggingRef.current = movingOrDragging;\n  }, [movingOrDragging]);\n\n  useEffect(() => {\n    const { pointerUp, pointerMove } = board;\n\n    board.pointerMove = (event: PointerEvent) => {\n      if (\n        (isMovingElements(board) || isDragging(board)) &&\n        !movingOrDraggingRef.current\n      ) {\n        setMovingOrDragging(true);\n      }\n      pointerMove(event);\n    };\n\n    board.pointerUp = (event: PointerEvent) => {\n      if (\n        movingOrDraggingRef.current &&\n        (isMovingElements(board) || isDragging(board))\n      ) {\n        setMovingOrDragging(false);\n      }\n      pointerUp(event);\n    };\n\n    return () => {\n      board.pointerUp = pointerUp;\n      board.pointerMove = pointerMove;\n    };\n  }, [board]);\n\n  return (\n    <>\n      {open && !movingOrDragging && (\n        <Island\n          padding={1}\n          className={classNames('popup-toolbar', ATTACHED_ELEMENT_CLASS_NAME)}\n          ref={refs.setFloating}\n          style={floatingStyles}\n        >\n          <Stack.Row gap={1}>\n            {state.hasText && (\n              <PopupFontSizeControl\n                board={board}\n                key={'font-size'}\n                currentFontSize={getFontSizeFromMarks(state.marks)}\n                title={t('popupToolbar.fontSize')}\n              />\n            )}\n            {state.hasFontColor && (\n              <PopupFontColorButton\n                board={board}\n                key={0}\n                currentColor={state.marks?.color}\n                title={t('popupToolbar.fontColor')}\n                fontColorIcon={\n                  <FontColorIcon currentColor={state.marks?.color} />\n                }\n              ></PopupFontColorButton>\n            )}\n            {state.hasStroke && (\n              <PopupStrokeButton\n                board={board}\n                key={1}\n                currentColor={state.strokeColor}\n                currentStyle={state.strokeStyle}\n                title={t('popupToolbar.stroke')}\n                hasStrokeStyle={state.hasStrokeStyle || false}\n              >\n                <label\n                  className={classNames('stroke-label', 'color-label')}\n                  style={{ borderColor: state.strokeColor }}\n                ></label>\n              </PopupStrokeButton>\n            )}\n            {state.hasFill && (\n              <PopupFillButton\n                board={board}\n                key={2}\n                currentColor={state.fill}\n                title={t('popupToolbar.fillColor')}\n              >\n                <label\n                  className={classNames('fill-label', 'color-label', {\n                    'color-white':\n                      state.fill && isWhite(removeHexAlpha(state.fill)),\n                  })}\n                  style={{ backgroundColor: state.fill }}\n                ></label>\n              </PopupFillButton>\n            )}\n            {state.hasText && (\n              <PopupLinkButton\n                board={board}\n                key={3}\n                title={t('popupToolbar.link')}\n              ></PopupLinkButton>\n            )}\n            {state.isLine && (\n              <>\n                <ArrowMarkButton\n                  board={board}\n                  key={4}\n                  end={'source'}\n                  endProperty={state.source}\n                />\n                <ArrowMarkButton\n                  board={board}\n                  key={5}\n                  end={'target'}\n                  endProperty={state.target}\n                />\n              </>\n            )}\n          </Stack.Row>\n        </Island>\n      )}\n    </>\n  );\n};\n\nexport const getMindElementState = (\n  board: PlaitBoard,\n  element: MindElement\n) => {\n  const marks = getTextMarksByElement(element);\n  return {\n    fill: element.fill,\n    strokeColor: getStrokeColorByMindElement(board, element),\n    strokeStyle: getStrokeStyleByElement(board, element),\n    marks,\n  };\n};\n\nexport const getDrawElementState = (\n  board: PlaitBoard,\n  element: PlaitDrawElement\n) => {\n  const marks: Omit<CustomText, 'text'> = getTextMarksByElement(element);\n  return {\n    fill: element.fill,\n    strokeColor: getStrokeColorByDrawElement(board, element),\n    strokeStyle: getStrokeStyleByElement(board, element),\n    marks,\n    source: element?.source || {},\n    target: element?.target || {},\n  };\n};\n\nexport const getElementState = (board: PlaitBoard) => {\n  const selectedElement = getSelectedElements(board)[0];\n  if (MindElement.isMindElement(board, selectedElement)) {\n    return getMindElementState(board, selectedElement);\n  }\n  return getDrawElementState(board, selectedElement as PlaitDrawElement);\n};\n\nexport const hasFillProperty = (board: PlaitBoard, element: PlaitElement) => {\n  if (MindElement.isMindElement(board, element)) {\n    return true;\n  }\n  if (isClosedCustomGeometry(board, element)) {\n    return true;\n  }\n  if (PlaitDrawElement.isDrawElement(element)) {\n    return (\n      PlaitDrawElement.isShapeElement(element) &&\n      !PlaitDrawElement.isImage(element) &&\n      !PlaitDrawElement.isText(element) &&\n      isClosedDrawElement(element)\n    );\n  }\n  return false;\n};\n\nexport const hasStrokeProperty = (board: PlaitBoard, element: PlaitElement) => {\n  if (MindElement.isMindElement(board, element)) {\n    return true;\n  }\n  if (Freehand.isFreehand(element)) {\n    return true;\n  }\n  if (PlaitDrawElement.isDrawElement(element)) {\n    return (\n      (PlaitDrawElement.isShapeElement(element) &&\n        !PlaitDrawElement.isImage(element) &&\n        !PlaitDrawElement.isText(element)) ||\n      PlaitDrawElement.isArrowLine(element) ||\n      PlaitDrawElement.isVectorLine(element) ||\n      PlaitDrawElement.isTable(element)\n    );\n  }\n  return false;\n};\n\nexport const hasStrokeStyleProperty = (\n  board: PlaitBoard,\n  element: PlaitElement\n) => {\n  return hasStrokeProperty(board, element);\n};\n\nexport const hasTextProperty = (board: PlaitBoard, element: PlaitElement) => {\n  if (MindElement.isMindElement(board, element)) {\n    return true;\n  }\n  if (PlaitDrawElement.isDrawElement(element)) {\n    return isDrawElementsIncludeText([element]);\n  }\n  return false;\n};\n\nexport const getColorPropertyValue = (color: string) => {\n  if (color === NO_COLOR) {\n    return null;\n  } else {\n    return color;\n  }\n};\n\nconst getFontSizeFromMarks = (marks?: Omit<CustomText, 'text'>) => {\n  const value = (marks as any)?.['font-size'];\n  const size = typeof value === 'number' ? value : Number(value);\n  return Number.isFinite(size) && size > 0 ? size : undefined;\n};\n"
  },
  {
    "path": "packages/drawnix/src/components/toolbar/popup-toolbar/stroke-button.tsx",
    "content": "import React, { useState } from 'react';\nimport { ToolButton } from '../../tool-button';\nimport classNames from 'classnames';\nimport { ATTACHED_ELEMENT_CLASS_NAME, PlaitBoard } from '@plait/core';\nimport { Island } from '../../island';\nimport { ColorPicker } from '../../color-picker';\nimport {\n  hexAlphaToOpacity,\n  isFullyTransparent,\n  isWhite,\n  removeHexAlpha,\n} from '../../../utils/color';\nimport {\n  StrokeIcon,\n  StrokeStyleDashedIcon,\n  StrokeStyleDotedIcon,\n  StrokeStyleNormalIcon,\n  StrokeWhiteIcon,\n} from '../../icons';\nimport { Popover, PopoverContent, PopoverTrigger } from '../../popover/popover';\nimport Stack from '../../stack';\nimport { PropertyTransforms, StrokeStyle } from '@plait/common';\nimport { getMemorizeKey } from '@plait/draw';\nimport {\n  setStrokeColor,\n  setStrokeColorOpacity,\n} from '../../../transforms/property';\nimport { useI18n } from '../../../i18n';\n\nexport type PopupStrokeButtonProps = {\n  board: PlaitBoard;\n  currentColor: string | undefined;\n  currentStyle?: StrokeStyle;\n  title: string;\n  hasStrokeStyle: boolean;\n  children?: React.ReactNode;\n};\n\nexport const PopupStrokeButton: React.FC<PopupStrokeButtonProps> = ({\n  board,\n  currentColor,\n  currentStyle,\n  title,\n  hasStrokeStyle,\n  children,\n}) => {\n  const [isStrokePropertyOpen, setIsStrokePropertyOpen] = useState(false);\n  const hexColor = currentColor && removeHexAlpha(currentColor);\n  const opacity = currentColor ? hexAlphaToOpacity(currentColor) : 100;\n  const container = PlaitBoard.getBoardContainer(board);\n  const { t } = useI18n();\n\n  const icon = isFullyTransparent(opacity)\n    ? StrokeIcon\n    : isWhite(hexColor)\n    ? StrokeWhiteIcon\n    : undefined;\n\n  const setStrokeStyle = (style: StrokeStyle) => {\n    PropertyTransforms.setStrokeStyle(board, style, { getMemorizeKey });\n  };\n\n  return (\n    <Popover\n      sideOffset={12}\n      open={isStrokePropertyOpen}\n      onOpenChange={(open) => {\n        setIsStrokePropertyOpen(open);\n      }}\n      placement={'top'}\n    >\n      <PopoverTrigger asChild>\n        <ToolButton\n          className={classNames(`property-button`)}\n          visible={true}\n          selected={isStrokePropertyOpen}\n          icon={icon}\n          type=\"button\"\n          title={title}\n          aria-label={title}\n          onPointerUp={() => {\n            setIsStrokePropertyOpen(!isStrokePropertyOpen);\n          }}\n        >\n          {!icon && children}\n        </ToolButton>\n      </PopoverTrigger>\n      <PopoverContent container={container}>\n        <Island\n          padding={4}\n          className={classNames(\n            `${ATTACHED_ELEMENT_CLASS_NAME}`,\n            'stroke-setting',\n            { 'has-stroke-style': hasStrokeStyle }\n          )}\n        >\n          <Stack.Col>\n            {hasStrokeStyle && (\n              <Stack.Row className={classNames('stroke-style-picker')}>\n                <ToolButton\n                  visible={true}\n                  selected={!currentStyle || currentStyle === StrokeStyle.solid}\n                  icon={StrokeStyleNormalIcon}\n                  type=\"button\"\n                  title={`${title} — ${t('stroke.solid')}`}\n                  aria-label={`${title} — ${t('stroke.solid')}`}\n                  onPointerUp={() => {\n                    setStrokeStyle(StrokeStyle.solid);\n                  }}\n                ></ToolButton>\n                <ToolButton\n                  visible={true}\n                  selected={currentStyle === StrokeStyle.dashed}\n                  icon={StrokeStyleDashedIcon}\n                  type=\"button\"\n                  title={`${title} — ${t('stroke.dashed')}`}\n                  aria-label={`${title} — ${t('stroke.dashed')}`}\n                  onPointerUp={() => {\n                    setStrokeStyle(StrokeStyle.dashed);\n                  }}\n                ></ToolButton>\n                <ToolButton\n                  visible={true}\n                  selected={currentStyle === StrokeStyle.dotted}\n                  icon={StrokeStyleDotedIcon}\n                  type=\"button\"\n                  title={`${title} — ${t('stroke.dotted')}`}\n                  aria-label={`${title} — ${t('stroke.dotted')}`}\n                  onPointerUp={() => {\n                    setStrokeStyle(StrokeStyle.dotted);\n                  }}\n                ></ToolButton>\n              </Stack.Row>\n            )}\n            <ColorPicker\n              onColorChange={(selectedColor: string) => {\n                setStrokeColor(board, selectedColor);\n              }}\n              onOpacityChange={(opacity: number) => {\n                setStrokeColorOpacity(board, opacity);\n              }}\n              currentColor={currentColor}\n            ></ColorPicker>\n          </Stack.Col>\n        </Island>\n      </PopoverContent>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "packages/drawnix/src/components/toolbar/theme-toolbar.tsx",
    "content": "import { useBoard } from '@plait-board/react-board';\nimport classNames from 'classnames';\nimport {\n  ATTACHED_ELEMENT_CLASS_NAME,\n  BoardTransforms,\n  ThemeColorMode,\n} from '@plait/core';\nimport { Island } from '../island';\nimport { useI18n } from '../../i18n';\n\nexport const ThemeToolbar = () => {\n  const board = useBoard();\n  const { t } = useI18n();\n  const theme = board.theme;\n  return (\n    <Island\n      padding={1}\n      className={classNames('theme-toolbar', ATTACHED_ELEMENT_CLASS_NAME)}\n    >\n      <select\n        onChange={(e) => {\n          const value = (e.target as HTMLSelectElement).value;\n          BoardTransforms.updateThemeColor(board, value as ThemeColorMode);\n        }}\n        value={theme.themeColorMode}\n      >\n        <option value=\"default\">{t('theme.default')}</option>\n        <option value=\"colorful\">{t('theme.colorful')}</option>\n        <option value=\"soft\">{t('theme.soft')}</option>\n        <option value=\"retro\">{t('theme.retro')}</option>\n        <option value=\"dark\">{t('theme.dark')}</option>\n        <option value=\"starry\">{t('theme.starry')}</option>\n      </select>\n    </Island>\n  );\n};\n"
  },
  {
    "path": "packages/drawnix/src/components/toolbar/zoom-toolbar.tsx",
    "content": "import { useBoard } from '@plait-board/react-board';\nimport Stack from '../stack';\nimport { ToolButton } from '../tool-button';\nimport {  ZoomInIcon, ZoomOutIcon } from '../icons';\nimport classNames from 'classnames';\nimport {\n  ATTACHED_ELEMENT_CLASS_NAME,\n  BoardTransforms,\n  PlaitBoard,\n} from '@plait/core';\nimport { Island } from '../island';\nimport { Popover, PopoverContent, PopoverTrigger } from '../popover/popover';\nimport { useState } from 'react';\nimport Menu from '../menu/menu';\nimport MenuItem from '../menu/menu-item';\nimport { useI18n } from '../../i18n';\n\nexport const ZoomToolbar = () => {\n  const board = useBoard();\n  const { t } = useI18n();\n  const container = PlaitBoard.getBoardContainer(board);\n  const [zoomMenuOpen, setZoomMenuOpen] = useState(false);\n  return (\n    <Island\n      padding={1}\n      className={classNames('zoom-toolbar', ATTACHED_ELEMENT_CLASS_NAME)}\n    >\n      <Stack.Row gap={1}>\n        <ToolButton\n          key={0}\n          type=\"button\"\n          icon={ZoomOutIcon}\n          visible={true}\n          title={t('zoom.out')}\n          aria-label={t('zoom.out')}\n          onPointerUp={() => {\n            BoardTransforms.updateZoom(board, board.viewport.zoom - 0.1);\n          }}\n          className=\"zoom-out-button\"\n        />\n        <Popover\n          sideOffset={12}\n          open={zoomMenuOpen}\n          onOpenChange={(open) => {\n            setZoomMenuOpen(open);\n          }}\n          placement=\"bottom-end\"\n        >\n          <PopoverTrigger asChild>\n            <div\n              key={1}\n              title={t('zoom.fit')}\n              aria-label={t('zoom.fit')}\n              className={classNames('zoom-menu-trigger', {\n                active: zoomMenuOpen,\n              })}\n              onPointerUp={() => {\n                setZoomMenuOpen(!zoomMenuOpen);\n              }}\n            >\n              {Number(((board?.viewport?.zoom || 1) * 100).toFixed(0))}%\n            </div>\n          </PopoverTrigger>\n          <PopoverContent container={container}>\n            <Menu\n              onSelect={() => {\n                setZoomMenuOpen(false);\n              }}\n            >\n              <MenuItem\n                data-testid=\"open-button\"\n                onSelect={() => {\n                  BoardTransforms.fitViewport(board);\n                }}\n                aria-label={t('zoom.fit')}\n                shortcut={`Cmd+Shift+=`}\n              >{t('zoom.fit')}</MenuItem>\n              <MenuItem\n                data-testid=\"open-button\"\n                onSelect={() => {\n                  BoardTransforms.updateZoom(board, 1);\n                }}\n                aria-label={t('zoom.100')}\n                shortcut={`Cmd+0`}\n              >{t('zoom.100')}</MenuItem>\n             \n            </Menu>\n          </PopoverContent>\n        </Popover>\n        <ToolButton\n          key={2}\n          type=\"button\"\n          icon={ZoomInIcon}\n          visible={true}\n          title={t('zoom.in')}\n          aria-label={t('zoom.in')}\n          onPointerUp={() => {\n            BoardTransforms.updateZoom(board, board.viewport.zoom + 0.1);\n          }}\n          className=\"zoom-in-button\"\n        />\n      </Stack.Row>\n    </Island>\n  );\n};\n\n"
  },
  {
    "path": "packages/drawnix/src/components/ttd-dialog/markdown-to-drawnix.tsx",
    "content": "import { useState, useEffect, useDeferredValue } from 'react';\nimport './mermaid-to-drawnix.scss';\nimport './ttd-dialog.scss';\nimport { TTDDialogPanels } from './ttd-dialog-panels';\nimport { TTDDialogPanel } from './ttd-dialog-panel';\nimport { TTDDialogInput } from './ttd-dialog-input';\nimport { TTDDialogOutput } from './ttd-dialog-output';\nimport { TTDDialogSubmitShortcut } from './ttd-dialog-submit-shortcut';\nimport { useDrawnix } from '../../hooks/use-drawnix';\nimport { useI18n } from '../../i18n';\nimport { useBoard } from '@plait-board/react-board';\nimport {\n  getViewportOrigination,\n  PlaitBoard,\n  PlaitElement,\n  WritableClipboardOperationType,\n} from '@plait/core';\nimport { MindElement } from '@plait/mind';\n\nexport interface MarkdownToDrawnixLibProps {\n  loaded: boolean;\n  api: Promise<{\n    parseMarkdownToDrawnix: (\n      definition: string,\n      mainTopic?: string\n    ) => MindElement;\n  }>;\n}\n\nconst MarkdownToDrawnix = () => {\n  const { appState, setAppState } = useDrawnix();\n  const { t, language } = useI18n();\n  const [markdownToDrawnixLib, setMarkdownToDrawnixLib] =\n    useState<MarkdownToDrawnixLibProps>({\n      loaded: false,\n      api: Promise.resolve({\n        parseMarkdownToDrawnix: (definition: string, mainTopic?: string) =>\n          null as any as MindElement,\n      }),\n    });\n\n  useEffect(() => {\n    const loadLib = async () => {\n      try {\n        const module = await import('@plait-board/markdown-to-drawnix');\n        setMarkdownToDrawnixLib({\n          loaded: true,\n          api: Promise.resolve(module),\n        });\n      } catch (err) {\n        console.error('Failed to load mermaid library:', err);\n        setError(new Error(t('dialog.error.loadMermaid')));\n      }\n    };\n    loadLib();\n  }, []);\n  const [text, setText] = useState(() => t('markdown.example'));\n  const [value, setValue] = useState<PlaitElement[]>(() => []);\n  const deferredText = useDeferredValue(text.trim());\n  const [error, setError] = useState<Error | null>(null);\n  const board = useBoard();\n   \n  // Update markdown example when language changes\n  useEffect(() => {\n    setText(t('markdown.example'));\n  }, [language]);\n\n  useEffect(() => {\n    const convertMarkdown = async () => {\n      try {\n        const api = await markdownToDrawnixLib.api;\n        let ret;\n        try {\n          ret = await api.parseMarkdownToDrawnix(deferredText);\n        } catch (err: any) {\n          ret = await api.parseMarkdownToDrawnix(\n            deferredText.replace(/\"/g, \"'\")\n          );\n        }\n        const mind = ret;\n        mind.points = [[0, 0]];\n        if (mind) {\n          setValue([mind]);\n          setError(null);\n        }\n      } catch (err: any) {\n        setError(err);\n      }\n    };\n    convertMarkdown();\n  }, [deferredText, markdownToDrawnixLib]);\n\n  const insertToBoard = () => {\n    if (!value.length) {\n      return;\n    }\n    const boardContainerRect =\n      PlaitBoard.getBoardContainer(board).getBoundingClientRect();\n    const focusPoint = [\n      boardContainerRect.width / 4,\n      boardContainerRect.height / 2 - 20,\n    ];\n    const zoom = board.viewport.zoom;\n    const origination = getViewportOrigination(board);\n    const focusX = origination![0] + focusPoint[0] / zoom;\n    const focusY = origination![1] + focusPoint[1] / zoom;\n    const elements = value;\n    board.insertFragment(\n      {\n        elements: JSON.parse(JSON.stringify(elements)),\n      },\n      [focusX, focusY],\n      WritableClipboardOperationType.paste\n    );\n    setAppState({ ...appState, openDialogType: null });\n  };\n\n  return (\n    <>\n      <div className=\"ttd-dialog-desc\">\n        {t('dialog.markdown.description')}\n      </div>\n      <TTDDialogPanels>\n        <TTDDialogPanel label={t('dialog.markdown.syntax')}>\n          <TTDDialogInput\n            input={text}\n            placeholder={t('dialog.markdown.placeholder')}\n            onChange={(event) => setText(event.target.value)}\n            onKeyboardSubmit={() => {\n              insertToBoard();\n            }}\n          />\n        </TTDDialogPanel>\n        <TTDDialogPanel\n          label={t('dialog.markdown.preview')}\n          panelAction={{\n            action: () => {\n              insertToBoard();\n            },\n            label: t('dialog.markdown.insert'),\n          }}\n          renderSubmitShortcut={() => <TTDDialogSubmitShortcut />}\n        >\n          <TTDDialogOutput\n            value={value}\n            loaded={markdownToDrawnixLib.loaded}\n            error={error}\n          />\n        </TTDDialogPanel>\n      </TTDDialogPanels>\n    </>\n  );\n};\nexport default MarkdownToDrawnix;\n"
  },
  {
    "path": "packages/drawnix/src/components/ttd-dialog/mermaid-to-drawnix.scss",
    "content": ".drawnix {\n  .dialog-mermaid {\n    &-title {\n      margin-block: 0.25rem;\n      font-size: 1.25rem;\n      font-weight: 700;\n      padding-inline: 2.5rem;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/drawnix/src/components/ttd-dialog/mermaid-to-drawnix.tsx",
    "content": "import { useState, useEffect, useDeferredValue } from 'react';\nimport './mermaid-to-drawnix.scss';\nimport './ttd-dialog.scss';\nimport { TTDDialogPanels } from './ttd-dialog-panels';\nimport { TTDDialogPanel } from './ttd-dialog-panel';\nimport { TTDDialogInput } from './ttd-dialog-input';\nimport { TTDDialogOutput } from './ttd-dialog-output';\nimport { TTDDialogSubmitShortcut } from './ttd-dialog-submit-shortcut';\nimport { useDrawnix } from '../../hooks/use-drawnix';\nimport { useI18n } from '../../i18n';\nimport { useBoard } from '@plait-board/react-board';\nimport {\n  getViewportOrigination,\n  PlaitBoard,\n  PlaitElement,\n  PlaitGroupElement,\n  Point,\n  RectangleClient,\n  WritableClipboardOperationType,\n} from '@plait/core';\nimport type { MermaidConfig } from '@plait-board/mermaid-to-drawnix/dist';\nimport type { MermaidToDrawnixResult } from '@plait-board/mermaid-to-drawnix/dist/interfaces';\n\nexport interface MermaidToDrawnixLibProps {\n  loaded: boolean;\n  api: Promise<{\n    parseMermaidToDrawnix: (\n      definition: string,\n      config?: MermaidConfig\n    ) => Promise<MermaidToDrawnixResult>;\n  }>;\n}\n\nconst MERMAID_EXAMPLE =\n  'flowchart TD\\n A[Christmas] -->|Get money| B(Go shopping)\\n B --> C{Let me think}\\n C -->|One| D[Laptop]\\n C -->|Two| E[iPhone]\\n C -->|Three| F[Car]';\n\nconst MermaidToDrawnix = () => {\n  const { appState, setAppState } = useDrawnix();\n  const { t, language } = useI18n();\n  const [mermaidToDrawnixLib, setMermaidToDrawnixLib] =\n    useState<MermaidToDrawnixLibProps>({\n      loaded: false,\n      api: Promise.resolve({\n        parseMermaidToDrawnix: async () => ({ elements: [] }),\n      }),\n    });\n\n  useEffect(() => {\n    const loadLib = async () => {\n      try {\n        const module = await import('@plait-board/mermaid-to-drawnix');\n        setMermaidToDrawnixLib({\n          loaded: true,\n          api: Promise.resolve(module),\n        });\n      } catch (err) {\n        console.error('Failed to load mermaid library:', err);\n        setError(new Error(t('dialog.error.loadMermaid')));\n      }\n    };\n    loadLib();\n  }, []);\n  const [text, setText] = useState(() => MERMAID_EXAMPLE);\n  const [value, setValue] = useState<PlaitElement[]>(() => []);\n  const deferredText = useDeferredValue(text.trim());\n  const [error, setError] = useState<Error | null>(null);\n  const board = useBoard();\n\n  useEffect(() => {\n    const convertMermaid = async () => {\n      try {\n        const api = await mermaidToDrawnixLib.api;\n        let ret;\n        try {\n          ret = await api.parseMermaidToDrawnix(deferredText);\n        } catch (err: any) {\n          ret = await api.parseMermaidToDrawnix(\n            deferredText.replace(/\"/g, \"'\")\n          );\n        }\n        const { elements } = ret;\n        setValue(elements);\n        setError(null);\n      } catch (err: any) {\n        setError(err);\n      }\n    };\n    convertMermaid();\n  }, [deferredText, mermaidToDrawnixLib]);\n\n  const insertToBoard = () => {\n    if (!value.length) {\n      return;\n    }\n    const boardContainerRect =\n      PlaitBoard.getBoardContainer(board).getBoundingClientRect();\n    const focusPoint = [\n      boardContainerRect.width / 2,\n      boardContainerRect.height / 2,\n    ];\n    const zoom = board.viewport.zoom;\n    const origination = getViewportOrigination(board);\n    const centerX = origination![0] + focusPoint[0] / zoom;\n    const centerY = origination![1] + focusPoint[1] / zoom;\n    const elements = value;\n    const elementRectangle = RectangleClient.getBoundingRectangle(\n      elements\n        .filter((ele) => !PlaitGroupElement.isGroup(ele))\n        .map((ele) =>\n          RectangleClient.getRectangleByPoints(ele.points as Point[])\n        )\n    );\n    const startPoint = [\n      centerX - elementRectangle.width / 2,\n      centerY - elementRectangle.height / 2,\n    ] as Point;\n    board.insertFragment(\n      {\n        elements: JSON.parse(JSON.stringify(elements)),\n      },\n      startPoint,\n      WritableClipboardOperationType.paste\n    );\n    setAppState({ ...appState, openDialogType: null });\n  };\n\n  return (\n    <>\n      <div className=\"ttd-dialog-desc\">\n        {language === 'zh' ? (\n          <>\n            {t('dialog.mermaid.description')}\n            {' '}\n            <a\n              href=\"https://mermaid.js.org/syntax/flowchart.html\"\n              target=\"_blank\"\n              rel=\"noreferrer\"\n            >\n              {t('dialog.mermaid.flowchart')}\n            </a>\n            、\n            <a\n              href=\"https://mermaid.js.org/syntax/sequenceDiagram.html\"\n              target=\"_blank\"\n              rel=\"noreferrer\"\n            >\n              {t('dialog.mermaid.sequence')}\n            </a>\n            {' '}\n            和\n            {' '}\n            <a\n              href=\"https://mermaid.js.org/syntax/classDiagram.html\"\n              target=\"_blank\"\n              rel=\"noreferrer\"\n            >\n              {t('dialog.mermaid.class')}\n            </a>\n            {t('dialog.mermaid.otherTypes')}\n          </>\n        ) : (\n          <>\n            {t('dialog.mermaid.description')}\n            {' '}\n            <a\n              href=\"https://mermaid.js.org/syntax/flowchart.html\"\n              target=\"_blank\"\n              rel=\"noreferrer\"\n            >\n              {t('dialog.mermaid.flowchart')}\n            </a>\n            ,{' '}\n            <a\n              href=\"https://mermaid.js.org/syntax/sequenceDiagram.html\"\n              target=\"_blank\"\n              rel=\"noreferrer\"\n            >\n              {t('dialog.mermaid.sequence')}\n            </a>\n            ,{' '}\n            <a\n              href=\"https://mermaid.js.org/syntax/classDiagram.html\"\n              target=\"_blank\"\n              rel=\"noreferrer\"\n            >\n              {t('dialog.mermaid.class')}\n            </a>\n            {t('dialog.mermaid.otherTypes')}\n          </>\n        )}\n      </div>\n      <TTDDialogPanels>\n        <TTDDialogPanel label={t('dialog.mermaid.syntax')}>\n          <TTDDialogInput\n            input={text}\n            placeholder={t('dialog.mermaid.placeholder')}\n            onChange={(event) => setText(event.target.value)}\n            onKeyboardSubmit={() => {\n              insertToBoard();\n            }}\n          />\n        </TTDDialogPanel>\n        <TTDDialogPanel\n          label={t('dialog.mermaid.preview')}\n          panelAction={{\n            action: () => {\n              insertToBoard();\n            },\n            label: t('dialog.mermaid.insert'),\n          }}\n          renderSubmitShortcut={() => <TTDDialogSubmitShortcut />}\n        >\n          <TTDDialogOutput\n            value={value}\n            loaded={mermaidToDrawnixLib.loaded}\n            error={error}\n          />\n        </TTDDialogPanel>\n      </TTDDialogPanels>\n    </>\n  );\n};\nexport default MermaidToDrawnix;\n"
  },
  {
    "path": "packages/drawnix/src/components/ttd-dialog/ttd-dialog-input.tsx",
    "content": "import type { ChangeEventHandler } from \"react\";\nimport { useEffect, useRef } from \"react\";\nimport { EVENT } from \"../../constants\";\nimport { KEYS } from \"../../keys\";\n\ninterface TTDDialogInputProps {\n  input: string;\n  placeholder: string;\n  onChange: ChangeEventHandler<HTMLTextAreaElement>;\n  onKeyboardSubmit?: () => void;\n}\n\nexport const TTDDialogInput = ({\n  input,\n  placeholder,\n  onChange,\n  onKeyboardSubmit,\n}: TTDDialogInputProps) => {\n  const ref = useRef<HTMLTextAreaElement>(null);\n\n  const callbackRef = useRef(onKeyboardSubmit);\n  callbackRef.current = onKeyboardSubmit;\n\n  useEffect(() => {\n    if (!callbackRef.current) {\n      return;\n    }\n    const textarea = ref.current;\n    if (textarea) {\n      const handleKeyDown = (event: KeyboardEvent) => {\n        if (event[KEYS.CTRL_OR_CMD] && event.key === KEYS.ENTER) {\n          event.preventDefault();\n          callbackRef.current?.();\n        }\n      };\n      textarea.addEventListener(EVENT.KEYDOWN, handleKeyDown);\n      return () => {\n        textarea.removeEventListener(EVENT.KEYDOWN, handleKeyDown);\n      };\n    }\n  }, []);\n\n  return (\n    <textarea\n      className=\"ttd-dialog-input\"\n      onChange={onChange}\n      value={input}\n      placeholder={placeholder}\n      autoFocus\n      ref={ref}\n    />\n  );\n};\n"
  },
  {
    "path": "packages/drawnix/src/components/ttd-dialog/ttd-dialog-output.tsx",
    "content": "import { withGroup } from '@plait/common';\nimport { PlaitElement, PlaitPlugin } from '@plait/core';\nimport { withDraw } from '@plait/draw';\nimport { withCommonPlugin } from '../../plugins/with-common';\nimport { Board, Wrapper } from '@plait-board/react-board';\nimport { MindThemeColors, withMind } from '@plait/mind';\n\nconst ErrorComp = ({ error }: { error: string }) => {\n  return (\n    <div\n      data-testid=\"ttd-dialog-output-error\"\n      className=\"ttd-dialog-output-error\"\n    >\n      Error! <p>{error}</p>\n    </div>\n  );\n};\n\ninterface TTDDialogOutputProps {\n  error: Error | null;\n  value: PlaitElement[];\n  loaded: boolean;\n}\n\nexport const TTDDialogOutput = ({\n  error,\n  value,\n  loaded,\n}: TTDDialogOutputProps) => {\n  const plugins: PlaitPlugin[] = [withDraw, withMind, withGroup, withCommonPlugin];\n  const options = {\n    readonly: true,\n    hideScrollbar: false,\n    disabledScrollOnNonFocus: true,\n    themeColors: MindThemeColors,\n  };\n  return (\n    <div className=\"ttd-dialog-output-wrapper\">\n      {error && <ErrorComp error={error.message} />}\n      {\n        <div\n          style={{ opacity: error ? '0.15' : 1 }}\n          className=\"ttd-dialog-output-canvas-container\"\n        >\n          <Wrapper value={value} options={options} plugins={plugins}>\n            <Board></Board>\n          </Wrapper>\n        </div>\n      }\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/drawnix/src/components/ttd-dialog/ttd-dialog-panel.tsx",
    "content": "import type { ReactNode } from 'react';\nimport classNames from 'classnames';\n\ninterface TTDDialogPanelProps {\n  label: string;\n  children: ReactNode;\n  panelAction?: {\n    label: string;\n    action: () => void;\n    icon?: ReactNode;\n  };\n  panelActionDisabled?: boolean;\n  onTextSubmitInProgress?: boolean;\n  renderTopRight?: () => ReactNode;\n  renderSubmitShortcut?: () => ReactNode;\n  renderBottomRight?: () => ReactNode;\n}\n\nexport const TTDDialogPanel = ({\n  label,\n  children,\n  panelAction,\n  panelActionDisabled = false,\n  onTextSubmitInProgress,\n  renderTopRight,\n  renderSubmitShortcut,\n  renderBottomRight,\n}: TTDDialogPanelProps) => {\n  return (\n    <div className=\"ttd-dialog-panel\">\n      <div className=\"ttd-dialog-panel__header\">\n        <label>{label}</label>\n        {renderTopRight?.()}\n      </div>\n\n      {children}\n      <div\n        className={classNames('ttd-dialog-panel-button-container', {\n          invisible: !panelAction,\n        })}\n        style={{ display: 'flex', alignItems: 'center' }}\n      >\n        <button\n          className=\"ttd-dialog-panel-button drawnix-button \"\n          onClick={panelAction && panelAction.action}\n          disabled={panelActionDisabled || onTextSubmitInProgress}\n        >\n          <div className={classNames({ invisible: onTextSubmitInProgress })}>\n            {panelAction?.label}\n            {panelAction?.icon && <span>{panelAction.icon}</span>}\n          </div>\n        </button>\n        {!panelActionDisabled &&\n          !onTextSubmitInProgress &&\n          renderSubmitShortcut?.()}\n        {renderBottomRight?.()}\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/drawnix/src/components/ttd-dialog/ttd-dialog-panels.tsx",
    "content": "import type { ReactNode } from \"react\";\n\nexport const TTDDialogPanels = ({ children }: { children: ReactNode }) => {\n  return <div className=\"ttd-dialog-panels\">{children}</div>;\n};\n"
  },
  {
    "path": "packages/drawnix/src/components/ttd-dialog/ttd-dialog-submit-shortcut.tsx",
    "content": "import { getShortcutKey } from \"../../utils/common\";\n\nexport const TTDDialogSubmitShortcut = () => {\n  return (\n    <div className=\"ttd-dialog-submit-shortcut\">\n      <div className=\"ttd-dialog-submit-shortcut__key\">\n        {getShortcutKey(\"CtrlOrCmd\")}\n      </div>\n      <div className=\"ttd-dialog-submit-shortcut__key\">\n        {getShortcutKey(\"Enter\")}\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/drawnix/src/components/ttd-dialog/ttd-dialog.scss",
    "content": "@import \"../../styles/variables.module.scss\";\n\n$verticalBreakpoint: 861px;\n\n.drawnix {\n  .Dialog.ttd-dialog {\n    padding: 1.25rem;\n    display: flex;\n    flex-direction: column;\n    width: 100%;\n    max-width: 1024px;\n    height: 100%;\n    max-height: 540px;\n\n    &.Dialog--fullscreen {\n      margin-top: 0;\n    }\n\n    .Island {\n      padding-inline: 0 !important;\n      height: 100%;\n      display: flex;\n      flex-direction: column;\n      flex: 1 1 auto;\n      box-shadow: none;\n    }\n\n    .Modal__content {\n      height: auto;\n      max-height: 100%;\n\n      @media screen and (min-width: $verticalBreakpoint) {\n        max-height: 750px;\n        height: 100%;\n      }\n    }\n\n    .Dialog__content {\n      flex: 1 1 auto;\n    }\n  }\n\n  .ttd-dialog-desc {\n    font-size: 15px;\n    font-style: italic;\n    font-weight: 500;\n    margin-bottom: 1.5rem;\n  }\n\n  .ttd-dialog-tabs-root {\n    width: 100%;\n    height: 100%;\n    display: flex;\n    flex-direction: column;\n  }\n\n  .ttd-dialog-tab-trigger {\n    color: var(--color-on-surface);\n    font-size: 0.875rem;\n    margin: 0;\n    padding: 0 1rem;\n    background-color: transparent;\n    border: 0;\n    height: 2.875rem;\n    font-weight: 600;\n    font-family: inherit;\n    letter-spacing: 0.4px;\n\n    &[data-state=\"active\"] {\n      border-bottom: 2px solid var(--color-primary);\n    }\n  }\n\n  .ttd-dialog-triggers {\n    border-bottom: 1px solid var(--color-surface-high);\n    margin-bottom: 1.5rem;\n    padding-inline: 2.5rem;\n  }\n\n  .ttd-dialog-content {\n    padding-inline: 2.5rem;\n    height: 100%;\n    display: flex;\n    flex-direction: column;\n\n    &[hidden] {\n      display: none;\n    }\n  }\n\n  .ttd-dialog-input {\n    width: auto;\n    height: 10rem;\n    resize: none;\n    border-radius: var(--border-radius-lg);\n    border: 1px solid var(--dialog-border-color);\n    white-space: pre-wrap;\n    padding: 0.85rem;\n    box-sizing: border-box;\n    font-family: monospace;\n\n    @media screen and (min-width: $verticalBreakpoint) {\n      width: 100%;\n      height: 100%;\n    }\n  }\n\n  .ttd-dialog-output-wrapper {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    padding: 0.85rem;\n    box-sizing: border-box;\n    flex-grow: 1;\n    position: relative;\n\n    // background: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==\")\n    //   left center;\n    border-radius: var(--border-radius-lg);\n    border: 1px solid var(--dialog-border-color);\n\n    height: 400px;\n    width: auto;\n\n    @media screen and (min-width: $verticalBreakpoint) {\n      width: 100%;\n      // acts as min-height\n      height: 200px;\n    }\n\n    canvas {\n      max-width: 100%;\n      max-height: 100%;\n    }\n  }\n\n  .ttd-dialog-output-canvas-container {\n    display: flex;\n    width: 100%;\n    height: 100%;\n    align-items: center;\n    justify-content: center;\n    flex-grow: 1;\n    overflow: hidden;\n  }\n\n  .ttd-dialog-output-error {\n    color: red;\n    font-weight: 700;\n    font-size: 30px;\n    word-break: break-word;\n    overflow: auto;\n    max-height: 100%;\n    height: 100%;\n    width: 100%;\n    text-align: center;\n    position: absolute;\n    z-index: 10;\n\n    p {\n      font-weight: 500;\n      font-family: Cascadia;\n      text-align: left;\n      white-space: pre-wrap;\n      font-size: 0.875rem;\n      padding: 0 10px;\n    }\n  }\n\n  .ttd-dialog-panels {\n    height: 100%;\n\n    @media screen and (min-width: $verticalBreakpoint) {\n      display: grid;\n      grid-template-columns: 1fr 1fr;\n      gap: 4rem;\n    }\n  }\n\n  .ttd-dialog-panel {\n    display: flex;\n    flex-direction: column;\n    width: 100%;\n\n    &__header {\n      display: flex;\n      margin: 0px 4px 4px 4px;\n      align-items: center;\n      gap: 1rem;\n\n      label {\n        font-size: 14px;\n        font-style: normal;\n        font-weight: 600;\n      }\n    }\n\n    &:first-child {\n      .ttd-dialog-panel-button-container:not(.invisible) {\n        margin-bottom: 4rem;\n      }\n    }\n\n    @media screen and (min-width: $verticalBreakpoint) {\n      .ttd-dialog-panel-button-container:not(.invisible) {\n        margin-bottom: 0.5rem !important;\n      }\n    }\n\n    textarea {\n      height: 100%;\n      resize: none;\n      border-radius: var(--border-radius-lg);\n      border: 1px solid var(--dialog-border-color);\n      white-space: pre-wrap;\n      padding: 0.85rem;\n      box-sizing: border-box;\n      width: 100%;\n      font-family: monospace;\n\n      @media screen and (max-width: $verticalBreakpoint) {\n        width: auto;\n        height: 10rem;\n      }\n    }\n  }\n\n  .ttd-dialog-panel-button-container {\n    margin-top: 1rem;\n    margin-bottom: 0.5rem;\n\n    &.invisible {\n      .ttd-dialog-panel-button {\n        display: none;\n\n        @media screen and (min-width: $verticalBreakpoint) {\n          display: block;\n          visibility: hidden;\n        }\n      }\n    }\n  }\n\n  .ttd-dialog-panel-button {\n    &.drawnix-button {\n      font-family: inherit;\n      font-weight: 600;\n      height: 2.5rem;\n\n      font-size: 12px;\n      color: $oc-white;\n      background-color: var(--color-primary);\n      width: 100%;\n\n      &:hover {\n        background-color: var(--color-primary-darker);\n      }\n      &:active {\n        background-color: var(--color-primary-darkest);\n      }\n\n      &:disabled {\n        opacity: 0.5;\n        cursor: not-allowed;\n\n        &:hover {\n          background-color: var(--color-primary);\n        }\n      }\n\n      @media screen and (min-width: $verticalBreakpoint) {\n        width: auto;\n        min-width: 7.5rem;\n      }\n\n      @at-root .drawnix.theme--dark#{&} {\n        color: var(--color-gray-100);\n      }\n    }\n\n    position: relative;\n\n    div {\n      display: contents;\n\n      &.invisible {\n        visibility: hidden;\n      }\n\n      &.Spinner {\n        display: flex !important;\n        position: absolute;\n        inset: 0;\n\n        --spinner-color: white;\n\n        @at-root .drawnix.theme--dark#{&} {\n          --spinner-color: var(--color-gray-100);\n        }\n      }\n\n      span {\n        padding-left: 0.5rem;\n        display: flex;\n      }\n    }\n  }\n\n  .ttd-dialog-submit-shortcut {\n    margin-inline-start: 0.5rem;\n    font-size: 0.625rem;\n    opacity: 0.6;\n    display: flex;\n    gap: 0.125rem;\n\n    &__key {\n      border: 1px solid gray;\n      padding: 2px 3px;\n      border-radius: 4px;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/drawnix/src/components/ttd-dialog/ttd-dialog.tsx",
    "content": "import { Dialog, DialogContent } from '../dialog/dialog';\nimport MermaidToDrawnix from './mermaid-to-drawnix';\nimport { DialogType, useDrawnix } from '../../hooks/use-drawnix';\nimport MarkdownToDrawnix from './markdown-to-drawnix';\n\nexport const TTDDialog = ({ container }: { container: HTMLElement | null }) => {\n  const { appState, setAppState } = useDrawnix();\n  return (\n    <>\n      <Dialog\n        open={appState.openDialogType === DialogType.mermaidToDrawnix}\n        onOpenChange={(open) => {\n          setAppState({\n            ...appState,\n            openDialogType: open ? DialogType.mermaidToDrawnix : null,\n          });\n        }}\n      >\n        <DialogContent className=\"Dialog ttd-dialog\" container={container}>\n          <MermaidToDrawnix></MermaidToDrawnix>\n        </DialogContent>\n      </Dialog>\n      <Dialog\n        open={appState.openDialogType === DialogType.markdownToDrawnix}\n        onOpenChange={(open) => {\n          setAppState({\n            ...appState,\n            openDialogType: open ? DialogType.markdownToDrawnix : null,\n          });\n        }}\n      >\n        <DialogContent className=\"Dialog ttd-dialog\" container={container}>\n          <MarkdownToDrawnix></MarkdownToDrawnix>\n        </DialogContent>\n      </Dialog>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/drawnix/src/components/tutorial.scss",
    "content": ".drawnix-tutorial {\n  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Noto Sans', 'Noto Sans CJK SC', 'Microsoft Yahei', 'Hiragino Sans GB', Arial, sans-serif;\n  position: fixed;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  pointer-events: none;\n  background-color: transparent;\n  p {\n    margin: 0;\n    font-size: 14px;\n    color: #888;\n    line-height: 1.5;\n  }\n\n  .tutorial-overlay {\n    position: absolute;\n    width: 100%;\n    height: 100%;\n    pointer-events: none;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n  }\n\n  .tutorial-content {\n    position: relative;\n    width: 100%;\n    height: 100%;\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n  }\n\n  .brand-title {\n    font-size: 72px;\n    font-weight: 400;\n    color: #333;\n    letter-spacing: 2px;\n    margin: 0;\n    margin-bottom: 25px;\n  }\n\n  .brand-description {\n    font-size: 18px;\n    color: #333;\n    text-align: center;\n    max-width: 600px;\n    line-height: 1.6;\n    font-style: italic;\n    margin-bottom: 25px;\n  }\n\n  .brand-tooltip {\n    color: #888;\n    text-align: center;\n    max-width: 600px;\n    line-height: 1.6;\n    margin-bottom: 40px;\n  }\n\n  .feature-pointer {\n    position: absolute;\n  }\n\n  .top-left {\n    position: absolute;\n    top: 100px;\n    left: 60px;\n    .pointer-content {\n      position: absolute;\n      top: 100px;\n      width: 100%;\n      text-align: center;\n      left: 20px;\n    }\n  }\n\n  .top-center {\n    top: 100px;\n    left: 50%;\n    width: 200px;\n    transform: translateX(-50%);\n    .pointer-content {\n      position: absolute;\n      width: 100%;\n      top: 50px;\n      left: 60px;\n    }\n  }\n\n  .bottom-right {\n    bottom: 70px;\n    right: 40px;\n    .pointer-content {\n      position: absolute;\n      top: -30px;\n      right: 80px;\n      width: 100%;\n    }\n  }\n}\n\n@media screen and (max-width: 768px){\n  .drawnix-tutorial {\n    .tutorial-content {\n      width: 95%;\n      height: 95%;\n    }\n    \n    .feature-pointer {\n      display: none;\n    }\n  }\n}"
  },
  {
    "path": "packages/drawnix/src/components/tutorial.tsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport { useI18n } from \"../i18n\";\nimport \"./tutorial.scss\";\n\nexport const Tutorial: React.FC = () => {\n  const { t } = useI18n();\n\n  return (\n    <div className=\"drawnix-tutorial\">\n      <div className=\"tutorial-overlay\">\n        <div className=\"tutorial-content\">\n          \n          <h1 className=\"brand-title\">{t('tutorial.title')}</h1>\n          <p className=\"brand-description\">{t('tutorial.description')}</p>\n          <p className=\"brand-tooltip\">{t('tutorial.dataDescription')}</p>\n          \n          <div className=\"feature-pointer top-left\">\n            <svg className=\"pointer-arrow-svg\" width=\"130\" height=\"100\" viewBox=\"0 0 130 100\">\n              <defs>\n                <marker id=\"arrow-left\" markerWidth=\"10\" markerHeight=\"10\" refX=\"0\" refY=\"3\" orient=\"auto\" markerUnits=\"strokeWidth\">\n                  <path d=\"M0,0 L0,6 L6,3 z\" fill=\"#888\" />\n                </marker>\n              </defs>\n              <path d=\"M 80,70 Q 35,60 15,15\" fill=\"none\" stroke=\"#aaa\" strokeWidth=\"1.5\" markerEnd=\"url(#arrow-left)\" />\n            </svg>\n            <div className=\"pointer-content\">\n              <p>{t('tutorial.appToolbar')}</p>\n            </div>\n          </div>\n          \n          <div className=\"feature-pointer top-center\">\n            <svg className=\"pointer-arrow-svg\" width=\"100\" height=\"130\" viewBox=\"0 0 100 130\">\n              <defs>\n                <marker id=\"arrow-top\" markerWidth=\"10\" markerHeight=\"10\" refX=\"0\" refY=\"3\" orient=\"auto\" markerUnits=\"strokeWidth\">\n                  <path d=\"M0,0 L0,6 L6,3 z\" fill=\"#888\" />\n                </marker>\n              </defs>\n              <path d=\"M 45,90 Q 20,50 45,10\" fill=\"none\" stroke=\"#aaa\" strokeWidth=\"1.5\" markerEnd=\"url(#arrow-top)\" />\n            </svg>\n            <div className=\"pointer-content\">\n              <p>{t('tutorial.creationToolbar')}</p>\n            </div>\n          </div>\n          \n          <div className=\"feature-pointer bottom-right\">\n            <svg className=\"pointer-arrow-svg\" width=\"180\" height=\"100\" viewBox=\"0 0 180 100\">\n              <defs>\n                <marker id=\"arrow-right\" markerWidth=\"10\" markerHeight=\"10\" refX=\"0\" refY=\"3\" orient=\"auto\" markerUnits=\"strokeWidth\">\n                  <path d=\"M0,0 L0,6 L6,3 z\" fill=\"#888\" />\n                </marker>\n              </defs>\n              <path d=\"M 20,25 Q 75,20 105,70\" fill=\"none\" stroke=\"#aaa\" strokeWidth=\"1.5\" markerEnd=\"url(#arrow-right)\" />\n            </svg>\n            <div className=\"pointer-content\">\n              <p>{t('tutorial.themeDescription')}</p>\n            </div>\n          </div>\n          \n        </div>\n      </div>\n    </div>\n  );\n};"
  },
  {
    "path": "packages/drawnix/src/constants/color.ts",
    "content": "import { DEFAULT_COLOR } from '@plait/core';\n\nexport const TRANSPARENT = 'TRANSPARENT';\n\nexport const NO_COLOR = 'NO_COLOR';\n\nexport const WHITE = '#FFFFFF';\n\nexport const CLASSIC_COLORS = [\n  { name: 'color.none', value: NO_COLOR },\n  { name: 'color.default', value: DEFAULT_COLOR },\n  { name: 'color.white', value: WHITE },\n  { name: 'color.gray', value: '#808080' },\n  { name: 'color.deepBlue', value: '#1E90FF' },\n  { name: 'color.red', value: '#FF4500' },\n  { name: 'color.green', value: '#2ECC71' },\n  { name: 'color.yellow', value: '#FFD700' },\n  { name: 'color.purple', value: '#8A2BE2' },\n  { name: 'color.orange', value: '#FFA500' },\n  { name: 'color.pastelPink', value: '#FFB3BA' },\n  { name: 'color.cyan', value: '#00CED1' },\n  { name: 'color.brown', value: '#8B4513' },\n  { name: 'color.forestGreen', value: '#228B22' },\n  { name: 'color.lightGray', value: '#D3D3D3' },\n];\n"
  },
  {
    "path": "packages/drawnix/src/constants.ts",
    "content": "export enum EVENT {\n  COPY = 'copy',\n  PASTE = 'paste',\n  CUT = 'cut',\n  KEYDOWN = 'keydown',\n  KEYUP = 'keyup',\n  MOUSE_MOVE = 'mousemove',\n  RESIZE = 'resize',\n  UNLOAD = 'unload',\n  FOCUS = 'focus',\n  BLUR = 'blur',\n  DRAG_OVER = 'dragover',\n  DROP = 'drop',\n  GESTURE_END = 'gestureend',\n  BEFORE_UNLOAD = 'beforeunload',\n  GESTURE_START = 'gesturestart',\n  GESTURE_CHANGE = 'gesturechange',\n  POINTER_MOVE = 'pointermove',\n  POINTER_DOWN = 'pointerdown',\n  POINTER_UP = 'pointerup',\n  STATE_CHANGE = 'statechange',\n  WHEEL = 'wheel',\n  TOUCH_START = 'touchstart',\n  TOUCH_END = 'touchend',\n  HASHCHANGE = 'hashchange',\n  VISIBILITY_CHANGE = 'visibilitychange',\n  SCROLL = 'scroll',\n  MENU_ITEM_SELECT = 'menu.itemSelect',\n  MESSAGE = 'message',\n  FULLSCREENCHANGE = 'fullscreenchange',\n}\n\nexport const IMAGE_MIME_TYPES = {\n  svg: \"image/svg+xml\",\n  png: \"image/png\",\n  jpg: \"image/jpeg\",\n  gif: \"image/gif\",\n  webp: \"image/webp\",\n  bmp: \"image/bmp\",\n  ico: \"image/x-icon\",\n  avif: \"image/avif\",\n  jfif: \"image/jfif\",\n} as const;\n\nexport const MIME_TYPES = {\n  json: 'application/json',\n  drawnix: 'application/vnd.drawnix+json',\n  // image\n  ...IMAGE_MIME_TYPES,\n} as const;\n\nexport const VERSIONS = {\n  drawnix: 1,\n} as const;\n"
  },
  {
    "path": "packages/drawnix/src/css.d.ts",
    "content": "import \"csstype\";\n\ndeclare module \"csstype\" {\n  interface Properties {\n    \"--max-width\"?: number | string;\n    \"--swatch-color\"?: string;\n    \"--gap\"?: number | string;\n    \"--padding\"?: number | string;\n  }\n}\n"
  },
  {
    "path": "packages/drawnix/src/data/blob.ts",
    "content": "import { PlaitBoard } from '@plait/core';\nimport { isValidDrawnixData } from './json';\nimport { IMAGE_MIME_TYPES, MIME_TYPES } from '../constants';\nimport { ValueOf } from '../utils/utility-types';\nimport { DataURL } from '../types';\n\nexport const loadFromBlob = async (board: PlaitBoard, blob: Blob | File) => {\n  const contents = await parseFileContents(blob);\n  let data;\n  try {\n    data = JSON.parse(contents);\n    if (isValidDrawnixData(data)) {\n      return data;\n    }\n    throw new Error('Error: invalid file');\n  } catch (error: any) {\n    throw new Error('Error: invalid file');\n  }\n};\n\nexport const createFile = (\n  blob: File | Blob | ArrayBuffer,\n  mimeType: ValueOf<typeof MIME_TYPES>,\n  name: string | undefined\n) => {\n  return new File([blob], name || '', {\n    type: mimeType,\n  });\n};\n\nexport const blobToArrayBuffer = (blob: Blob): Promise<ArrayBuffer> => {\n  if ('arrayBuffer' in blob) {\n    return blob.arrayBuffer();\n  }\n  // Safari\n  return new Promise((resolve, reject) => {\n    const reader = new FileReader();\n    reader.onload = (event) => {\n      if (!event.target?.result) {\n        return reject(new Error(\"Couldn't convert blob to ArrayBuffer\"));\n      }\n      resolve(event.target.result as ArrayBuffer);\n    };\n    reader.readAsArrayBuffer(blob);\n  });\n};\n\nexport const normalizeFile = async (file: File) => {\n  if (!file.type) {\n    if (file?.name?.endsWith('.drawnix')) {\n      file = createFile(\n        await blobToArrayBuffer(file),\n        MIME_TYPES.drawnix,\n        file.name\n      );\n    }\n  }\n  return file;\n};\n\nexport const parseFileContents = async (blob: Blob | File) => {\n  let contents: string;\n  if ('text' in Blob) {\n    contents = await blob.text();\n  } else {\n    contents = await new Promise((resolve) => {\n      const reader = new FileReader();\n      reader.readAsText(blob, 'utf8');\n      reader.onloadend = () => {\n        if (reader.readyState === FileReader.DONE) {\n          resolve(reader.result as string);\n        }\n      };\n    });\n  }\n  return contents;\n};\n\nexport const getDataURL = async (file: Blob | File): Promise<DataURL> => {\n  return new Promise((resolve, reject) => {\n    const reader = new FileReader();\n    reader.onload = () => {\n      const dataURL = reader.result as DataURL;\n      resolve(dataURL);\n    };\n    reader.onerror = (error) => reject(error);\n    reader.readAsDataURL(file);\n  });\n};\n\nexport const isSupportedImageFileType = (type: string | null | undefined) => {\n  return !!type && (Object.values(IMAGE_MIME_TYPES) as string[]).includes(type);\n};\n\nexport const isSupportedImageFile = (\n  blob: Blob | null | undefined\n): blob is Blob & { type: ValueOf<typeof IMAGE_MIME_TYPES> } => {\n  const { type } = blob || {};\n  return isSupportedImageFileType(type);\n};\n"
  },
  {
    "path": "packages/drawnix/src/data/filesystem.ts",
    "content": "import type { FileSystemHandle } from 'browser-fs-access';\nimport {\n  fileOpen as _fileOpen,\n  fileSave as _fileSave,\n  supported as nativeFileSystemSupported,\n} from 'browser-fs-access';\nimport { MIME_TYPES } from '../constants';\n\ntype FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, 'binary'>;\n\nexport const fileOpen = <M extends boolean | undefined = false>(opts: {\n  extensions?: FILE_EXTENSION[];\n  description: string;\n  multiple?: M;\n}): Promise<M extends false | undefined ? File : File[]> => {\n  // an unsafe TS hack, alas not much we can do AFAIK\n  type RetType = M extends false | undefined ? File : File[];\n\n  const mimeTypes = opts.extensions?.reduce((mimeTypes, type) => {\n    mimeTypes.push(MIME_TYPES[type]);\n\n    return mimeTypes;\n  }, [] as string[]);\n\n  const extensions = opts.extensions?.reduce((acc, ext) => {\n    if (ext === 'jpg') {\n      return acc.concat('.jpg', '.jpeg');\n    }\n    return acc.concat(`.${ext}`);\n  }, [] as string[]);\n\n  return _fileOpen({\n    description: opts.description,\n    extensions,\n    mimeTypes,\n    multiple: opts.multiple ?? false,\n  }) as Promise<RetType>;\n};\n\nexport const fileSave = (\n  blob: Blob | Promise<Blob>,\n  opts: {\n    /** supply without the extension */\n    name: string;\n    /** file extension */\n    extension: FILE_EXTENSION;\n    description: string;\n    /** existing FileSystemHandle */\n    fileHandle?: FileSystemHandle | null;\n  }\n) => {\n  return _fileSave(\n    blob,\n    {\n      fileName: `${opts.name}.${opts.extension}`,\n      description: opts.description,\n      extensions: [`.${opts.extension}`],\n    },\n    opts.fileHandle as any\n  );\n};\n\nexport type { FileSystemHandle };\nexport { nativeFileSystemSupported };\n"
  },
  {
    "path": "packages/drawnix/src/data/image.ts",
    "content": "import {\n  getHitElementByPoint,\n  getSelectedElements,\n  PlaitBoard,\n  Point,\n} from '@plait/core';\nimport { DataURL } from '../types';\nimport { getDataURL } from './blob';\nimport { MindElement, MindTransforms } from '@plait/mind';\nimport { DrawTransforms } from '@plait/draw';\nimport { getElementOfFocusedImage } from '@plait/common';\n\nexport const loadHTMLImageElement = (dataURL: DataURL) => {\n  return new Promise<HTMLImageElement>((resolve, reject) => {\n    const image = new Image();\n    image.onload = () => {\n      resolve(image);\n    };\n    image.onerror = (error) => {\n      reject(error);\n    };\n    image.src = dataURL;\n  });\n};\n\nexport const buildImage = (\n  image: HTMLImageElement,\n  dataURL: DataURL,\n  maxWidth: number\n) => {\n  const width = image.width > maxWidth ? maxWidth : image.width;\n  const height = (width / image.width) * image.height;\n  return {\n    url: dataURL,\n    width,\n    height,\n  };\n};\n\nexport const insertImage = async (\n  board: PlaitBoard,\n  imageFile: File,\n  startPoint?: Point,\n  isDrop?: boolean\n) => {\n  const selectedElement =\n    getSelectedElements(board)[0] || getElementOfFocusedImage(board);\n  const defaultImageWidth = selectedElement ? 240 : 400;\n  const dataURL = await getDataURL(imageFile);\n  const image = await loadHTMLImageElement(dataURL);\n  const imageItem = buildImage(image, dataURL, defaultImageWidth);\n  const element = startPoint && getHitElementByPoint(board, startPoint);\n  if (isDrop && element && MindElement.isMindElement(board, element)) {\n    MindTransforms.setImage(board, element as MindElement, imageItem);\n    return;\n  }\n  if (\n    selectedElement &&\n    MindElement.isMindElement(board, selectedElement) &&\n    !isDrop\n  ) {\n    MindTransforms.setImage(board, selectedElement as MindElement, imageItem);\n  } else {\n    DrawTransforms.insertImage(board, imageItem, startPoint);\n  }\n};\n"
  },
  {
    "path": "packages/drawnix/src/data/json.ts",
    "content": "import { PlaitBoard, PlaitElement } from '@plait/core';\nimport { MIME_TYPES, VERSIONS } from '../constants';\nimport { fileOpen, fileSave } from './filesystem';\nimport { DrawnixExportedData, DrawnixExportedType } from './types';\nimport { loadFromBlob, normalizeFile } from './blob';\n\nexport const getDefaultName = () => {\n  const time = new Date().getTime();\n  return time.toString();\n};\n\nexport const saveAsJSON = async (\n  board: PlaitBoard,\n  name: string = getDefaultName()\n) => {\n  const serialized = serializeAsJSON(board);\n  const blob = new Blob([serialized], {\n    type: MIME_TYPES.drawnix,\n  });\n\n  const fileHandle = await fileSave(blob, {\n    name,\n    extension: 'drawnix',\n    description: 'Drawnix file',\n  });\n  return { fileHandle };\n};\n\nexport const loadFromJSON = async (board: PlaitBoard) => {\n  const file = await fileOpen({\n    description: 'Drawnix files',\n    // ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442\n    // gets resolved. Else, iOS users cannot open `.drawnix` files.\n    // extensions: [\"json\", \"drawnix\", \"png\", \"svg\"],\n  });\n  return loadFromBlob(board, await normalizeFile(file));\n};\n\nexport const isValidDrawnixData = (data?: any): data is DrawnixExportedData => {\n  return (\n    data &&\n    data.type === DrawnixExportedType.drawnix &&\n    Array.isArray(data.elements) &&\n    typeof data.viewport === 'object'\n  );\n};\n\nexport const serializeAsJSON = (board: PlaitBoard): string => {\n  const data = {\n    type: DrawnixExportedType.drawnix,\n    version: VERSIONS.drawnix,\n    source: 'web',\n    elements: board.children,\n    viewport: board.viewport,\n    theme: board.theme,\n  };\n\n  return JSON.stringify(data, null, 2);\n};\n"
  },
  {
    "path": "packages/drawnix/src/data/types.ts",
    "content": "import { PlaitElement, PlaitTheme, Viewport } from '@plait/core';\n\nexport interface DrawnixExportedData {\n  type: DrawnixExportedType.drawnix;\n  version: number;\n  source: 'web';\n  elements: PlaitElement[];\n  viewport: Viewport;\n  theme?: PlaitTheme;\n}\n\nexport enum DrawnixExportedType {\n    drawnix = 'drawnix'\n}"
  },
  {
    "path": "packages/drawnix/src/drawnix.spec.tsx",
    "content": "describe('Drawnix', () => {\n  it('should render successfully', () => {\n    // const { baseElement } = render(<Drawnix value={[]} />);\n    // expect(baseElement).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "packages/drawnix/src/drawnix.tsx",
    "content": "import { Board, BoardChangeData, Wrapper } from '@plait-board/react-board';\nimport {\n  PlaitBoard,\n  PlaitBoardOptions,\n  PlaitElement,\n  PlaitPlugin,\n  PlaitPointerType,\n  PlaitTheme,\n  Selection,\n  ThemeColorMode,\n  Viewport,\n} from '@plait/core';\nimport React, { useState, useRef, useEffect } from 'react';\nimport { withGroup } from '@plait/common';\nimport { withDraw } from '@plait/draw';\nimport { MindThemeColors, withMind } from '@plait/mind';\nimport MobileDetect from 'mobile-detect';\nimport { withMindExtend } from './plugins/with-mind-extend';\nimport { withCommonPlugin } from './plugins/with-common';\nimport { CreationToolbar } from './components/toolbar/creation-toolbar';\nimport { ZoomToolbar } from './components/toolbar/zoom-toolbar';\nimport { PopupToolbar } from './components/toolbar/popup-toolbar/popup-toolbar';\nimport { AppToolbar } from './components/toolbar/app-toolbar/app-toolbar';\nimport classNames from 'classnames';\nimport './styles/index.scss';\nimport { buildDrawnixHotkeyPlugin } from './plugins/with-hotkey';\nimport { withFreehand } from './plugins/freehand/with-freehand';\nimport { ThemeToolbar } from './components/toolbar/theme-toolbar';\nimport { buildPencilPlugin } from './plugins/with-pencil';\nimport {\n  DrawnixBoard,\n  DrawnixContext,\n  DrawnixState,\n} from './hooks/use-drawnix';\nimport { ClosePencilToolbar } from './components/toolbar/pencil-mode-toolbar';\nimport { TTDDialog } from './components/ttd-dialog/ttd-dialog';\nimport { CleanConfirm } from './components/clean-confirm/clean-confirm';\nimport { buildTextLinkPlugin } from './plugins/with-text-link';\nimport { LinkPopup } from './components/popup/link-popup/link-popup';\nimport { I18nProvider } from './i18n';\nimport { Tutorial } from './components/tutorial';\nimport { LASER_POINTER_CLASS_NAME } from './utils/laser-pointer';\n\nexport type DrawnixProps = {\n  value: PlaitElement[];\n  viewport?: Viewport;\n  theme?: PlaitTheme;\n  onChange?: (value: BoardChangeData) => void;\n  onSelectionChange?: (selection: Selection | null) => void;\n  onValueChange?: (value: PlaitElement[]) => void;\n  onViewportChange?: (value: Viewport) => void;\n  onThemeChange?: (value: ThemeColorMode) => void;\n  afterInit?: (board: PlaitBoard) => void;\n  tutorial?: boolean;\n} & React.HTMLAttributes<HTMLDivElement>;\n\nexport const Drawnix: React.FC<DrawnixProps> = ({\n  value,\n  viewport,\n  theme,\n  onChange,\n  onSelectionChange,\n  onViewportChange,\n  onThemeChange,\n  onValueChange,\n  afterInit,\n  tutorial = false,\n}) => {\n  const options: PlaitBoardOptions = {\n    readonly: false,\n    hideScrollbar: false,\n    disabledScrollOnNonFocus: false,\n    themeColors: MindThemeColors,\n  };\n\n  const [appState, setAppState] = useState<DrawnixState>(() => {\n    // TODO: need to consider how to maintenance the pointer state in future\n    const md = new MobileDetect(window.navigator.userAgent);\n    return {\n      pointer: PlaitPointerType.hand,\n      isMobile: md.mobile() !== null,\n      isPencilMode: false,\n      openDialogType: null,\n      openCleanConfirm: false,\n    };\n  });\n\n  const [board, setBoard] = useState<DrawnixBoard | null>(null);\n\n  if (board) {\n    board.appState = appState;\n  }\n\n  const updateAppState = (newAppState: Partial<DrawnixState>) => {\n    setAppState({\n      ...appState,\n      ...newAppState,\n    });\n  };\n\n  const plugins: PlaitPlugin[] = [\n    withDraw,\n    withGroup,\n    withMind,\n    withMindExtend,\n    withCommonPlugin,\n    buildDrawnixHotkeyPlugin(updateAppState),\n    withFreehand,\n    buildPencilPlugin(updateAppState),\n    buildTextLinkPlugin(updateAppState),\n  ];\n\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  return (\n    <I18nProvider>\n      <DrawnixContext.Provider value={{ appState, setAppState }}>\n        <div\n          className={classNames('drawnix', {\n            'drawnix--mobile': appState.isMobile,\n          })}\n          ref={containerRef}\n        >\n          <Wrapper\n            value={value}\n            viewport={viewport}\n            theme={theme}\n            options={options}\n            plugins={plugins}\n            onChange={(data: BoardChangeData) => {\n              onChange && onChange(data);\n            }}\n            onSelectionChange={onSelectionChange}\n            onViewportChange={onViewportChange}\n            onThemeChange={onThemeChange}\n            onValueChange={onValueChange}\n          >\n            <Board\n              afterInit={(board) => {\n                setBoard(board as DrawnixBoard);\n                afterInit && afterInit(board);\n              }}\n            >\n              {tutorial &&\n                board &&\n                PlaitBoard.isPointer(board, PlaitPointerType.selection) && (\n                  <Tutorial />\n                )}\n            </Board>\n            <AppToolbar></AppToolbar>\n            <CreationToolbar></CreationToolbar>\n            <ZoomToolbar></ZoomToolbar>\n            <ThemeToolbar></ThemeToolbar>\n            <PopupToolbar></PopupToolbar>\n            <LinkPopup></LinkPopup>\n            <ClosePencilToolbar></ClosePencilToolbar>\n            <TTDDialog container={containerRef.current}></TTDDialog>\n            <CleanConfirm container={containerRef.current}></CleanConfirm>\n          </Wrapper>\n          <canvas className={`${LASER_POINTER_CLASS_NAME} mouse-course-hidden`}></canvas>\n        </div>\n      </DrawnixContext.Provider>\n    </I18nProvider>\n  );\n};\n"
  },
  {
    "path": "packages/drawnix/src/errors.ts",
    "content": "export class AbortError extends DOMException {\n  constructor(message = 'Request Aborted') {\n    super(message, 'AbortError');\n  }\n}\n"
  },
  {
    "path": "packages/drawnix/src/hooks/use-drawnix.tsx",
    "content": "/**\n * A React context for sharing the board object, in a way that re-renders the\n * context whenever changes occur.\n */\nimport { PlaitBoard, PlaitPointerType } from '@plait/core';\nimport { createContext, useContext } from 'react';\nimport { MindPointerType } from '@plait/mind';\nimport { DrawPointerType } from '@plait/draw';\nimport { FreehandShape } from '../plugins/freehand/type';\nimport { Editor } from 'slate';\nimport { LinkElement } from '@plait/common';\n\nexport enum DialogType {\n  mermaidToDrawnix = 'mermaidToDrawnix',\n  markdownToDrawnix = 'markdownToDrawnix',\n}\n\nexport type DrawnixPointerType =\n  | PlaitPointerType\n  | MindPointerType\n  | DrawPointerType\n  | FreehandShape;\n\nexport interface DrawnixBoard extends PlaitBoard {\n  appState: DrawnixState;\n}\n\nexport type LinkState = {\n  targetDom: HTMLElement;\n  editor: Editor;\n  targetElement: LinkElement;\n  isEditing: boolean;\n  isHovering: boolean;\n  isHoveringOrigin: boolean;\n};\n\nexport type DrawnixState = {\n  pointer: DrawnixPointerType;\n  isMobile: boolean;\n  isPencilMode: boolean;\n  openDialogType: DialogType | null;\n  openCleanConfirm: boolean;\n  linkState?: LinkState | null;\n};\n\nexport const DrawnixContext = createContext<{\n  appState: DrawnixState;\n  setAppState: (appState: DrawnixState) => void;\n} | null>(null);\n\nexport const useDrawnix = (): {\n  appState: DrawnixState;\n  setAppState: (appState: DrawnixState) => void;\n} => {\n  const context = useContext(DrawnixContext);\n\n  if (!context) {\n    throw new Error(\n      `The \\`useDrawnix\\` hook must be used inside the <Drawnix> component's context.`\n    );\n  }\n\n  return context;\n};\n\nexport const useSetPointer = () => {\n  const { appState, setAppState } = useDrawnix();\n  return (pointer: DrawnixPointerType) => {\n    setAppState({ ...appState, pointer });\n  };\n};\n"
  },
  {
    "path": "packages/drawnix/src/i18n/index.tsx",
    "content": "import React, { createContext, useContext, useState, useMemo } from 'react';\nimport { zhTranslations, enTranslations, ruTranslations, arTranslations, viTranslations } from './translations';\nimport { Language, Translations, I18nContextType, I18nProviderProps } from './types';\n\n// Translation data\nconst translations: Record<Language, Translations> = {\n  zh: zhTranslations,\n  en: enTranslations,\n  ru: ruTranslations,\n  ar: arTranslations,\n  vi: viTranslations\n};\n\n// Create the context\nconst I18nContext = createContext<I18nContextType | undefined>(undefined);\n\nexport const I18nProvider: React.FC<I18nProviderProps> = ({\n    children,\n    defaultLanguage = 'zh',\n}) => {\n\n    const [language, setLanguageState] = useState<Language>(() => {\n        const storedLanguage = localStorage.getItem('language') as Language;\n        return storedLanguage || defaultLanguage;\n    });\n\n    const setLanguage = (newLanguage: Language) => {\n        localStorage.setItem('language', newLanguage);\n        setLanguageState(newLanguage);\n    };\n\n    const t = (key: keyof Translations): string => {\n        return translations[language][key] || key;\n    };\n\n    const value: I18nContextType = useMemo(\n        () => ({\n            language,\n            setLanguage,\n            t,\n        }),\n        [language]\n    );\n\n    return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;\n};\n\nexport const useI18n = (): I18nContextType => {\n  const context = useContext(I18nContext);\n\n  if (!context) {\n    throw new Error('useI18n must be used within I18nProvider');\n  }\n\n  return context;\n};\n\nexport const i18nInsidePlaitHook = () => {\n\n    const i18n = {\n        t: (key: keyof Translations): string => {  \n            const currentLang = localStorage.getItem('language') as Language || 'zh';\n            return translations[currentLang][key] || key;\n        },\n        language: localStorage.getItem('language') as Language || 'zh',\n    };\n\n    return i18n;\n}\n\nexport type { Language, Translations, I18nContextType };\n"
  },
  {
    "path": "packages/drawnix/src/i18n/translations/ar.ts",
    "content": "import { Translations } from '../types';\n\nconst arTranslations: Translations = {\n    // Toolbar items\n    \"toolbar.hand\": \"اليد — H\",\n    \"toolbar.selection\": \"التحديد — V\",\n    \"toolbar.mind\": \"خريطة ذهنية — M\",\n    'toolbar.eraser': 'ممحاة — E',\n    \"toolbar.text\": \"نص — T\",\n    \"toolbar.pen\": \"قلم — P\",\n    \"toolbar.arrow\": \"سهم — A\",\n    \"toolbar.shape\": \"أشكال\",\n    \"toolbar.image\": \"صورة — Cmd+U\",\n    \"toolbar.extraTools\": \"أدوات إضافية\",\n\n    \"toolbar.arrow.straight\": \"سهم مستقيم\",\n    \"toolbar.arrow.elbow\": \"سهم بزوايا\",\n    \"toolbar.arrow.curve\": \"سهم منحني\",\n\n    \"toolbar.shape.rectangle\": \"مستطيل — R\",\n    \"toolbar.shape.ellipse\": \"بيضاوي — O\",\n    \"toolbar.shape.triangle\": \"مثلث\",\n    \"toolbar.shape.terminal\": \"نهائي\",\n    \"toolbar.shape.noteCurlyLeft\": \"ملاحظة معقوفة — يسار\",\n    \"toolbar.shape.noteCurlyRight\": \"ملاحظة معقوفة — يمين\",\n    \"toolbar.shape.diamond\": \"معين\",\n    \"toolbar.shape.parallelogram\": \"متوازي أضلاع\",\n    \"toolbar.shape.roundRectangle\": \"مستطيل دائري الحواف\",\n\n\n    // Zoom controls\n    \"zoom.in\": \"تكبير — Cmd++\",\n    \"zoom.out\": \"تصغير — Cmd+-\",\n    \"zoom.fit\": \"ملاءمة الشاشة\",\n    \"zoom.100\": \"تكبير إلى 100%\",\n\n    // Themes\n    \"theme.default\": \"افتراضي\",\n    \"theme.colorful\": \"ملون\",\n    \"theme.soft\": \"ناعم\",\n    \"theme.retro\": \"كلاسيكي\",\n    \"theme.dark\": \"داكن\",\n    \"theme.starry\": \"ليلي\",\n\n    // Colors\n    \"color.none\": \"لون الموضوع\",\n    \"color.unknown\": \"لون آخر\",\n    \"color.default\": \"أسود أساسي\",\n    \"color.white\": \"أبيض\",\n    \"color.gray\": \"رمادي\",\n    \"color.deepBlue\": \"أزرق غامق\",\n    \"color.red\": \"أحمر\",\n    \"color.green\": \"أخضر\",\n    \"color.yellow\": \"أصفر\",\n    \"color.purple\": \"بنفسجي\",\n    \"color.orange\": \"برتقالي\",\n    \"color.pastelPink\": \"وردي فاتح\",\n    \"color.cyan\": \"سماوي\",\n    \"color.brown\": \"بني\",\n    \"color.forestGreen\": \"أخضر غامق (غابة)\",\n    \"color.lightGray\": \"رمادي فاتح\",\n\n    // General\n    \"general.undo\": \"تراجع\",\n    \"general.redo\": \"إعادة\",\n    \"general.menu\": \"قائمة التطبيق\",\n    \"general.duplicate\": \"تكرار\",\n    \"general.delete\": \"حذف\",\n\n    // Language\n    \"language.switcher\": \"اللغة\",\n    \"language.chinese\": \"中文\",\n    \"language.english\": \"English\",\n    \"language.russian\": \"Русский\",\n    \"language.arabic\": \"عربي\",\n    'language.vietnamese': 'Tiếng Việt',\n\n    // Menu items\n    \"menu.open\": \"فتح\",\n    \"menu.saveFile\": \"حفظ الملف\",\n    \"menu.exportImage\": \"تصدير صورة\",\n    \"menu.exportImage.svg\": \"SVG\",\n    \"menu.exportImage.png\": \"PNG\",\n    \"menu.exportImage.jpg\": \"JPG\",\n    \"menu.cleanBoard\": \"مسح اللوحة\",\n    \"menu.github\": \"غيت هب\",\n\n    // Dialog translations\n    \"dialog.mermaid.title\": \"من Mermaid إلى Drawnix\",\n    \"dialog.mermaid.description\": \"يدعم حاليًا\",\n    \"dialog.mermaid.flowchart\": \"المخططات الانسيابية\",\n    \"dialog.mermaid.sequence\": \"مخططات التسلسل\",\n    \"dialog.mermaid.class\": \"مخططات الفئات\",\n    \"dialog.mermaid.otherTypes\": \"، وأنواع أخرى من المخططات (تُعرض كصور).\",\n    \"dialog.mermaid.syntax\": \"صيغة Mermaid\",\n    \"dialog.mermaid.placeholder\": \"اكتب تعريف المخطط هنا...\",\n    \"dialog.mermaid.preview\": \"معاينة\",\n    \"dialog.mermaid.insert\": \"إدراج\",\n    \"dialog.markdown.description\": \"يدعم التحويل التلقائي من Markdown إلى خريطة ذهنية.\",\n    \"dialog.markdown.syntax\": \"صيغة Markdown\",\n    \"dialog.markdown.placeholder\": \"اكتب نص Markdown هنا...\",\n    \"dialog.markdown.preview\": \"معاينة\",\n    \"dialog.markdown.insert\": \"إدراج\",\n    \"dialog.error.loadMermaid\": \"فشل في تحميل مكتبة Mermaid\",\n\n    // Extra tools menu items\n    \"extraTools.mermaidToDrawnix\": \"من Mermaid إلى Drawnix\",\n    \"extraTools.markdownToDrawnix\": \"من Markdown إلى Drawnix\",\n\n    // Clean confirm dialog\n    \"cleanConfirm.title\": \"مسح اللوحة\",\n    \"cleanConfirm.description\": \"سيؤدي هذا إلى مسح اللوحة بالكامل. هل تريد المتابعة؟\",\n    \"cleanConfirm.cancel\": \"إلغاء\",\n    \"cleanConfirm.ok\": \"موافق\",\n\n    // Link popup items\n    \"popupLink.delLink\": \"حذف الرابط\",\n\n    // Tool popup items\n    \"popupToolbar.fillColor\": \"لون التعبئة\",\n    \"popupToolbar.fontSize\": \"حجم الخط\",\n    \"popupToolbar.fontColor\": \"لون الخط\",\n    \"popupToolbar.link\": \"إدراج رابط\",\n    \"popupToolbar.stroke\": \"الحد\",\n    'popupToolbar.opacity': 'مستوى شفافية',\n\n    // Text placeholders\n    \"textPlaceholders.link\": \"رابط\",\n    \"textPlaceholders.text\": \"نص\",\n\n    // Line tool\n    \"line.source\": \"بداية\",\n    \"line.target\": \"نهاية\",\n    \"line.arrow\": \"سهم\",\n    \"line.none\": \"لا شيء\",\n\n    // Stroke style\n    \"stroke.solid\": \"صلب\",\n    \"stroke.dashed\": \"متقطع\",\n    \"stroke.dotted\": \"منقط\",\n\n    //markdown example\n    //   \"markdown.example\": \"# لقد بدأت\\n\\n- دعني أرى من تسبب بهذا الخطأ 🕵️ ♂️ 🔍\\n  - 😯 💣\\n    - اتضح أنه أنا 👈 🎯 💘\\n\\n- بشكل غير متوقع، لا يعمل؛ لماذا 🚫 ⚙️ ❓\\n  - بشكل غير متوقع، أصبح يعمل الآن؛ لماذا؟ 🎢 ✨\\n    - 🤯 ⚡ ➡️ 🎉\\n\\n- ما الذي يمكن تشغيله 🐞 🚀\\n  - إذًا لا تلمسه 🛑 ✋\\n    - 👾 💥 🏹 🎯\\n\\n## ولد أم بنت 👶 ❓ 🤷 ♂️ ♀️\\n\\n### مرحبًا بالعالم 👋 🌍 ✨ 💻\\n\\n#### واو، مبرمج 🤯 ⌨️ 💡 👩 💻\",\n    'markdown.example': `# I have started\n\n  - دعني أرى من تسبب بهذا الخطأ  🕵️ ♂️ 🔍\n    - 😯 💣\n      - اتضح أنه أنا 👈 🎯 💘\n\n  - بشكل غير متوقع، لا يعمل؛ لماذا  🚫 ⚙️ ❓\n    - بشكل غير متوقع، أصبح يعمل الآن؛ لماذا؟ 🎢 ✨\n      - 🤯 ⚡ ➡️ 🎉\n\n  - ما الذي يمكن تشغيله 🐞 🚀\n    - إذًا لا تلمسه 🛑 ✋\n      - 👾 💥 🏹 🎯\n    \n  ## ولد أم بنت  👶 ❓ 🤷 ♂️ ♀️\n\n  ### Hello world 👋 🌍 ✨ 💻\n\n  #### Wow, a programmer 🤯 ⌨️ 💡 👩 💻`,\n\n    // Draw elements text\n    \"draw.lineText\": \"نص\",\n    \"draw.geometryText\": \"نص\",\n\n    // Mind map elements text\n    \"mind.centralText\": \"الموضوع المركزي\",\n    \"mind.abstractNodeText\": \"ملخص\",\n    \n    'tutorial.title': 'Drawnix',\n    'tutorial.description': 'سبورة شاملة تتضمن الخرائط الذهنية والمخططات الانسيابية والرسم الحر وغير ذلك',\n    'tutorial.dataDescription': 'تُحفظ جميع البيانات محليًا في متصفحك',\n    'tutorial.appToolbar': 'تصدير، إعدادات اللغة، ...',\n    'tutorial.creationToolbar': 'اختر أداة لبدء الإنشاء',\n    'tutorial.themeDescription': 'التبديل بين السمة الفاتحة والداكنة',\n};\n\nexport default arTranslations;\n"
  },
  {
    "path": "packages/drawnix/src/i18n/translations/en.ts",
    "content": "import { Translations } from '../types';\n\nconst enTranslations: Translations = {\n  // Toolbar items\n  'toolbar.hand': 'Hand — H',\n  'toolbar.selection': 'Selection — V',\n  'toolbar.mind': 'Mind — M',\n  'toolbar.text': 'Text — T',\n  'toolbar.arrow': 'Arrow — A',\n  'toolbar.shape': 'Shape',\n  'toolbar.image': 'Image — Cmd+U',\n  'toolbar.extraTools': 'Extra Tools',\n\n  'toolbar.pen': 'Pen — P',\n  'toolbar.eraser': 'Eraser — E',\n\n  'toolbar.arrow.straight': 'Straight Arrow Line',\n  'toolbar.arrow.elbow': 'Elbow Arrow Line',\n  'toolbar.arrow.curve': 'Curve Arrow Line',\n\n  'toolbar.shape.rectangle': 'Rectangle — R',\n  'toolbar.shape.ellipse': 'Ellipse — O',\n  'toolbar.shape.triangle': 'Triangle',\n  'toolbar.shape.terminal': 'Terminal',\n  'toolbar.shape.noteCurlyLeft': 'Curly Note — Left',\n  'toolbar.shape.noteCurlyRight': 'Curly Note — Right',\n  'toolbar.shape.diamond': 'Diamond',\n  'toolbar.shape.parallelogram': 'Parallelogram',\n  'toolbar.shape.roundRectangle': 'Round Rectangle',\n\n  // Zoom controls\n  'zoom.in': 'Zoom In — Cmd++',\n  'zoom.out': 'Zoom Out — Cmd+-',\n  'zoom.fit': 'Fit to Screen',\n  'zoom.100': 'Zoom to 100%',\n\n  // Themes\n  'theme.default': 'Default',\n  'theme.colorful': 'Colorful',\n  'theme.soft': 'Soft',\n  'theme.retro': 'Retro',\n  'theme.dark': 'Dark',\n  'theme.starry': 'Starry',\n\n  // Colors\n  'color.none': 'Topic Color',\n  'color.unknown': 'Other Color',\n  'color.default': 'Basic Black',\n  'color.white': 'White',\n  'color.gray': 'Grey',\n  'color.deepBlue': 'Deep Blue',\n  'color.red': 'Red',\n  'color.green': 'Green',\n  'color.yellow': 'Yellow',\n  'color.purple': 'Purple',\n  'color.orange': 'Orange',\n  'color.pastelPink': 'Paster Pink',\n  'color.cyan': 'Cyan',\n  'color.brown': 'Brown',\n  'color.forestGreen': 'Forest Green',\n  'color.lightGray': 'Light Grey',\n\n  // General\n  'general.undo': 'Undo',\n  'general.redo': 'Redo',\n  'general.menu': 'App Menu',\n  'general.duplicate': 'Duplicate',\n  'general.delete': 'Delete',\n\n  // Language\n  'language.switcher': 'Language',\n  'language.chinese': '中文',\n  'language.english': 'English',\n  'language.russian': 'Русский',\n  'language.arabic': 'عربي',\n  'language.vietnamese': 'Tiếng Việt',\n  // Menu items\n  'menu.open': 'Open',\n  'menu.saveFile': 'Save File',\n  'menu.exportImage': 'Export Image',\n  'menu.exportImage.svg': 'SVG',\n  'menu.exportImage.png': 'PNG',\n  'menu.exportImage.jpg': 'JPG',\n  'menu.cleanBoard': 'Clear Board',\n  'menu.github': 'GitHub',\n\n  // Dialog translations\n  'dialog.mermaid.title': 'Mermaid to Drawnix',\n  'dialog.mermaid.description': 'Currently supports',\n  'dialog.mermaid.flowchart': 'flowcharts',\n  'dialog.mermaid.sequence': 'sequence diagrams',\n  'dialog.mermaid.class': 'class diagrams',\n  'dialog.mermaid.otherTypes':\n    ', and other diagram types (rendered as images).',\n  'dialog.mermaid.syntax': 'Mermaid Syntax',\n  'dialog.mermaid.placeholder': 'Write your Mermaid chart definition here…',\n  'dialog.mermaid.preview': 'Preview',\n  'dialog.mermaid.insert': 'Insert',\n  'dialog.markdown.description':\n    'Supports automatic conversion of Markdown syntax to mind map.',\n  'dialog.markdown.syntax': 'Markdown Syntax',\n  'dialog.markdown.placeholder': 'Write your Markdown text definition here...',\n  'dialog.markdown.preview': 'Preview',\n  'dialog.markdown.insert': 'Insert',\n  'dialog.error.loadMermaid': 'Failed to load Mermaid library',\n\n  // Extra tools menu items\n  'extraTools.mermaidToDrawnix': 'Mermaid to Drawnix',\n  'extraTools.markdownToDrawnix': 'Markdown to Drawnix',\n\n  // Clean confirm dialog\n  'cleanConfirm.title': 'Clear Board',\n  'cleanConfirm.description':\n    'This will clear the entire board. Do you want to continue?',\n  'cleanConfirm.cancel': 'Cancel',\n  'cleanConfirm.ok': 'OK',\n\n  // Link popup items\n  'popupLink.delLink': 'Delete Link',\n\n  // Tool popup items\n  'popupToolbar.fillColor': 'Fill Color',\n  'popupToolbar.fontSize': 'Font Size',\n  'popupToolbar.fontColor': 'Font Color',\n  'popupToolbar.link': 'Insert Link',\n  'popupToolbar.stroke': 'Stroke',\n  'popupToolbar.opacity': 'Opacity',\n\n  // Text placeholders\n  'textPlaceholders.link': 'Link',\n  'textPlaceholders.text': 'Text',\n\n  // Line tool\n  'line.source': 'Start',\n  'line.target': 'End',\n  'line.arrow': 'Arrow',\n  'line.none': 'None',\n\n  // Stroke style\n  'stroke.solid': 'Solid',\n  'stroke.dashed': 'Dashed',\n  'stroke.dotted': 'Dotted',\n\n  //markdown example\n  'markdown.example': `# I have started\n\n  - Let me see who made this bug 🕵️ ♂️ 🔍\n    - 😯 💣\n      - Turns out it was me 👈 🎯 💘\n\n  - Unexpectedly, it cannot run; why is that 🚫 ⚙️ ❓\n    - Unexpectedly, it can run now; why is that? 🎢 ✨\n      - 🤯 ⚡ ➡️ 🎉\n\n  - What can run 🐞 🚀\n    - then do not touch it 🛑 ✋\n      - 👾 💥 🏹 🎯\n    \n  ## Boy or girl 👶 ❓ 🤷 ♂️ ♀️\n\n  ### Hello world 👋 🌍 ✨ 💻\n\n  #### Wow, a programmer 🤯 ⌨️ 💡 👩 💻`,\n\n  // Draw elements text\n  'draw.lineText': 'Text',\n  'draw.geometryText': 'Text',\n\n  // Mind map elements text\n  'mind.centralText': 'Central Topic',\n  'mind.abstractNodeText': 'Summary',\n\n  'tutorial.title': 'Drawnix',\n  'tutorial.description':\n    'All-in-one whiteboard, including mind maps, flowcharts, free drawing, and more',\n  'tutorial.dataDescription': 'All data is stored locally in your browser',\n  'tutorial.appToolbar': 'Export, language settings, ...',\n  'tutorial.creationToolbar': 'Select a tool to start your creation',\n  'tutorial.themeDescription': 'Switch between light and dark themes',\n};\n\nexport default enTranslations;\n"
  },
  {
    "path": "packages/drawnix/src/i18n/translations/index.ts",
    "content": "import zhTranslations from './zh';\nimport enTranslations from './en';\nimport ruTranslations from './ru';\nimport arTranslations from './ar';\nimport viTranslations from './vi';\n\nexport { zhTranslations, enTranslations, ruTranslations,arTranslations, viTranslations };\n"
  },
  {
    "path": "packages/drawnix/src/i18n/translations/ru.ts",
    "content": "import { Translations } from '../types';\n\nconst ruTranslations: Translations = {\n  // Toolbar items\n  'toolbar.hand': 'Рука — H',\n  'toolbar.selection': 'Выделение — V',\n  'toolbar.mind': 'Mind-карта — M',\n  'toolbar.text': 'Текст — T',\n  'toolbar.arrow': 'Стрелка — A',\n  'toolbar.shape': 'Фигуры',\n  'toolbar.image': 'Изображение — Cmd+U',\n  'toolbar.extraTools': 'Дополнительно',\n\n  'toolbar.pen': 'Карандаш — P',\n  'toolbar.eraser': 'Ластик — E',\n\n  'toolbar.arrow.straight': 'Прямая стрелка',\n  'toolbar.arrow.elbow': 'Ломаная стрелка',\n  'toolbar.arrow.curve': 'Кривая стрелка',\n\n  'toolbar.shape.rectangle': 'Прямоугольник — R',\n  'toolbar.shape.ellipse': 'Эллипс — O',\n  'toolbar.shape.triangle': 'Треугольник',\n  'toolbar.shape.terminal': 'Останов',\n  'toolbar.shape.noteCurlyLeft': 'Фигурная заметка — слева',\n  'toolbar.shape.noteCurlyRight': 'Фигурная заметка — справа',\n  'toolbar.shape.diamond': 'Ромб',\n  'toolbar.shape.parallelogram': 'Параллелограмм',\n  'toolbar.shape.roundRectangle': 'Скруглённый прямоугольник',\n\n  // Zoom controls\n  'zoom.in': 'Увеличить — Cmd++',\n  'zoom.out': 'Уменьшить — Cmd+-',\n  'zoom.fit': 'По размеру экрана',\n  'zoom.100': 'Сбросить к 100%',\n  \n  // Themes\n  'theme.default': 'Стандартная',\n  'theme.colorful': 'Красочная',\n  'theme.soft': 'Мягкая',\n  'theme.retro': 'Старинная',\n  'theme.dark': 'Тёмная',\n  'theme.starry': 'Звёздная',\n\n  // Colors\n  'color.none': 'Автоматически',\n  'color.unknown': 'Другой цвет',\n  'color.default': 'Чёрный',\n  'color.white': 'Белый',\n  'color.gray': 'Серый',\n  'color.deepBlue': 'Голубой',\n  'color.red': 'Красный',\n  'color.green': 'Зелёный',\n  'color.yellow': 'Жёлтый',\n  'color.purple': 'Фиолетовый',\n  'color.orange': 'Оранжевый',\n  'color.pastelPink': 'Розовый',\n  'color.cyan': 'Лиловый',\n  'color.brown': 'Коричневый',\n  'color.forestGreen': 'Сосновный',\n  'color.lightGray': 'Светло-серый',\n\n  // General\n  'general.undo': 'Отменить',\n  'general.redo': 'Вернуть',\n  'general.menu': 'Меню приложения',\n  'general.duplicate': 'Дублировать',\n  'general.delete': 'Удалить',\n  \n  // Language\n  'language.switcher': 'Language',\n  'language.chinese': '中文',\n  'language.english': 'English',\n  'language.russian': 'Русский',\n  'language.arabic': 'عربي',\n  'language.vietnamese': 'Tiếng Việt',\n  \n  // Menu items\n  'menu.open': 'Открыть',\n  'menu.saveFile': 'Сохранить',\n  'menu.exportImage': 'Экспортировать',\n  'menu.exportImage.svg': 'SVG',\n  'menu.exportImage.png': 'PNG',\n  'menu.exportImage.jpg': 'JPG',\n  'menu.cleanBoard': 'Очистить доску',\n  'menu.github': 'GitHub',\n  \n  // Dialog translations\n  'dialog.mermaid.title': 'Mermaid в Drawnix',\n  'dialog.mermaid.description': 'Поддерживаются',\n  'dialog.mermaid.flowchart': 'блок-схемы',\n  'dialog.mermaid.sequence': 'диаграммы последовательностей', \n  'dialog.mermaid.class': 'диаграммы классов',\n  'dialog.mermaid.otherTypes':\n    ' и другие диаграммы (преобразуются в изображения).',\n  'dialog.mermaid.syntax': 'Синтаксис Mermaid',\n  'dialog.mermaid.placeholder':\n    'Введите сюда описание вашей Mermaid-диаграммы…',\n  'dialog.mermaid.preview': 'Предпросмотр',\n  'dialog.mermaid.insert': 'Вставить',\n  'dialog.markdown.description':\n    'Поддерживается автоматическое преобразование синтаксиса Markdown в mind-карты.',\n  'dialog.markdown.syntax': 'Синтаксис Markdown',\n  'dialog.markdown.placeholder':\n    'Введите сюда описание вашего текста Markdown…',\n  'dialog.markdown.preview': 'Предпросмотр',\n  'dialog.markdown.insert': 'Вставить',\n  'dialog.error.loadMermaid': 'Не удалось загрузить библотеку Mermaid',\n  \n  // Extra tools menu items\n  'extraTools.mermaidToDrawnix': 'Mermaid в Drawnix',\n  'extraTools.markdownToDrawnix': 'Markdown в Drawnix',\n\n  // Clean confirm dialog\n  'cleanConfirm.title': 'Очистить доску',\n  'cleanConfirm.description':\n    'Это удалит всё содержимое доски. Вы хотите продолжить?',\n  'cleanConfirm.cancel': 'Отмена',\n  'cleanConfirm.ok': 'ОК',\n\n  // Link popup items\n  'popupLink.delLink': 'Удалить ссылку',\n\n  // Tool popup items\n  'popupToolbar.fillColor': 'Цвет заливки',\n  'popupToolbar.fontSize': 'Размер шрифта',\n  'popupToolbar.fontColor': 'Цвет текста',\n  'popupToolbar.link': 'Вставить ссылку',\n  'popupToolbar.stroke': 'Контур',\n  'popupToolbar.opacity': 'Прозрачность',\n  \n  // Text placeholders\n  'textPlaceholders.link': 'Ссылка',\n  'textPlaceholders.text': 'Текст',\n\n  // Line tool\n  'line.source': 'Начало',\n  'line.target': 'Конец',\n  'line.arrow': 'Стрелка',\n  'line.none': 'Нет',\n\n  // Stroke style\n  'stroke.solid': 'Сплошной',\n  'stroke.dashed': 'Штриховой',\n  'stroke.dotted': 'Пунктирный',\n\n  //markdown example\n  'markdown.example': `# I have started\n\n  - Let me see who made this bug 🕵️ ♂️ 🔍\n    - 😯 💣\n      - Turns out it was me 👈 🎯 💘\n\n  - Unexpectedly, it cannot run; why is that 🚫 ⚙️ ❓\n    - Unexpectedly, it can run now; why is that? 🎢 ✨\n      - 🤯 ⚡ ➡️ 🎉\n\n  - What can run 🐞 🚀\n    - then do not touch it 🛑 ✋\n      - 👾 💥 🏹 🎯\n    \n  ## Boy or girl 👶 ❓ 🤷 ♂️ ♀️\n\n  ### Hello world 👋 🌍 ✨ 💻\n\n  #### Wow, a programmer 🤯 ⌨️ 💡 👩 💻`,\n\n  // Draw elements text\n  'draw.lineText': 'Текст',\n  'draw.geometryText': 'Текст',\n  \n  // Mind map elements text\n  'mind.centralText': 'Центральная тема',\n  'mind.abstractNodeText': 'Резюме',\n  \n  'tutorial.title': 'Drawnix',\n  'tutorial.description':\n    'Универсальная доска: майнд-карты, блок-схемы, свободное рисование и многое другое',\n  'tutorial.dataDescription': 'Все данные хранятся локально в вашем браузере',\n  'tutorial.appToolbar': 'Экспорт, настройки языка, ...',\n  'tutorial.creationToolbar': 'Выберите инструмент, чтобы начать творить',\n  'tutorial.themeDescription': 'Переключение между светлой и тёмной темами',\n};\n\nexport default ruTranslations;\n"
  },
  {
    "path": "packages/drawnix/src/i18n/translations/vi.ts",
    "content": "import { Translations } from '../types';\n\nconst viTranslations: Translations = {\n    // Toolbar items\n    'toolbar.hand': 'Kéo — H',\n    'toolbar.selection': 'Chọn — V',\n    'toolbar.mind': 'Mind Map — M',\n    'toolbar.text': 'Văn bản — T',\n    'toolbar.arrow': 'Mũi tên — A',\n    'toolbar.shape': 'Hình dạng',\n    'toolbar.image': 'Hình ảnh — Cmd+U',\n    'toolbar.extraTools': 'Công cụ mở rộng',\n\n    'toolbar.pen': 'Bút vẽ — P',\n    'toolbar.eraser': 'Tẩy — E',\n\n    'toolbar.arrow.straight': 'Mũi tên thẳng',\n    'toolbar.arrow.elbow': 'Mũi tên vuông góc',\n    'toolbar.arrow.curve': 'Mũi tên cong',\n\n    'toolbar.shape.rectangle': 'Hình chữ nhật — R',\n    'toolbar.shape.ellipse': 'Hình elip — O',\n    'toolbar.shape.triangle': 'Hình tam giác',\n    'toolbar.shape.terminal': 'Terminal',\n    'toolbar.shape.noteCurlyLeft': 'Ghi chú ngoặc móc trái',\n    'toolbar.shape.noteCurlyRight': 'Ghi chú ngoặc móc phải',\n    'toolbar.shape.diamond': 'Hình thoi',\n    'toolbar.shape.parallelogram': 'Hình bình hành',\n    'toolbar.shape.roundRectangle': 'Hình chữ nhật bo tròn',\n\n    // Zoom controls\n    'zoom.in': 'Phóng to — Cmd++',\n    'zoom.out': 'Thu nhỏ — Cmd+-',\n    'zoom.fit': 'Vừa màn hình',\n    'zoom.100': 'Zoom 100%',\n\n    // Themes\n    'theme.default': 'Mặc định',\n    'theme.colorful': 'Đầy màu sắc',\n    'theme.soft': 'Nhẹ nhàng',\n    'theme.retro': 'Cổ điển',\n    'theme.dark': 'Tối',\n    'theme.starry': 'Bầu trời sao',\n\n    // Colors\n    'color.none': 'Màu chủ đề',\n    'color.unknown': 'Màu khác',\n    'color.default': 'Đen cơ bản',\n    'color.white': 'Trắng',\n    'color.gray': 'Xám',\n    'color.deepBlue': 'Xanh đậm',\n    'color.red': 'Đỏ',\n    'color.green': 'Xanh lá',\n    'color.yellow': 'Vàng',\n    'color.purple': 'Tím',\n    'color.orange': 'Cam',\n    'color.pastelPink': 'Hồng phấn',\n    'color.cyan': 'Xanh lơ',\n    'color.brown': 'Nâu',\n    'color.forestGreen': 'Xanh rừng',\n    'color.lightGray': 'Xám nhạt',\n\n    // General\n    'general.undo': 'Hoàn tác',\n    'general.redo': 'Làm lại',\n    'general.menu': 'Menu ứng dụng',\n    'general.duplicate': 'Nhân bản',\n    'general.delete': 'Xóa',\n\n    // Language\n    'language.switcher': 'Ngôn ngữ',\n    'language.chinese': '中文',\n    'language.english': 'English',\n    'language.russian': 'Русский',\n    'language.arabic': 'عربي',\n    'language.vietnamese': 'Tiếng Việt',\n\n    // Menu items\n    'menu.open': 'Mở',\n    'menu.saveFile': 'Lưu tệp',\n    'menu.exportImage': 'Xuất hình ảnh',\n    'menu.exportImage.svg': 'SVG',\n    'menu.exportImage.png': 'PNG',\n    'menu.exportImage.jpg': 'JPG',\n    'menu.cleanBoard': 'Xóa bảng',\n    'menu.github': 'GitHub',\n\n    // Dialog translations\n    'dialog.mermaid.title': 'Mermaid sang Drawnix',\n    'dialog.mermaid.description': 'Hiện hỗ trợ',\n    'dialog.mermaid.flowchart': 'lưu đồ',\n    'dialog.mermaid.sequence': 'biểu đồ tuần tự',\n    'dialog.mermaid.class': 'biểu đồ lớp',\n    'dialog.mermaid.otherTypes':\n        ', và các loại biểu đồ khác (hiển thị dưới dạng hình ảnh).',\n    'dialog.mermaid.syntax': 'Cú pháp Mermaid',\n    'dialog.mermaid.placeholder': 'Viết định nghĩa biểu đồ Mermaid của bạn ở đây...',\n    'dialog.mermaid.preview': 'Xem trước',\n    'dialog.mermaid.insert': 'Chèn',\n    'dialog.markdown.description':\n        'Hỗ trợ tự động chuyển đổi cú pháp Markdown sang sơ đồ tư duy.',\n    'dialog.markdown.syntax': 'Cú pháp Markdown',\n    'dialog.markdown.placeholder': 'Viết nội dung Markdown của bạn ở đây...',\n    'dialog.markdown.preview': 'Xem trước',\n    'dialog.markdown.insert': 'Chèn',\n    'dialog.error.loadMermaid': 'Không thể tải thư viện Mermaid',\n\n    // Extra tools menu items\n    'extraTools.mermaidToDrawnix': 'Mermaid sang Drawnix',\n    'extraTools.markdownToDrawnix': 'Markdown sang Drawnix',\n\n    // Clean confirm dialog\n    'cleanConfirm.title': 'Xóa bảng',\n    'cleanConfirm.description':\n        'Thao tác này sẽ xóa toàn bộ bảng. Bạn có muốn tiếp tục không?',\n    'cleanConfirm.cancel': 'Hủy',\n    'cleanConfirm.ok': 'Đồng ý',\n\n    // Link popup items\n    'popupLink.delLink': 'Xóa liên kết',\n\n    // Tool popup items\n    'popupToolbar.fillColor': 'Màu tô',\n    'popupToolbar.fontSize': 'Cỡ chữ',\n    'popupToolbar.fontColor': 'Màu chữ',\n    'popupToolbar.link': 'Chèn liên kết',\n    'popupToolbar.stroke': 'Đường viền',\n    'popupToolbar.opacity': 'Độ trong suốt',\n\n    // Text placeholders\n    'textPlaceholders.link': 'Liên kết',\n    'textPlaceholders.text': 'Văn bản',\n\n    // Line tool\n    'line.source': 'Bắt đầu',\n    'line.target': 'Kết thúc',\n    'line.arrow': 'Mũi tên',\n    'line.none': 'Không',\n\n    // Stroke style\n    'stroke.solid': 'Nét liền',\n    'stroke.dashed': 'Nét đứt',\n    'stroke.dotted': 'Nét chấm',\n\n    //markdown example\n    'markdown.example': `# Tôi đã bắt đầu\n  \n    - Hãy xem ai đã tạo ra lỗi này 🕵️ ♂️ 🔍\n      - 😯 💣\n        - Hóa ra là tôi 👈 🎯 💘\n  \n    - Bất ngờ thay, nó không chạy được; tại sao vậy 🚫 ⚙️ ❓\n      - Bất ngờ thay, giờ nó chạy được rồi; tại sao vậy? 🎢 ✨\n        - 🤯 ⚡ ➡️ 🎉\n  \n    - Cái gì chạy được 🐞 🚀\n      - thì đừng chạm vào nó 🛑 ✋\n        - 👾 💥 🏹 🎯\n      \n    ## Trai hay gái 👶 ❓ 🤷 ♂️ ♀️\n  \n    ### Xin chào thế giới 👋 🌍 ✨ 💻\n  \n    #### Wow, một lập trình viên 🤯 ⌨️ 💡 👩 💻`,\n\n    // Draw elements text\n    'draw.lineText': 'Văn bản',\n    'draw.geometryText': 'Văn bản',\n\n    // Mind map elements text\n    'mind.centralText': 'Chủ đề trung tâm',\n    'mind.abstractNodeText': 'Tóm tắt',\n\n    'tutorial.title': 'DPIT Draw MindMap',\n    'tutorial.description': 'Bảng trắng tất cả trong một, bao gồm sơ đồ tư duy, lưu đồ, vẽ tự do và hơn thế nữa',\n    'tutorial.dataDescription': 'Tất cả dữ liệu được lưu trữ cục bộ trong trình duyệt của bạn',\n    'tutorial.appToolbar': 'Xuất, cài đặt ngôn ngữ, ...',\n    'tutorial.creationToolbar': 'Chọn một công cụ để bắt đầu sáng tạo',\n    'tutorial.themeDescription': 'Chuyển đổi giữa chế độ sáng và tối',\n};\n\nexport default viTranslations;\n"
  },
  {
    "path": "packages/drawnix/src/i18n/translations/zh.ts",
    "content": "import { Translations } from '../types';\n\nconst zhTranslations: Translations = {\n  // Toolbar items\n  'toolbar.hand': '手形工具 — H',\n  'toolbar.selection': '选择 — V',\n  'toolbar.mind': '思维导图 — M',\n  'toolbar.text': '文本 — T',\n  'toolbar.arrow': '箭头 — A',\n  'toolbar.shape': '形状',\n  'toolbar.image': '图片 — Cmd+U',\n  'toolbar.extraTools': '更多工具',\n\n  'toolbar.pen': '画笔 — P',\n  'toolbar.eraser': '橡皮擦 — E',\n\n  'toolbar.arrow.straight': '直线',\n  'toolbar.arrow.elbow': '肘线',\n  'toolbar.arrow.curve': '曲线',\n\n  'toolbar.shape.rectangle': '长方形 — R',\n  'toolbar.shape.ellipse': '圆 — O',\n  'toolbar.shape.triangle': '三角形',\n  'toolbar.shape.terminal': '椭圆角矩形',\n  'toolbar.shape.noteCurlyLeft': '左花括注释',\n  'toolbar.shape.noteCurlyRight': '右花括注释',\n  'toolbar.shape.diamond': '菱形',\n  'toolbar.shape.parallelogram': '平行四边形',\n  'toolbar.shape.roundRectangle': '圆角矩形',\n\n  // Zoom controls\n  'zoom.in': '放大 — Cmd++',\n  'zoom.out': '缩小 — Cmd+-',\n  'zoom.fit': '自适应',\n  'zoom.100': '缩放至 100%',\n\n  // Themes\n  'theme.default': '默认',\n  'theme.colorful': '缤纷',\n  'theme.soft': '柔和',\n  'theme.retro': '复古',\n  'theme.dark': '暗夜',\n  'theme.starry': '星空',\n\n  // Colors\n  'color.none': '主题颜色',\n  'color.unknown': '其他颜色',\n  'color.default': '黑色',\n  'color.white': '白色',\n  'color.gray': '灰色',\n  'color.deepBlue': '深蓝色',\n  'color.red': '红色',\n  'color.green': '绿色',\n  'color.yellow': '黄色',\n  'color.purple': '紫色',\n  'color.orange': '橙色',\n  'color.pastelPink': '淡粉色',\n  'color.cyan': '青色',\n  'color.brown': '棕色',\n  'color.forestGreen': '森绿色',\n  'color.lightGray': '浅灰色',\n\n  // General\n  'general.undo': '撤销',\n  'general.redo': '重做',\n  'general.menu': '应用菜单',\n  'general.duplicate': '复制',\n  'general.delete': '删除',\n\n  // Language\n  'language.switcher': 'Language',\n  'language.chinese': '中文',\n  'language.english': 'English',\n  'language.russian': 'Русский',\n  'language.arabic': 'عربي',\n  'language.vietnamese': 'Tiếng Việt',\n  // Menu items\n  'menu.open': '打开',\n  'menu.saveFile': '保存文件',\n  'menu.exportImage': '导出图片',\n  'menu.exportImage.svg': 'SVG',\n  'menu.exportImage.png': 'PNG',\n  'menu.exportImage.jpg': 'JPG',\n  'menu.cleanBoard': '清除画布',\n  'menu.github': 'GitHub',\n\n  // Dialog translations\n  'dialog.mermaid.title': 'Mermaid 转 Drawnix',\n  'dialog.mermaid.description': '目前仅支持',\n  'dialog.mermaid.flowchart': '流程图',\n  'dialog.mermaid.sequence': '序列图',\n  'dialog.mermaid.class': '类图',\n  'dialog.mermaid.otherTypes': '。其他类型在 Drawnix 中将以图片呈现。',\n  'dialog.mermaid.syntax': 'Mermaid 语法',\n  'dialog.mermaid.placeholder': '在此处编写 Mermaid 图表定义…',\n  'dialog.mermaid.preview': '预览',\n  'dialog.mermaid.insert': '插入',\n  'dialog.markdown.description': '支持 Markdown 语法自动转换为思维导图。',\n  'dialog.markdown.syntax': 'Markdown 语法',\n  'dialog.markdown.placeholder': '在此处编写 Markdown 文本定义…',\n  'dialog.markdown.preview': '预览',\n  'dialog.markdown.insert': '插入',\n  'dialog.error.loadMermaid': '加载 Mermaid 库失败',\n\n  // Extra tools menu items\n  'extraTools.mermaidToDrawnix': 'Mermaid 到 Drawnix',\n  'extraTools.markdownToDrawnix': 'Markdown 到 Drawnix',\n\n  // Clean confirm dialog\n  'cleanConfirm.title': '清除画布',\n  'cleanConfirm.description': '这将会清除整个画布。你是否要继续?',\n  'cleanConfirm.cancel': '取消',\n  'cleanConfirm.ok': '确认',\n\n  // Link popup items\n  'popupLink.delLink': '移除连结',\n\n  // Tool popup items\n  'popupToolbar.fillColor': '填充颜色',\n  'popupToolbar.fontSize': '字号',\n  'popupToolbar.fontColor': '字体颜色',\n  'popupToolbar.link': '链接',\n  'popupToolbar.stroke': '边框',\n  'popupToolbar.opacity': '不透明度',\n\n  // Text placeholders\n  'textPlaceholders.link': '链接',\n  'textPlaceholders.text': '文本',\n\n  // Line tool\n  'line.source': '起点',\n  'line.target': '终点',\n  'line.arrow': '箭头',\n  'line.none': '无',\n\n  // Stroke style\n  'stroke.solid': '实线',\n  'stroke.dashed': '虚线',\n  'stroke.dotted': '点线',\n\n  // Draw elements text\n  'draw.lineText': '文本',\n  'draw.geometryText': '文本',\n\n  // Mind map elements text\n  'mind.centralText': '中心主题',\n  'mind.abstractNodeText': '摘要',\n\n  //markdown example\n  'markdown.example': `# 我开始了\n  \n  - 让我看看是谁搞出了这个 bug 🕵️ ♂️ 🔍\n    - 😯 💣\n      - 原来是我 👈 🎯 💘\n  \n  - 竟然不可以运行，为什么呢 🚫 ⚙️ ❓\n    - 竟然可以运行了，为什么呢？🎢 ✨\n      - 🤯 ⚡ ➡️ 🎉\n  \n  - 能运行起来的 🐞 🚀\n    - 就不要去动它 🛑 ✋\n      - 👾 💥 🏹 🎯\n      \n  ## 男孩还是女孩 👶 ❓ 🤷 ♂️ ♀️\n  \n  ### Hello world 👋 🌍 ✨ 💻\n  \n  #### 哇 是个程序员 🤯 ⌨️ 💡 👩 💻`,\n\n  'tutorial.title': 'Drawnix',\n  'tutorial.description': 'All-in-one 白板，包含思维导图、流程图、自由画笔等',\n  'tutorial.dataDescription': '所有数据被存在你的浏览器本地',\n  'tutorial.appToolbar': '导出，语言设置，...',\n  'tutorial.creationToolbar': '选择一个工具开始你的创作',\n  'tutorial.themeDescription': '在明亮和黑暗主题之间切换',\n};\n\nexport default zhTranslations;\n"
  },
  {
    "path": "packages/drawnix/src/i18n/types.ts",
    "content": "import { ReactNode } from 'react';\n\n// Define supported languages\nexport type Language = 'zh' | 'en' | 'ru' | 'ar' | 'vi';\n\n// Define translation keys and their corresponding values\nexport interface Translations {\n  // Toolbar items\n  'toolbar.hand': string;\n  'toolbar.selection': string;\n  'toolbar.mind': string;\n  'toolbar.text': string;\n  'toolbar.arrow': string;\n  'toolbar.shape': string;\n  'toolbar.image': string;\n  'toolbar.extraTools': string;\n\n  'toolbar.pen': string;\n  'toolbar.eraser': string;\n\n  'toolbar.arrow.straight': string;\n  'toolbar.arrow.elbow': string;\n  'toolbar.arrow.curve': string;\n\n  'toolbar.shape.rectangle': string;\n  'toolbar.shape.ellipse': string;\n  'toolbar.shape.triangle': string;\n  'toolbar.shape.terminal': string;\n  'toolbar.shape.noteCurlyLeft': string;\n  'toolbar.shape.noteCurlyRight': string;\n  'toolbar.shape.diamond': string;\n  'toolbar.shape.parallelogram': string;\n  'toolbar.shape.roundRectangle': string;\n\n  // Zoom controls\n  'zoom.in': string;\n  'zoom.out': string;\n  'zoom.fit': string;\n  'zoom.100': string;\n\n  // Themes\n  'theme.default': string;\n  'theme.colorful': string;\n  'theme.soft': string;\n  'theme.retro': string;\n  'theme.dark': string;\n  'theme.starry': string;\n\n  // Colors\n  'color.none': string;\n  'color.unknown': string;\n  'color.default': string;\n  'color.white': string;\n  'color.gray': string;\n  'color.deepBlue': string;\n  'color.red': string;\n  'color.green': string;\n  'color.yellow': string;\n  'color.purple': string;\n  'color.orange': string;\n  'color.pastelPink': string;\n  'color.cyan': string;\n  'color.brown': string;\n  'color.forestGreen': string;\n  'color.lightGray': string;\n\n  // General\n  'general.undo': string;\n  'general.redo': string;\n  'general.menu': string;\n  'general.duplicate': string;\n  'general.delete': string;\n\n  // Language\n  'language.switcher': string;\n  'language.chinese': string;\n  'language.english': string;\n  'language.russian': string;\n  'language.arabic': string;\n  'language.vietnamese': string;\n\n  // Menu items\n  'menu.open': string;\n  'menu.saveFile': string;\n  'menu.exportImage': string;\n  'menu.exportImage.svg': string;\n  'menu.exportImage.png': string;\n  'menu.exportImage.jpg': string;\n  'menu.cleanBoard': string;\n  'menu.github': string;\n\n  // Dialog translations\n  'dialog.mermaid.title': string;\n  'dialog.mermaid.description': string;\n  'dialog.mermaid.flowchart': string;\n  'dialog.mermaid.sequence': string;\n  'dialog.mermaid.class': string;\n  'dialog.mermaid.otherTypes': string;\n  'dialog.mermaid.syntax': string;\n  'dialog.mermaid.placeholder': string;\n  'dialog.mermaid.preview': string;\n  'dialog.mermaid.insert': string;\n  'dialog.markdown.description': string;\n  'dialog.markdown.syntax': string;\n  'dialog.markdown.placeholder': string;\n  'dialog.markdown.preview': string;\n  'dialog.markdown.insert': string;\n  'dialog.error.loadMermaid': string;\n\n  // Extra tools menu items\n  'extraTools.mermaidToDrawnix': string;\n  'extraTools.markdownToDrawnix': string;\n\n  // Clean confirm dialog\n  'cleanConfirm.title': string;\n  'cleanConfirm.description': string;\n  'cleanConfirm.cancel': string;\n  'cleanConfirm.ok': string;\n\n  // Link popup items\n  'popupLink.delLink': string;\n\n  // Tool popup items\n  'popupToolbar.fillColor': string;\n  'popupToolbar.fontSize': string;\n  'popupToolbar.fontColor': string;\n  'popupToolbar.link': string;\n  'popupToolbar.stroke': string;\n  'popupToolbar.opacity': string;\n\n  // Text placeholders\n  'textPlaceholders.link': string;\n  'textPlaceholders.text': string;\n\n  // Line tool\n  'line.source': string;\n  'line.target': string;\n  'line.arrow': string;\n  'line.none': string;\n\n  // Stroke style\n  'stroke.solid': string;\n  'stroke.dashed': string;\n  'stroke.dotted': string;\n\n  //markdown example\n  'markdown.example': string;\n\n  // Draw elements text\n  'draw.lineText': string;\n  'draw.geometryText': string;\n\n  // Mind map elements text\n  'mind.centralText': string;\n  'mind.abstractNodeText': string;\n\n  'tutorial.title': string;\n  'tutorial.description': string;\n  'tutorial.dataDescription': string;\n  'tutorial.appToolbar': string;\n  'tutorial.creationToolbar': string;\n  'tutorial.themeDescription': string;\n}\n\n// I18n context interface\nexport interface I18nContextType {\n  language: Language;\n  setLanguage: (language: Language) => void;\n  t: (key: keyof Translations) => string;\n}\n\n// Provider props\nexport interface I18nProviderProps {\n  children: ReactNode;\n  defaultLanguage?: Language;\n}\n"
  },
  {
    "path": "packages/drawnix/src/i18n.tsx",
    "content": "export { I18nProvider, useI18n, i18nInsidePlaitHook } from './i18n/index';\nexport type { Language, Translations, I18nContextType } from './i18n/types';\n"
  },
  {
    "path": "packages/drawnix/src/index.ts",
    "content": "export * from './drawnix';\nexport * from './utils';\nexport * from './i18n';\n"
  },
  {
    "path": "packages/drawnix/src/keys.ts",
    "content": "import { IS_APPLE, IS_IOS } from '@plait/core';\n\nexport const CODES = {\n  EQUAL: 'Equal',\n  MINUS: 'Minus',\n  NUM_ADD: 'NumpadAdd',\n  NUM_SUBTRACT: 'NumpadSubtract',\n  NUM_ZERO: 'Numpad0',\n  BRACKET_RIGHT: 'BracketRight',\n  BRACKET_LEFT: 'BracketLeft',\n  ONE: 'Digit1',\n  TWO: 'Digit2',\n  THREE: 'Digit3',\n  NINE: 'Digit9',\n  QUOTE: 'Quote',\n  ZERO: 'Digit0',\n  SLASH: 'Slash',\n  C: 'KeyC',\n  D: 'KeyD',\n  H: 'KeyH',\n  V: 'KeyV',\n  Z: 'KeyZ',\n  R: 'KeyR',\n  S: 'KeyS',\n} as const;\n\nexport const KEYS = {\n  ARROW_DOWN: 'ArrowDown',\n  ARROW_LEFT: 'ArrowLeft',\n  ARROW_RIGHT: 'ArrowRight',\n  ARROW_UP: 'ArrowUp',\n  PAGE_UP: 'PageUp',\n  PAGE_DOWN: 'PageDown',\n  BACKSPACE: 'Backspace',\n  ALT: 'Alt',\n  CTRL_OR_CMD: IS_IOS || IS_APPLE ? 'metaKey' : 'ctrlKey',\n  DELETE: 'Delete',\n  ENTER: 'Enter',\n  ESCAPE: 'Escape',\n  QUESTION_MARK: '?',\n  SPACE: ' ',\n  TAB: 'Tab',\n  CHEVRON_LEFT: '<',\n  CHEVRON_RIGHT: '>',\n  PERIOD: '.',\n  COMMA: ',',\n  SUBTRACT: '-',\n  SLASH: '/',\n\n  A: 'a',\n  C: 'c',\n  D: 'd',\n  E: 'e',\n  F: 'f',\n  G: 'g',\n  H: 'h',\n  I: 'i',\n  L: 'l',\n  O: 'o',\n  P: 'p',\n  Q: 'q',\n  R: 'r',\n  S: 's',\n  T: 't',\n  V: 'v',\n  X: 'x',\n  Y: 'y',\n  Z: 'z',\n  K: 'k',\n  W: 'w',\n\n  0: '0',\n  1: '1',\n  2: '2',\n  3: '3',\n  4: '4',\n  5: '5',\n  6: '6',\n  7: '7',\n  8: '8',\n  9: '9',\n} as const;\n\nexport type Key = keyof typeof KEYS;\n"
  },
  {
    "path": "packages/drawnix/src/libs/image-viewer.ts",
    "content": "interface ImageViewerOptions {\n  zoomStep?: number;\n  minZoom?: number;\n  maxZoom?: number;\n  enableKeyboard?: boolean;\n}\n\ninterface ImageState {\n  zoom: number;\n  x: number;\n  y: number;\n  isDragging: boolean;\n  dragStartX: number;\n  dragStartY: number;\n  imageStartX: number;\n  imageStartY: number;\n}\n\nexport class ImageViewer {\n  private options: Required<ImageViewerOptions>;\n  private overlay: HTMLDivElement | null = null;\n  private imageContainer: HTMLDivElement | null = null;\n  private image: HTMLImageElement | null = null;\n  private closeButton: HTMLDivElement | null = null;\n  private controlsContainer: HTMLDivElement | null = null;\n  private delegationHandler: ((e: Event) => void) | null = null;\n  private dragHandler: ((e: MouseEvent) => void) | null = null;\n  private mouseUpHandler: (() => void) | null = null;\n  private animationFrameId: number | null = null;\n  private pendingUpdate = false;\n  private state: ImageState = {\n    zoom: 1,\n    x: 0,\n    y: 0,\n    isDragging: false,\n    dragStartX: 0,\n    dragStartY: 0,\n    imageStartX: 0,\n    imageStartY: 0,\n  };\n\n  constructor(options: ImageViewerOptions = {}) {\n    this.options = {\n      zoomStep: options.zoomStep || 0.2,\n      minZoom: options.minZoom || 0.1,\n      maxZoom: options.maxZoom || 5,\n      enableKeyboard: options.enableKeyboard !== false,\n    };\n\n    this.addStyles();\n    this.bindEvents();\n  }\n\n  // 打开图片查看器\n  open(src: string, alt = ''): void {\n    this.createOverlay();\n    this.createImage(src, alt);\n    this.resetState();\n    document.body.style.overflow = 'hidden';\n  }\n\n  // 关闭图片查看器\n  close(): void {\n    if (this.overlay) {\n      // 清理拖动事件监听器\n      this.cleanupDragEvents();\n      \n      // 清理全局事件监听器\n      document.removeEventListener('mousemove', this.delegationHandler!);\n      document.removeEventListener('mouseup', this.delegationHandler!);\n      document.removeEventListener('keydown', this.delegationHandler!);\n      document.removeEventListener('wheel', this.delegationHandler!);\n      \n      // 取消动画帧\n      if (this.animationFrameId) {\n        cancelAnimationFrame(this.animationFrameId);\n        this.animationFrameId = null;\n      }\n      \n      document.body.removeChild(this.overlay);\n      this.overlay = null;\n      this.image = null;\n      this.imageContainer = null;\n      this.closeButton = null;\n      this.controlsContainer = null;\n      this.delegationHandler = null;\n      this.dragHandler = null;\n      this.mouseUpHandler = null;\n      this.pendingUpdate = false;\n    }\n    document.body.style.overflow = '';\n  }\n\n  // 创建遮罩层\n  private createOverlay(): void {\n    this.overlay = document.createElement('div');\n    this.overlay.style.cssText = `\n      position: fixed;\n      top: 0;\n      left: 0;\n      width: 100%;\n      height: 100%;\n      background: rgba(45, 45, 45, 0.95);\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      z-index: 9999;\n      cursor: grab;\n    `;\n\n    // 点击遮罩层关闭\n    this.overlay.addEventListener('click', (e) => {\n      if (e.target === this.overlay) {\n        this.close();\n      }\n    });\n\n    this.createCloseButton();\n    this.createControls();\n    document.body.appendChild(this.overlay);\n  }\n\n  // 创建关闭按钮\n  private createCloseButton(): void {\n    this.closeButton = document.createElement('div');\n    this.closeButton.innerHTML = '×';\n    this.closeButton.className = 'image-viewer-close-btn';\n    this.closeButton.addEventListener('click', () => this.close());\n    this.overlay!.appendChild(this.closeButton);\n  }\n\n  // 创建控制按钮\n  private createControls(): void {\n    this.controlsContainer = document.createElement('div');\n    this.controlsContainer.style.cssText = `\n      position: absolute;\n      bottom: 30px;\n      left: 50%;\n      transform: translateX(-50%);\n      display: flex;\n      gap: 10px;\n      z-index: 10001;\n    `;\n\n    this.addStyles();\n\n    // 放大按钮\n    const zoomInBtn = document.createElement('button');\n    zoomInBtn.innerHTML = '+';\n    zoomInBtn.className = 'image-viewer-control-btn';\n    zoomInBtn.addEventListener('click', () => this.zoomIn());\n\n    // 缩小按钮\n    const zoomOutBtn = document.createElement('button');\n    zoomOutBtn.innerHTML = '-';\n    zoomOutBtn.className = 'image-viewer-control-btn';\n    zoomOutBtn.addEventListener('click', () => this.zoomOut());\n\n    // 重置按钮\n    const resetBtn = document.createElement('button');\n    resetBtn.innerHTML = '⌂';\n    resetBtn.className = 'image-viewer-control-btn';\n    resetBtn.addEventListener('click', () => this.resetState());\n\n    this.controlsContainer.appendChild(zoomOutBtn);\n    this.controlsContainer.appendChild(resetBtn);\n    this.controlsContainer.appendChild(zoomInBtn);\n    this.overlay!.appendChild(this.controlsContainer);\n  }\n\n  // 创建图片元素\n  private  createImage(src: string, alt: string): void {\n    this.imageContainer = document.createElement('div');\n    this.imageContainer.style.cssText = `\n      position: relative;\n      cursor: grab;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      max-width: calc(100vw - 80px);\n      max-height: calc(100vh - 160px);\n    `;\n\n    this.image = document.createElement('img');\n    this.image.src = src;\n    this.image.alt = alt;\n    this.image.style.cssText = `\n      max-width: calc(100vw - 80px);\n      max-height: calc(100vh - 160px);\n      width: auto;\n      height: auto;\n      display: block;\n      user-select: none;\n      pointer-events: none;\n      object-fit: contain;\n    `;\n\n    this.imageContainer.appendChild(this.image);\n    this.overlay!.appendChild(this.imageContainer);\n\n    // 绑定拖拽事件\n    this.bindDragEvents();\n  }\n\n  // 绑定拖拽事件\n  private bindDragEvents(): void {\n    if (!this.imageContainer) return;\n\n    // 使用 requestAnimationFrame 优化的拖动处理器\n    this.dragHandler = (e: MouseEvent) => {\n      if (!this.state.isDragging) return;\n\n      const deltaX = e.clientX - this.state.dragStartX;\n      const deltaY = e.clientY - this.state.dragStartY;\n\n      this.state.x = this.state.imageStartX + deltaX;\n      this.state.y = this.state.imageStartY + deltaY;\n\n      // 使用 requestAnimationFrame 优化渲染\n      if (!this.pendingUpdate) {\n        this.pendingUpdate = true;\n        this.animationFrameId = requestAnimationFrame(() => {\n          this.updateImageTransform();\n          this.pendingUpdate = false;\n        });\n      }\n    };\n\n    this.mouseUpHandler = () => {\n      if (this.state.isDragging) {\n        this.state.isDragging = false;\n        if (this.imageContainer) {\n          this.imageContainer.style.cursor = 'grab';\n        }\n        if (this.overlay) {\n          this.overlay.style.cursor = 'grab';\n        }\n        this.cleanupDragEvents();\n      }\n    };\n\n    this.imageContainer.addEventListener('mousedown', (e) => {\n      e.preventDefault();\n      this.state.isDragging = true;\n      this.state.dragStartX = e.clientX;\n      this.state.dragStartY = e.clientY;\n      this.state.imageStartX = this.state.x;\n      this.state.imageStartY = this.state.y;\n\n      if (this.imageContainer) {\n        this.imageContainer.style.cursor = 'grabbing';\n      }\n      if (this.overlay) {\n        this.overlay.style.cursor = 'grabbing';\n      }\n\n      // 添加事件监听器\n      if (this.dragHandler && this.mouseUpHandler) {\n        document.addEventListener('mousemove', this.dragHandler, { passive: true });\n        document.addEventListener('mouseup', this.mouseUpHandler, { once: true });\n      }\n    });\n  }\n\n  // 清理拖动事件监听器\n  private cleanupDragEvents(): void {\n    if (this.dragHandler) {\n      document.removeEventListener('mousemove', this.dragHandler);\n    }\n    if (this.mouseUpHandler) {\n      document.removeEventListener('mouseup', this.mouseUpHandler);\n    }\n  }\n\n  // 绑定全局事件\n  private bindEvents(): void {\n    this.delegationHandler = (e: Event) => {\n      if (!this.overlay) return;\n\n      if (e.type === 'keydown' && this.options.enableKeyboard) {\n        const keyboardEvent = e as KeyboardEvent;\n        switch (keyboardEvent.key) {\n          case 'Escape':\n            this.close();\n            break;\n          case '+':\n          case '=':\n            keyboardEvent.preventDefault();\n            this.zoomIn();\n            break;\n          case '-':\n            keyboardEvent.preventDefault();\n            this.zoomOut();\n            break;\n          case '0':\n            keyboardEvent.preventDefault();\n            this.resetState();\n            break;\n        }\n      } else if (e.type === 'wheel') {\n        const wheelEvent = e as WheelEvent;\n        wheelEvent.preventDefault();\n        if (wheelEvent.deltaY < 0) {\n          this.zoomIn();\n        } else {\n          this.zoomOut();\n        }\n      }\n    };\n\n    document.addEventListener('keydown', this.delegationHandler);\n    document.addEventListener('wheel', this.delegationHandler, {\n      passive: false,\n    });\n  }\n\n  // 放大\n  private zoomIn(): void {\n    this.state.zoom = Math.min(\n      this.state.zoom + this.options.zoomStep,\n      this.options.maxZoom\n    );\n    this.updateImageTransform();\n  }\n\n  // 缩小\n  private zoomOut(): void {\n    this.state.zoom = Math.max(\n      this.state.zoom - this.options.zoomStep,\n      this.options.minZoom\n    );\n    this.updateImageTransform();\n  }\n\n  // 重置状态\n  private resetState(): void {\n    this.state.zoom = 1;\n    this.state.x = 0;\n    this.state.y = 0;\n    this.updateImageTransform();\n  }\n\n  // 更新图片变换\n  private updateImageTransform(): void {\n    if (!this.imageContainer) return;\n    this.imageContainer.style.transform = `\n      translate(${this.state.x}px, ${this.state.y}px) \n      scale(${this.state.zoom})\n    `;\n  }\n\n  private styleElement: HTMLStyleElement | null = null;\n\n  // 添加样式\n  private addStyles(): void {\n    if (!this.styleElement) {\n      this.styleElement = document.createElement('style');\n      this.styleElement.textContent = `\n        .image-viewer-control-btn {\n          background: rgba(0, 0, 0, 0.8);\n          color: white;\n          border: none;\n          padding: 8px 14px;\n          border-radius: 4px;\n          cursor: pointer;\n          font-size: 18px;\n          transition: background 0.2s;\n          user-select: none;\n        }\n        \n        .image-viewer-control-btn:hover {\n          background: rgba(0, 0, 0, 0.4);\n        }\n        \n        .image-viewer-close-btn {\n          position: absolute;\n          top: 20px;\n          right: 30px;\n          color: white;\n          font-size: 18px;\n          cursor: pointer;\n          z-index: 10001;\n          user-select: none;\n          width: 36px;\n          height: 34px;\n          display: flex;\n          border-radius: 50%;\n          justify-content: center;\n          background: rgba(0, 0, 0, 0.8);\n          transition: all 0.2s ease;\n          line-height: 34px;\n          padding-bottom:2px;\n        }\n        \n        .image-viewer-close-btn:hover {\n          background: rgba(0, 0, 0, 0.4);\n        }\n      `;\n      document.head.appendChild(this.styleElement);\n    }\n  }\n\n  // 移除样式\n  private removeStyles(): void {\n    if (this.styleElement) {\n      document.head.removeChild(this.styleElement);\n      this.styleElement = null;\n    }\n  }\n\n  // 销毁实例\n  destroy(): void {\n    this.close();\n    this.removeStyles();\n  }\n}\n"
  },
  {
    "path": "packages/drawnix/src/plugins/components/emoji.tsx",
    "content": "import type { EmojiProps } from '@plait/mind';\n\nexport const Emoji: React.FC<EmojiProps> = (props: EmojiProps) => {\n  return (\n    <span\n      className=\"mind-node-emoji\"\n      style={{ fontSize: `${props.fontSize}px` }}\n    >\n      {props.emojiItem.name}\n    </span>\n  );\n};\n"
  },
  {
    "path": "packages/drawnix/src/plugins/components/image.tsx",
    "content": "import type { ImageProps } from '@plait/common';\nimport classNames from 'classnames';\n\nexport const Image: React.FC<ImageProps> = (props: ImageProps) => {\n  const imgProps = {\n    src: props.imageItem.url,\n    draggable: false,\n    width: '100%',\n  };\n  return (\n    <div style={{ display: 'flex' }}>\n      <img\n        {...imgProps}\n        className={classNames('image-origin', {\n          'image-origin--focus': props.isFocus,\n        })}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/drawnix/src/plugins/freehand/freehand.component.ts",
    "content": "import {\n  PlaitBoard,\n  PlaitPluginElementContext,\n  OnContextChanged,\n  RectangleClient,\n  isSelectionMoving,\n  ACTIVE_STROKE_WIDTH,\n} from '@plait/core';\nimport {\n  ActiveGenerator,\n  CommonElementFlavour,\n  createActiveGenerator,\n  hasResizeHandle,\n} from '@plait/common';\nimport { Freehand } from './type';\nimport { FreehandGenerator } from './freehand.generator';\n\nexport class FreehandComponent\n  extends CommonElementFlavour<Freehand, PlaitBoard>\n  implements OnContextChanged<Freehand, PlaitBoard>\n{\n  constructor() {\n    super();\n  }\n\n  activeGenerator!: ActiveGenerator<Freehand>;\n\n  generator!: FreehandGenerator;\n\n  initializeGenerator() {\n    this.activeGenerator = createActiveGenerator(this.board, {\n      getRectangle: (element: Freehand) => {\n        return RectangleClient.getRectangleByPoints(element.points);\n      },\n      getStrokeWidth: () => ACTIVE_STROKE_WIDTH,\n      getStrokeOpacity: () => 1,\n      hasResizeHandle: () => {\n        return hasResizeHandle(this.board, this.element);\n      },\n    });\n    this.generator = new FreehandGenerator(this.board);\n  }\n\n  initialize(): void {\n    super.initialize();\n    this.initializeGenerator();\n    this.generator.processDrawing(this.element, this.getElementG());\n  }\n\n  onContextChanged(\n    value: PlaitPluginElementContext<Freehand, PlaitBoard>,\n    previous: PlaitPluginElementContext<Freehand, PlaitBoard>\n  ) {\n    if (value.element !== previous.element || value.hasThemeChanged) {\n      this.generator.processDrawing(this.element, this.getElementG());\n      this.activeGenerator.processDrawing(\n        this.element,\n        PlaitBoard.getActiveHost(this.board),\n        {\n          selected: this.selected,\n        }\n      );\n    } else {\n      const needUpdate = value.selected !== previous.selected;\n      if (needUpdate || value.selected) {\n        this.activeGenerator.processDrawing(\n          this.element,\n          PlaitBoard.getActiveHost(this.board),\n          {\n            selected: this.selected,\n          }\n        );\n      }\n    }\n  }\n\n  destroy(): void {\n    super.destroy();\n    this.activeGenerator?.destroy();\n  }\n}\n"
  },
  {
    "path": "packages/drawnix/src/plugins/freehand/freehand.generator.ts",
    "content": "import { Generator } from '@plait/common';\nimport { PlaitBoard, setStrokeLinecap } from '@plait/core';\nimport { Options } from 'roughjs/bin/core';\nimport { Freehand } from './type';\nimport {\n  gaussianSmooth,\n  getFillByElement,\n  getStrokeColorByElement,\n} from './utils';\nimport { getStrokeWidthByElement } from '@plait/draw';\n\nexport class FreehandGenerator extends Generator<Freehand> {\n  protected draw(element: Freehand): SVGGElement | undefined {\n    const strokeWidth = getStrokeWidthByElement(element);\n    const strokeColor = getStrokeColorByElement(this.board, element);\n    const fill = getFillByElement(this.board, element);\n    const option: Options = { strokeWidth, stroke: strokeColor, fill, fillStyle: 'solid' };\n    const g = PlaitBoard.getRoughSVG(this.board).curve(\n      gaussianSmooth(element.points, 1, 3),\n      option\n    );\n    setStrokeLinecap(g, 'round');\n    return g;\n  }\n\n  canDraw(element: Freehand): boolean {\n    return true;\n  }\n}\n"
  },
  {
    "path": "packages/drawnix/src/plugins/freehand/smoother.ts",
    "content": "import { distanceBetweenPointAndPoint, Point } from '@plait/core';\n\ninterface StrokePoint {\n  point: Point;\n  pressure?: number;\n  timestamp: number;\n  tiltX?: number;\n  tiltY?: number;\n}\n\nexport interface SmootherOptions {\n  smoothing?: number;\n  velocityWeight?: number;\n  curvatureWeight?: number;\n  minDistance?: number;\n  maxPoints?: number;\n  pressureSensitivity?: number;\n  tiltSensitivity?: number;\n  velocityThreshold?: number;\n  samplingRate?: number;\n}\n\nexport class FreehandSmoother {\n  private readonly defaultOptions: Required<SmootherOptions> = {\n    smoothing: 0.65,\n    velocityWeight: 0.2,\n    curvatureWeight: 0.3,\n    minDistance: 0.2, // 降低最小距离阈值\n    maxPoints: 8,\n    pressureSensitivity: 0.5,\n    tiltSensitivity: 0.3,\n    velocityThreshold: 800,\n    samplingRate: 5, // 降低采样间隔\n  };\n\n  private options: Required<SmootherOptions>;\n  private points: StrokePoint[] = [];\n  private lastProcessedTime = 0;\n  private movingAverageVelocity: number[] = [];\n  private readonly velocityWindowSize = 3;\n\n  constructor(options: SmootherOptions = {}) {\n    this.options = { ...this.defaultOptions, ...options };\n  }\n\n  process(\n    point: Point,\n    data: Partial<Omit<StrokePoint, 'point'>> = {}\n  ): Point | null {\n    const timestamp = data.timestamp ?? Date.now();\n\n    // 第一个点直接返回\n    if (this.points.length === 0) {\n      const strokePoint: StrokePoint = { point, timestamp, ...data };\n      this.points.push(strokePoint);\n      this.lastProcessedTime = timestamp;\n      return point;\n    }\n\n    // 采样率控制 - 确保不会卡住\n    if (timestamp - this.lastProcessedTime < this.options.samplingRate) {\n      const timeDiff = timestamp - this.lastProcessedTime;\n      if (timeDiff < 2) {\n        // 如果时间间隔太小，跳过\n        return null;\n      }\n    }\n\n    const strokePoint: StrokePoint = {\n      point,\n      timestamp,\n      ...data,\n    };\n\n    // 距离检查 - 添加最小距离的动态调整\n    const distanceOk = this.checkDistance(point);\n    if (!distanceOk && this.points.length > 1) {\n      // 如果距离太近，但时间间隔较大，仍然处理该点\n      const timeDiff = timestamp - this.lastProcessedTime;\n      if (timeDiff < 32) {\n        // 32ms ≈ 30fps\n        return null;\n      }\n    }\n\n    // 更新历史点\n    this.updatePoints(strokePoint);\n\n    // 计算动态参数\n    const dynamicParams = this.calculateDynamicParameters(strokePoint);\n\n    // 应用平滑\n    const smoothedPoint = this.smooth(point, dynamicParams);\n\n    this.lastProcessedTime = timestamp;\n    return smoothedPoint;\n  }\n\n  reset(): void {\n    this.points = [];\n    this.lastProcessedTime = 0;\n    this.movingAverageVelocity = [];\n  }\n\n  private updatePoints(point: StrokePoint): void {\n    this.points.push(point);\n    if (this.points.length > this.options.maxPoints) {\n      this.points.shift();\n    }\n  }\n\n  private checkDistance(point: Point): boolean {\n    if (this.points.length === 0) return true;\n\n    const lastPoint = this.points[this.points.length - 1].point;\n    const distance = this.getDistance(lastPoint, point);\n\n    // 动态最小距离：根据当前速度调整\n    let minDistance = this.options.minDistance;\n    if (this.movingAverageVelocity.length > 0) {\n      const avgVelocity = this.getAverageVelocity();\n      minDistance *= Math.max(0.5, Math.min(1.5, avgVelocity / 200));\n    }\n\n    return distance >= minDistance;\n  }\n\n  private calculateDynamicParameters(strokePoint: StrokePoint) {\n    const velocity = this.calculateVelocity(strokePoint);\n    this.updateMovingAverage(velocity);\n    const avgVelocity = this.getAverageVelocity();\n\n    const params = { ...this.options };\n\n    // 压力适应 - 更温和的压力响应\n    if (strokePoint.pressure !== undefined) {\n      const pressureWeight = Math.pow(strokePoint.pressure, 1.2);\n      params.smoothing *= 1 - pressureWeight * params.pressureSensitivity * 0.8;\n    }\n\n    // 速度适应 - 更平滑的过渡\n    const velocityFactor = Math.min(avgVelocity / params.velocityThreshold, 1);\n    params.velocityWeight = 0.2 + velocityFactor * 0.3;\n    params.smoothing *= 1 + velocityFactor * 0.2;\n\n    // 倾斜适应 - 更温和的响应\n    if (strokePoint.tiltX !== undefined && strokePoint.tiltY !== undefined) {\n      const tiltFactor =\n        Math.sqrt(strokePoint.tiltX ** 2 + strokePoint.tiltY ** 2) / 90;\n      params.smoothing *= 1 + tiltFactor * params.tiltSensitivity * 0.7;\n    }\n\n    return params;\n  }\n\n  private smooth(point: Point, params: Required<SmootherOptions>): Point {\n    if (this.points.length < 2) return point;\n\n    const weights = this.calculateWeights(params);\n    const totalWeight = weights.reduce((sum, w) => sum + w, 0);\n\n    if (totalWeight === 0) return point;\n\n    const smoothedPoint: Point = [0, 0];\n    for (let i = 0; i < this.points.length; i++) {\n      const weight = weights[i] / totalWeight;\n      smoothedPoint[0] += this.points[i].point[0] * weight;\n      smoothedPoint[1] += this.points[i].point[1] * weight;\n    }\n\n    return smoothedPoint;\n  }\n\n  private calculateWeights(params: Required<SmootherOptions>): number[] {\n    const weights: number[] = [];\n    const lastIndex = this.points.length - 1;\n\n    for (let i = 0; i < this.points.length; i++) {\n      // 基础权重 - 使用更温和的衰减\n      let weight = Math.pow(params.smoothing, (lastIndex - i) * 0.8);\n\n      // 速度权重 - 更平滑的过渡\n      if (i < lastIndex) {\n        const velocity = this.getPointVelocity(i);\n        weight *= 1 + velocity * params.velocityWeight * 0.8;\n      }\n\n      // 曲率权重 - 更温和的影响\n      if (i > 0 && i < lastIndex) {\n        const curvature = this.getPointCurvature(i);\n        weight *= 1 + curvature * params.curvatureWeight * 0.7;\n      }\n\n      weights.push(weight);\n    }\n\n    return weights;\n  }\n\n  // 工具方法保持不变\n  private getDistance(p1: Point, p2: Point): number {\n    return distanceBetweenPointAndPoint(p1[0], p1[1], p2[0], p2[1]);\n  }\n\n  private calculateVelocity(point: StrokePoint): number {\n    if (this.points.length < 2) return 0;\n\n    const prevPoint = this.points[this.points.length - 1];\n    const distance = this.getDistance(prevPoint.point, point.point);\n    const timeDiff = point.timestamp - prevPoint.timestamp;\n    return timeDiff > 0 ? distance / timeDiff : 0;\n  }\n\n  private updateMovingAverage(velocity: number): void {\n    this.movingAverageVelocity.push(velocity);\n    if (this.movingAverageVelocity.length > this.velocityWindowSize) {\n      this.movingAverageVelocity.shift();\n    }\n  }\n\n  private getAverageVelocity(): number {\n    if (this.movingAverageVelocity.length === 0) return 0;\n    return (\n      this.movingAverageVelocity.reduce((a, b) => a + b) /\n      this.movingAverageVelocity.length\n    );\n  }\n\n  private getPointVelocity(index: number): number {\n    if (index >= this.points.length - 1) return 0;\n\n    const p1 = this.points[index];\n    const p2 = this.points[index + 1];\n    const distance = this.getDistance(p1.point, p2.point);\n    const timeDiff = p2.timestamp - p1.timestamp;\n    return timeDiff > 0 ? distance / timeDiff : 0;\n  }\n\n  private getPointCurvature(index: number): number {\n    if (index <= 0 || index >= this.points.length - 1) return 0;\n\n    const p1 = this.points[index - 1].point;\n    const p2 = this.points[index].point;\n    const p3 = this.points[index + 1].point;\n\n    const a = this.getDistance(p1, p2);\n    const b = this.getDistance(p2, p3);\n    const c = this.getDistance(p1, p3);\n\n    const s = (a + b + c) / 2;\n    const area = Math.sqrt(Math.max(0, s * (s - a) * (s - b) * (s - c)));\n    return (4 * area) / (a * b * c + 0.0001); // 避免除零\n  }\n}\n"
  },
  {
    "path": "packages/drawnix/src/plugins/freehand/type.ts",
    "content": "import { DEFAULT_COLOR, Point, ThemeColorMode } from '@plait/core';\nimport { PlaitCustomGeometry } from '@plait/draw';\n\nexport const FreehandThemeColors = {\n  [ThemeColorMode.default]: {\n      strokeColor: DEFAULT_COLOR,\n      fill: 'none'\n  },\n  [ThemeColorMode.colorful]: {\n      strokeColor: '#06ADBF',\n      fill: 'none'\n  },\n  [ThemeColorMode.soft]: {\n      strokeColor: '#6D89C1',\n      fill: 'none'\n  },\n  [ThemeColorMode.retro]: {\n      strokeColor: '#E9C358',\n      fill: 'none'\n  },\n  [ThemeColorMode.dark]: {\n      strokeColor: '#FFFFFF',\n      fill: 'none'\n  },\n  [ThemeColorMode.starry]: {\n      strokeColor: '#42ABE5',\n      fill: 'none'\n  }\n};\n\nexport enum FreehandShape {\n  eraser = 'eraser',\n  nibPen = 'nibPen',\n  feltTipPen = 'feltTipPen',\n  artisticBrush = 'artisticBrush',\n  markerHighlight = 'markerHighlight',\n}\n\nexport const FREEHAND_TYPE = 'freehand';\n\nexport type Freehand = PlaitCustomGeometry<typeof FREEHAND_TYPE, Point[], FreehandShape>\n\nexport const Freehand = {\n  isFreehand: (value: any): value is Freehand => {\n    return value.type === FREEHAND_TYPE;\n  },\n};\n"
  },
  {
    "path": "packages/drawnix/src/plugins/freehand/utils.ts",
    "content": "import {\n  getSelectedElements,\n  idCreator,\n  isPointInPolygon,\n  PlaitBoard,\n  PlaitElement,\n  Point,\n  RectangleClient,\n  rotateAntiPointsByElement,\n  Selection,\n  ThemeColorMode,\n} from '@plait/core';\nimport { Freehand, FreehandShape, FreehandThemeColors } from './type';\nimport {\n  DefaultDrawStyle,\n  isClosedCustomGeometry,\n  isClosedPoints,\n  isHitPolyLine,\n  isRectangleHitRotatedPoints,\n} from '@plait/draw';\n\nexport function getFreehandPointers() {\n  return [FreehandShape.feltTipPen, FreehandShape.eraser];\n}\n\nexport const createFreehandElement = (\n  shape: FreehandShape,\n  points: Point[]\n): Freehand => {\n  const element: Freehand = {\n    id: idCreator(),\n    type: 'freehand',\n    shape,\n    points,\n  };\n  return element;\n};\n\nexport const isHitFreehand = (\n  board: PlaitBoard,\n  element: Freehand,\n  point: Point\n) => {\n  const antiPoint = rotateAntiPointsByElement(board, point, element) || point;\n  const points = element.points;\n  const fill = getFillByElement(board, element);\n  if (isClosedPoints(element.points) && fill && fill !== 'none') {\n    return (\n      isPointInPolygon(antiPoint, points) || isHitPolyLine(points, antiPoint)\n    );\n  } else {\n    return isHitPolyLine(points, antiPoint);\n  }\n};\n\nexport const isRectangleHitFreehand = (\n  board: PlaitBoard,\n  element: Freehand,\n  selection: Selection\n) => {\n  const rangeRectangle = RectangleClient.getRectangleByPoints([\n    selection.anchor,\n    selection.focus,\n  ]);\n  return isRectangleHitRotatedPoints(\n    rangeRectangle,\n    element.points,\n    element.angle\n  );\n};\n\nexport const getSelectedFreehandElements = (board: PlaitBoard) => {\n  return getSelectedElements(board).filter((ele) => Freehand.isFreehand(ele));\n};\n\nexport const getFreehandDefaultStrokeColor = (theme: ThemeColorMode) => {\n  return FreehandThemeColors[theme].strokeColor;\n};\n\nexport const getFreehandDefaultFill = (theme: ThemeColorMode) => {\n  return FreehandThemeColors[theme].fill;\n};\n\nexport const getStrokeColorByElement = (\n  board: PlaitBoard,\n  element: PlaitElement\n) => {\n  const defaultColor = getFreehandDefaultStrokeColor(\n    board.theme.themeColorMode\n  );\n  const strokeColor = element.strokeColor || defaultColor;\n  return strokeColor;\n};\n\nexport const getFillByElement = (board: PlaitBoard, element: PlaitElement) => {\n  const defaultFill =\n    Freehand.isFreehand(element) && isClosedCustomGeometry(board, element)\n      ? getFreehandDefaultFill(board.theme.themeColorMode)\n      : DefaultDrawStyle.fill;\n  const fill = element.fill || defaultFill;\n  return fill;\n};\n\nexport function gaussianWeight(x: number, sigma: number) {\n  return Math.exp(-(x * x) / (2 * sigma * sigma));\n}\n\nexport function gaussianSmooth(\n  points: Point[],\n  sigma: number,\n  windowSize: number\n) {\n  if (points.length < 2) return points;\n\n  const halfWindow = Math.floor(windowSize / 2);\n  const smoothedPoints: Point[] = [];\n\n  // 方法1：端点镜像\n  function getMirroredPoint(idx: number): Point {\n    if (idx < 0) {\n      // 左端镜像\n      const mirrorIdx = -idx - 1;\n      if (mirrorIdx < points.length) {\n        // 以第一个点为中心的对称点\n        return [\n          2 * points[0][0] - points[mirrorIdx][0],\n          2 * points[0][1] - points[mirrorIdx][1],\n        ];\n      }\n    } else if (idx >= points.length) {\n      // 右端镜像\n      const mirrorIdx = 2 * points.length - idx - 1;\n      if (mirrorIdx >= 0) {\n        // 以最后一个点为中心的对称点\n        return [\n          2 * points[points.length - 1][0] - points[mirrorIdx][0],\n          2 * points[points.length - 1][1] - points[mirrorIdx][1],\n        ];\n      }\n    }\n    return points[idx];\n  }\n\n  // 方法2：自适应窗口\n  function getAdaptiveWindow(i: number): number {\n    // 端点处使用较小的窗口\n    const distToEdge = Math.min(i, points.length - 1 - i);\n    return Math.min(halfWindow, distToEdge + Math.floor(halfWindow / 2));\n  }\n\n  for (let i = 0; i < points.length; i++) {\n    let sumX = 0;\n    let sumY = 0;\n    let weightSum = 0;\n\n    // 对端点使用自适应窗口\n    const adaptiveWindow = getAdaptiveWindow(i);\n\n    for (let j = -adaptiveWindow; j <= adaptiveWindow; j++) {\n      const idx = i + j;\n      const point = getMirroredPoint(idx);\n\n      // 端点处使用渐变权重\n      let weight = gaussianWeight(j, sigma);\n\n      // 端点权重调整\n      if (i < halfWindow || i >= points.length - halfWindow) {\n        // 增加端点原始值的权重\n        const edgeFactor = 1 + 0.5 * (1 - Math.abs(j) / adaptiveWindow);\n        weight *= j === 0 ? edgeFactor : 1;\n      }\n\n      sumX += point[0] * weight;\n      sumY += point[1] * weight;\n      weightSum += weight;\n    }\n\n    // 端点处的特殊处理\n    if (i === 0 || i === points.length - 1) {\n      // 保持端点不变\n      smoothedPoints.push([points[i][0], points[i][1]]);\n    } else {\n      // 平滑中间点\n      smoothedPoints.push([sumX / weightSum, sumY / weightSum]);\n    }\n  }\n\n  return smoothedPoints;\n}\n"
  },
  {
    "path": "packages/drawnix/src/plugins/freehand/with-freehand-create.ts",
    "content": "import {\n  PlaitBoard,\n  Point,\n  Transforms,\n  distanceBetweenPointAndPoint,\n  toHostPoint,\n  toViewBoxPoint,\n} from '@plait/core';\nimport { isDrawingMode } from '@plait/common';\nimport { createFreehandElement, getFreehandPointers } from './utils';\nimport { Freehand, FreehandShape } from './type';\nimport { FreehandGenerator } from './freehand.generator';\nimport { FreehandSmoother } from './smoother';\nimport { isTwoFingerMode } from '@plait-board/react-board';\n\nexport const withFreehandCreate = (board: PlaitBoard) => {\n  const { pointerDown, pointerMove, pointerUp, globalPointerUp, touchStart } =\n    board;\n\n  let isDrawing = false;\n\n  let isSnappingStartAndEnd = false;\n\n  let points: Point[] = [];\n\n  let originScreenPoint: Point | null = null;\n\n  const generator = new FreehandGenerator(board);\n\n  const smoother = new FreehandSmoother({\n    smoothing: 0.7,\n    pressureSensitivity: 0.6,\n  });\n\n  let temporaryElement: Freehand | null = null;\n\n  const complete = (cancel?: boolean) => {\n    if (isDrawing) {\n      const pointer = PlaitBoard.getPointer(board) as FreehandShape;\n      if (isSnappingStartAndEnd) {\n        points.push(points[0]);\n      }\n      temporaryElement = createFreehandElement(pointer, points);\n    }\n    if (temporaryElement && !cancel) {\n      Transforms.insertNode(board, temporaryElement, [board.children.length]);\n    }\n    generator?.destroy();\n    temporaryElement = null;\n    isDrawing = false;\n    points = [];\n    smoother.reset();\n  };\n\n  board.touchStart = (event: TouchEvent) => {\n    const freehandPointers = getFreehandPointers();\n    const isFreehandPointer = PlaitBoard.isInPointer(board, freehandPointers);\n    if (isFreehandPointer && isDrawingMode(board)) {\n      return event.preventDefault();\n    }\n    touchStart(event);\n  };\n\n  board.pointerDown = (event: PointerEvent) => {\n    const freehandPointers = getFreehandPointers();\n    const isFreehandPointer = PlaitBoard.isInPointer(board, freehandPointers);\n    if (isFreehandPointer && isDrawingMode(board)) {\n      isDrawing = true;\n      originScreenPoint = [event.x, event.y];\n      const smoothingPoint = smoother.process(originScreenPoint) as Point;\n      const point = toViewBoxPoint(\n        board,\n        toHostPoint(board, smoothingPoint[0], smoothingPoint[1])\n      );\n      points.push(point);\n    }\n    pointerDown(event);\n  };\n\n  board.pointerMove = (event: PointerEvent) => {\n    if (isDrawing && !isTwoFingerMode(board)) {\n      const currentScreenPoint: Point = [event.x, event.y];\n      if (\n        originScreenPoint &&\n        distanceBetweenPointAndPoint(\n          originScreenPoint[0],\n          originScreenPoint[1],\n          currentScreenPoint[0],\n          currentScreenPoint[1]\n        ) < 8\n      ) {\n        isSnappingStartAndEnd = true;\n      } else {\n        isSnappingStartAndEnd = false;\n      }\n      const smoothingPoint = smoother.process(currentScreenPoint);\n      if (smoothingPoint) {\n        generator?.destroy();\n        const newPoint = toViewBoxPoint(\n          board,\n          toHostPoint(board, smoothingPoint[0], smoothingPoint[1])\n        );\n        points.push(newPoint);\n        const pointer = PlaitBoard.getPointer(board) as FreehandShape;\n        temporaryElement = createFreehandElement(pointer, points);\n        generator.processDrawing(\n          temporaryElement,\n          PlaitBoard.getElementTopHost(board)\n        );\n      }\n      return;\n    }\n    if (isTwoFingerMode(board) && isDrawing) {\n      complete(true);\n      return;\n    }\n    pointerMove(event);\n  };\n\n  board.pointerUp = (event: PointerEvent) => {\n    complete();\n    pointerUp(event);\n  };\n\n  board.globalPointerUp = (event: PointerEvent) => {\n    complete(true);\n    globalPointerUp(event);\n  };\n\n  return board;\n};\n"
  },
  {
    "path": "packages/drawnix/src/plugins/freehand/with-freehand-erase.ts",
    "content": "import {\n  PlaitBoard,\n  PlaitElement,\n  Point,\n  throttleRAF,\n  toHostPoint,\n  toViewBoxPoint,\n} from '@plait/core';\nimport { isDrawingMode } from '@plait/common';\nimport { isHitFreehand } from './utils';\nimport { Freehand, FreehandShape } from './type';\nimport { CoreTransforms } from '@plait/core';\nimport { LaserPointer } from '../../utils/laser-pointer';\nimport { isTwoFingerMode } from '@plait-board/react-board';\n\nexport const withFreehandErase = (board: PlaitBoard) => {\n  const { pointerDown, pointerMove, pointerUp, globalPointerUp, touchStart } =\n    board;\n\n  const laserPointer = new LaserPointer();\n\n  let isErasing = false;\n  const elementsToDelete = new Set<string>();\n\n  const checkAndMarkFreehandElementsForDeletion = (point: Point) => {\n    const viewBoxPoint = toViewBoxPoint(\n      board,\n      toHostPoint(board, point[0], point[1])\n    );\n\n    const freehandElements = board.children.filter((element) =>\n      Freehand.isFreehand(element)\n    ) as Freehand[];\n\n    freehandElements.forEach((element) => {\n      if (\n        !elementsToDelete.has(element.id) &&\n        isHitFreehand(board, element, viewBoxPoint)\n      ) {\n        PlaitElement.getElementG(element).style.opacity = '0.2';\n        elementsToDelete.add(element.id);\n      }\n    });\n  };\n\n  const deleteMarkedElements = () => {\n    if (elementsToDelete.size > 0) {\n      const elementsToRemove = board.children.filter((element) =>\n        elementsToDelete.has(element.id)\n      );\n\n      if (elementsToRemove.length > 0) {\n        CoreTransforms.removeElements(board, elementsToRemove);\n      }\n    }\n  };\n\n  const complete = () => {\n    if (isErasing) {\n      deleteMarkedElements();\n      isErasing = false;\n      elementsToDelete.clear();\n      laserPointer.destroy();\n    }\n  };\n\n  board.touchStart = (event: TouchEvent) => {\n    const isEraserPointer = PlaitBoard.isInPointer(board, [\n      FreehandShape.eraser,\n    ]);\n    if (isEraserPointer && isDrawingMode(board)) {\n      return event.preventDefault();\n    }\n    touchStart(event);\n  };\n\n  board.pointerDown = (event: PointerEvent) => {\n    const isEraserPointer = PlaitBoard.isInPointer(board, [\n      FreehandShape.eraser,\n    ]);\n\n    if (isEraserPointer && isDrawingMode(board)) {\n      isErasing = true;\n      elementsToDelete.clear();\n      const currentPoint: Point = [event.x, event.y];\n      checkAndMarkFreehandElementsForDeletion(currentPoint);\n      laserPointer.init(board);\n      return;\n    }\n\n    pointerDown(event);\n  };\n\n  board.pointerMove = (event: PointerEvent) => {\n    if (isErasing && !isTwoFingerMode(board)) {\n      throttleRAF(board, 'with-freehand-erase', () => {\n        const currentPoint: Point = [event.x, event.y];\n        checkAndMarkFreehandElementsForDeletion(currentPoint);\n      });\n      return;\n    }\n    if (isErasing && isTwoFingerMode(board)) {\n      complete();\n      return;\n    }\n    pointerMove(event);\n  };\n\n  board.pointerUp = (event: PointerEvent) => {\n    if (isErasing) {\n      complete();\n      return;\n    }\n\n    pointerUp(event);\n  };\n\n  board.globalPointerUp = (event: PointerEvent) => {\n    if (isErasing) {\n      complete();\n      return;\n    }\n\n    globalPointerUp(event);\n  };\n\n  return board;\n};\n"
  },
  {
    "path": "packages/drawnix/src/plugins/freehand/with-freehand-fragment.ts",
    "content": "import {\n  ClipboardData,\n  PlaitBoard,\n  PlaitElement,\n  Point,\n  RectangleClient,\n  WritableClipboardContext,\n  WritableClipboardOperationType,\n  WritableClipboardType,\n  addOrCreateClipboardContext,\n} from '@plait/core';\nimport { getSelectedFreehandElements } from './utils';\nimport { Freehand } from './type';\nimport { buildClipboardData, insertClipboardData } from '@plait/common';\n\nexport const withFreehandFragment = (baseBoard: PlaitBoard) => {\n  const board = baseBoard as PlaitBoard;\n  const { getDeletedFragment, buildFragment, insertFragment } = board;\n\n  board.getDeletedFragment = (data: PlaitElement[]) => {\n    const freehandElements = getSelectedFreehandElements(board);\n    if (freehandElements.length) {\n      data.push(...freehandElements);\n    }\n    return getDeletedFragment(data);\n  };\n\n  board.buildFragment = (\n    clipboardContext: WritableClipboardContext | null,\n    rectangle: RectangleClient | null,\n    operationType: WritableClipboardOperationType,\n    originData?: PlaitElement[]\n  ) => {\n    const freehandElements = getSelectedFreehandElements(board);\n    if (freehandElements.length) {\n      const elements = buildClipboardData(\n        board,\n        freehandElements,\n        rectangle ? [rectangle.x, rectangle.y] : [0, 0]\n      );\n      clipboardContext = addOrCreateClipboardContext(clipboardContext, {\n        text: '',\n        type: WritableClipboardType.elements,\n        elements,\n      });\n    }\n    return buildFragment(\n      clipboardContext,\n      rectangle,\n      operationType,\n      originData\n    );\n  };\n\n  board.insertFragment = (\n    clipboardData: ClipboardData | null,\n    targetPoint: Point,\n    operationType?: WritableClipboardOperationType\n  ) => {\n    const freehandElements = clipboardData?.elements?.filter((value) =>\n      Freehand.isFreehand(value)\n    ) as Freehand[];\n    if (freehandElements && freehandElements.length > 0) {\n      insertClipboardData(board, freehandElements, targetPoint);\n    }\n    insertFragment(clipboardData, targetPoint, operationType);\n  };\n\n  return board;\n};\n"
  },
  {
    "path": "packages/drawnix/src/plugins/freehand/with-freehand.ts",
    "content": "import {\n  PlaitBoard,\n  PlaitElement,\n  PlaitOptionsBoard,\n  PlaitPluginElementContext,\n  RectangleClient,\n  Selection,\n} from '@plait/core';\nimport { Freehand, FREEHAND_TYPE } from './type';\nimport { FreehandComponent } from './freehand.component';\nimport { withFreehandCreate } from './with-freehand-create';\nimport { isHitFreehand, isRectangleHitFreehand } from './utils';\nimport { withFreehandFragment } from './with-freehand-fragment';\nimport {\n  getHitDrawElement,\n  WithDrawOptions,\n  WithDrawPluginKey,\n} from '@plait/draw';\nimport { withFreehandErase } from './with-freehand-erase';\n\nexport const withFreehand = (board: PlaitBoard) => {\n  const {\n    getRectangle,\n    drawElement,\n    isHit,\n    isRectangleHit,\n    getOneHitElement,\n    isMovable,\n    isAlign,\n  } = board;\n\n  board.drawElement = (context: PlaitPluginElementContext) => {\n    if (Freehand.isFreehand(context.element)) {\n      return FreehandComponent;\n    }\n    return drawElement(context);\n  };\n\n  board.getRectangle = (element: PlaitElement) => {\n    if (Freehand.isFreehand(element)) {\n      return RectangleClient.getRectangleByPoints(element.points);\n    }\n    return getRectangle(element);\n  };\n\n  board.isRectangleHit = (element: PlaitElement, selection: Selection) => {\n    if (Freehand.isFreehand(element)) {\n      return isRectangleHitFreehand(board, element, selection);\n    }\n    return isRectangleHit(element, selection);\n  };\n\n  board.isHit = (element, point, isStrict?: boolean) => {\n    if (Freehand.isFreehand(element)) {\n      return isHitFreehand(board, element, point);\n    }\n    return isHit(element, point, isStrict);\n  };\n\n  board.getOneHitElement = (elements) => {\n    const isAllFreehand = elements.every((item) => Freehand.isFreehand(item));\n    if (isAllFreehand) {\n      return getHitDrawElement(board, elements as Freehand[]);\n    }\n    return getOneHitElement(elements);\n  };\n\n  board.isMovable = (element) => {\n    if (Freehand.isFreehand(element)) {\n      return true;\n    }\n    return isMovable(element);\n  };\n\n  board.isAlign = (element) => {\n    if (Freehand.isFreehand(element)) {\n      return true;\n    }\n    return isAlign(element);\n  };\n\n  (board as PlaitOptionsBoard).setPluginOptions<WithDrawOptions>(\n    WithDrawPluginKey,\n    { customGeometryTypes: [FREEHAND_TYPE] }\n  );\n\n  return withFreehandErase(withFreehandFragment(withFreehandCreate(board)));\n};\n"
  },
  {
    "path": "packages/drawnix/src/plugins/with-common.tsx",
    "content": "import type {\n  ImageProps,\n  PlaitImageBoard,\n  RenderComponentRef,\n} from '@plait/common';\nimport { PlaitBoard, PlaitI18nBoard } from '@plait/core';\nimport { createRoot } from 'react-dom/client';\nimport { Image } from './components/image';\nimport { withImagePlugin } from './with-image';\nimport { DrawI18nKey } from '@plait/draw';\nimport { MindI18nKey } from '@plait/mind';\nimport { i18nInsidePlaitHook } from '../i18n';\nexport const withCommonPlugin = (board: PlaitBoard) => {\n  const newBoard = board as PlaitBoard & PlaitImageBoard & PlaitI18nBoard;\n\n  newBoard.renderImage = (\n    container: Element | DocumentFragment,\n    props: ImageProps\n  ) => {\n    const root = createRoot(container);\n    root.render(<Image {...props}></Image>);\n    let newProps = { ...props };\n    const ref: RenderComponentRef<ImageProps> = {\n      destroy: () => {\n        setTimeout(() => {\n          root.unmount();\n        }, 0);\n      },\n      update: (updatedProps: Partial<ImageProps>) => {\n        newProps = { ...newProps, ...updatedProps };\n        root.render(<Image {...newProps}></Image>);\n      },\n    };\n    return ref;\n  };\n\n  const { t } = i18nInsidePlaitHook();\n\n  newBoard.getI18nValue = (key: string) => {\n    if (key === DrawI18nKey.lineText) {\n      return t('draw.lineText');\n    }\n    if (key === DrawI18nKey.geometryText) {\n      return t(\"draw.geometryText\");\n    }\n    if (key === MindI18nKey.mindCentralText) {\n      return t('mind.centralText');\n    }\n    if (key === MindI18nKey.abstractNodeText) {\n      return t('mind.abstractNodeText');\n    }\n\n    return null;\n  };\n\n  return withImagePlugin(newBoard);\n};\n"
  },
  {
    "path": "packages/drawnix/src/plugins/with-hotkey.ts",
    "content": "import {\n  BoardTransforms,\n  getSelectedElements,\n  PlaitBoard,\n  PlaitPointerType,\n} from '@plait/core';\nimport { isHotkey } from 'is-hotkey';\nimport { addImage, saveAsImage } from '../utils/image';\nimport { saveAsJSON } from '../data/json';\nimport { DrawnixState } from '../hooks/use-drawnix';\nimport { BoardCreationMode, setCreationMode } from '@plait/common';\nimport { MindPointerType } from '@plait/mind';\nimport { FreehandShape } from './freehand/type';\nimport { ArrowLineShape, BasicShapes } from '@plait/draw';\n\nexport const buildDrawnixHotkeyPlugin = (\n  updateAppState: (appState: Partial<DrawnixState>) => void\n) => {\n  const withDrawnixHotkey = (board: PlaitBoard) => {\n    const { globalKeyDown, keyDown } = board;\n    board.globalKeyDown = (event: KeyboardEvent) => {\n      const isTypingNormal =\n        event.target instanceof HTMLInputElement ||\n        event.target instanceof HTMLTextAreaElement;\n      if (\n        !isTypingNormal &&\n        (PlaitBoard.getMovingPointInBoard(board) ||\n          PlaitBoard.isMovingPointInBoard(board)) &&\n        !PlaitBoard.hasBeenTextEditing(board)\n      ) {\n        if (isHotkey(['mod+shift+e'], { byKey: true })(event)) {\n          saveAsImage(board, true);\n          event.preventDefault();\n          return;\n        }\n        if (isHotkey(['mod+s'], { byKey: true })(event)) {\n          saveAsJSON(board);\n          event.preventDefault();\n          return;\n        }\n        if (\n          isHotkey(['mod+backspace'])(event) ||\n          isHotkey(['mod+delete'])(event)\n        ) {\n          updateAppState({\n            openCleanConfirm: true,\n          });\n          event.preventDefault();\n          return;\n        }\n        if (isHotkey(['mod+u'])(event)) {\n          addImage(board);\n          event.preventDefault();\n          return;\n        }\n        if (!event.altKey && !event.metaKey && !event.ctrlKey) {\n          if (event.key === 'h') {\n            BoardTransforms.updatePointerType(board, PlaitPointerType.hand);\n            updateAppState({ pointer: PlaitPointerType.hand });\n            event.preventDefault();\n            return;\n          }\n          if (event.key === 'v') {\n            BoardTransforms.updatePointerType(\n              board,\n              PlaitPointerType.selection\n            );\n            updateAppState({ pointer: PlaitPointerType.selection });\n            event.preventDefault();\n            return;\n          }\n          if (event.key === 'm') {\n            setCreationMode(board, BoardCreationMode.dnd);\n            BoardTransforms.updatePointerType(board, MindPointerType.mind);\n            updateAppState({ pointer: MindPointerType.mind });\n            event.preventDefault();\n            return;\n          }\n          if (event.key === 'e') {\n            setCreationMode(board, BoardCreationMode.drawing);\n            BoardTransforms.updatePointerType(board, FreehandShape.eraser);\n            updateAppState({ pointer: FreehandShape.eraser });\n            event.preventDefault();\n            return;\n          }\n          if (event.key === 'p') {\n            setCreationMode(board, BoardCreationMode.drawing);\n            BoardTransforms.updatePointerType(board, FreehandShape.feltTipPen);\n            updateAppState({ pointer: FreehandShape.feltTipPen });\n            event.preventDefault();\n            return;\n          }\n          if (event.key === 'a' && !isHotkey(['mod+a'])(event)) {\n            // will trigger editing text\n            if (getSelectedElements(board).length === 0) {\n              setCreationMode(board, BoardCreationMode.drawing);\n              BoardTransforms.updatePointerType(board, ArrowLineShape.straight);\n              updateAppState({ pointer: ArrowLineShape.straight });\n              event.preventDefault();\n              return;\n            }\n          }\n          if (event.key === 'r' || event.key === 'o' || event.key === 't') {\n            const keyToPointer = {\n              r: BasicShapes.rectangle,\n              o: BasicShapes.ellipse,\n              t: BasicShapes.text,\n            };\n            if (keyToPointer[event.key] === BasicShapes.text) {\n              setCreationMode(board, BoardCreationMode.dnd);\n            } else {\n              setCreationMode(board, BoardCreationMode.drawing);\n            }\n            BoardTransforms.updatePointerType(board, keyToPointer[event.key]);\n            updateAppState({ pointer: keyToPointer[event.key] });\n            event.preventDefault();\n            return;\n          }\n        }\n      }\n      globalKeyDown(event);\n    };\n\n    board.keyDown = (event: KeyboardEvent) => {\n      if (isHotkey(['mod+z'], { byKey: true })(event)) {\n        board.undo();\n        event.preventDefault();\n        return;\n      }\n\n      if (isHotkey(['mod+shift+z'], { byKey: true })(event)) {\n        board.redo();\n        event.preventDefault();\n        return;\n      }\n\n      keyDown(event);\n    };\n\n    return board;\n  };\n  return withDrawnixHotkey;\n};\n"
  },
  {
    "path": "packages/drawnix/src/plugins/with-image.tsx",
    "content": "import {\n  getElementOfFocusedImage,\n  isResizing,\n  type PlaitImageBoard,\n} from '@plait/common';\nimport {\n  ClipboardData,\n  getHitElementByPoint,\n  isDragging,\n  isSelectionMoving,\n  PlaitBoard,\n  Point,\n  toHostPoint,\n  toViewBoxPoint,\n  WritableClipboardOperationType,\n} from '@plait/core';\nimport { isSupportedImageFileType } from '../data/blob';\nimport { insertImage } from '../data/image';\nimport { isHitImage, MindElement, ImageData } from '@plait/mind';\nimport { ImageViewer } from '../libs/image-viewer';\n\nexport const withImagePlugin = (board: PlaitBoard) => {\n  const newBoard = board as PlaitBoard & PlaitImageBoard;\n  const { insertFragment, drop, pointerUp } = newBoard;\n  const viewer = new ImageViewer({\n    zoomStep: 0.3,\n    minZoom: 0.1,\n    maxZoom: 5,\n    enableKeyboard: true,\n  });\n\n  newBoard.insertFragment = (\n    clipboardData: ClipboardData | null,\n    targetPoint: Point,\n    operationType?: WritableClipboardOperationType\n  ) => {\n    if (\n      clipboardData?.files?.length &&\n      isSupportedImageFileType(clipboardData.files[0].type)\n    ) {\n      const imageFile = clipboardData.files[0];\n      insertImage(board, imageFile, targetPoint, false);\n      return;\n    }\n    insertFragment(clipboardData, targetPoint, operationType);\n  };\n\n  newBoard.drop = (event: DragEvent) => {\n    if (event.dataTransfer?.files?.length) {\n      const imageFile = event.dataTransfer.files[0];\n      if (isSupportedImageFileType(imageFile.type)) {\n        const point = toViewBoxPoint(\n          board,\n          toHostPoint(board, event.x, event.y)\n        );\n        insertImage(board, imageFile, point, true);\n        return true;\n      }\n    }\n    return drop(event);\n  };\n\n  newBoard.pointerUp = (event: PointerEvent) => {\n    const focusMindNode = getElementOfFocusedImage(board);\n    if (\n      focusMindNode &&\n      !isResizing(board) &&\n      !isSelectionMoving(board) &&\n      !isDragging(board)\n    ) {\n      const point = toViewBoxPoint(board, toHostPoint(board, event.x, event.y));\n      const hitElement = getHitElementByPoint(board, point);\n      const isHittingImage =\n        hitElement &&\n        MindElement.isMindElement(board, hitElement) &&\n        MindElement.hasImage(hitElement) &&\n        isHitImage(board, hitElement as MindElement<ImageData>, point);\n      if (isHittingImage && focusMindNode === hitElement) {\n        viewer.open(hitElement.data.image.url);\n      }\n    }\n    pointerUp(event);\n  };\n\n  return newBoard;\n};\n"
  },
  {
    "path": "packages/drawnix/src/plugins/with-mind-extend.tsx",
    "content": "import type { PlaitBoard, PlaitOptionsBoard } from '@plait/core';\nimport {\n  WithMindPluginKey,\n  type EmojiProps,\n  type PlaitMindBoard,\n  type PlaitMindEmojiBoard,\n  type WithMindOptions,\n} from '@plait/mind';\nimport { createRoot } from 'react-dom/client';\nimport { Emoji } from './components/emoji';\nimport type { RenderComponentRef } from '@plait/common';\n\nexport const withMindExtend = (board: PlaitBoard) => {\n  const newBoard = board as PlaitBoard & PlaitMindBoard & PlaitMindEmojiBoard;\n\n  (board as PlaitOptionsBoard).setPluginOptions<WithMindOptions>(\n    WithMindPluginKey,\n    {\n      emojiPadding: 0,\n      spaceBetweenEmojis: 4,\n    }\n  );\n\n  newBoard.renderEmoji = (\n    container: Element | DocumentFragment,\n    props: EmojiProps\n  ) => {\n    const emojiContainer = document.createElement('span');\n    container.appendChild(emojiContainer);\n    const root = createRoot(emojiContainer);\n    root.render(<Emoji {...props}></Emoji>);\n    let newProps = { ...props };\n    const ref: RenderComponentRef<EmojiProps> = {\n      destroy: () => {\n        setTimeout(() => {\n          root.unmount();\n        }, 0);\n      },\n      update: (updatedProps: Partial<EmojiProps>) => {\n        newProps = { ...newProps, ...updatedProps };\n        root.render(<Emoji {...newProps}></Emoji>);\n      },\n    };\n    return ref;\n  };\n\n  return newBoard;\n};\n"
  },
  {
    "path": "packages/drawnix/src/plugins/with-pencil.ts",
    "content": "import { isPencilEvent, PlaitBoard } from '@plait/core';\nimport { DrawnixState } from '../hooks/use-drawnix';\n\nconst IS_PENCIL_MODE = new WeakMap<PlaitBoard, boolean>();\n\nexport const isPencilMode = (board: PlaitBoard) => {\n  return !!IS_PENCIL_MODE.get(board);\n};\n\nexport const setIsPencilMode = (board: PlaitBoard, isPencilMode: boolean) => {\n  IS_PENCIL_MODE.set(board, isPencilMode);\n};\n\nexport const buildPencilPlugin = (\n  updateAppState: (appState: Partial<DrawnixState>) => void\n) => {\n  const withPencil = (board: PlaitBoard) => {\n    const { pointerDown } = board;\n\n    board.pointerDown = (event: PointerEvent) => {\n      if (isPencilEvent(event) && !isPencilMode(board)) {\n        setIsPencilMode(board, true);\n        updateAppState({ isPencilMode: true });\n      }\n      if (isPencilMode(board) && !isPencilEvent(event)) {\n        return;\n      }\n      pointerDown(event);\n    };\n\n    return board;\n  };\n  return withPencil;\n};\n"
  },
  {
    "path": "packages/drawnix/src/plugins/with-text-link.tsx",
    "content": "import {\n  isMovingElements,\n  PlaitBoard,\n  PlaitPointerType,\n  throttleRAF,\n} from '@plait/core';\nimport { DrawnixBoard, DrawnixState } from '../hooks/use-drawnix';\nimport { ReactEditor } from 'slate-react';\nimport { Editor } from 'slate';\nimport { isResizing, LinkElement } from '@plait/common';\n\nexport const isHovering = (board: PlaitBoard) => {\n  const { appState } = board as DrawnixBoard;\n  const isHovering =\n    appState && appState.linkState && appState.linkState.isHovering;\n  return isHovering;\n};\n\nexport const isHoveringOrigin = (board: PlaitBoard) => {\n  const { appState } = board as DrawnixBoard;\n  const isHoveringOrigin =\n    appState && appState.linkState && appState.linkState.isHoveringOrigin;\n  return isHoveringOrigin;\n};\n\nexport const isEditing = (board: PlaitBoard) => {\n  const { appState } = board as DrawnixBoard;\n  const isEditing =\n    appState && appState.linkState && appState.linkState.isEditing;\n  return isEditing;\n};\n\nexport const buildTextLinkPlugin = (\n  updateAppState: (appState: Partial<DrawnixState>) => void\n) => {\n  const withTextLink = (board: PlaitBoard) => {\n    const { pointerMove } = board;\n\n    let target: HTMLElement | null = null;\n\n    let timeoutId: any | null = null;\n\n    board.pointerMove = (event: PointerEvent) => {\n      if (\n        (PlaitBoard.isPointer(board, PlaitPointerType.selection) ||\n          PlaitBoard.isPointer(board, PlaitPointerType.hand)) &&\n        !isMovingElements(board) &&\n        !isResizing(board) &&\n        !isHovering(board) &&\n        !isEditing(board)\n      ) {\n        throttleRAF(board, 'with-text-link', () => {\n          const textLinkDom = (event.target as HTMLElement).closest(\n            '.plait-board-link'\n          ) as HTMLElement | null;\n          if (textLinkDom && textLinkDom !== target) {\n            const editable = textLinkDom.closest(\n              '.plait-text-container'\n            ) as HTMLElement;\n            const editor = ReactEditor.toSlateNode(\n              undefined as unknown as Editor,\n              editable\n            ) as Editor;\n            const node = ReactEditor.toSlateNode(\n              undefined as unknown as Editor,\n              textLinkDom\n            ) as LinkElement;\n            target = textLinkDom;\n            updateAppState({\n              linkState: {\n                targetDom: textLinkDom,\n                targetElement: node,\n                editor,\n                isEditing: false,\n                isHovering: false,\n                isHoveringOrigin: true,\n              },\n            });\n            clearTimeout(timeoutId);\n          } else {\n            if (!textLinkDom && target) {\n              timeoutId = setTimeout(() => {\n                if (!isHovering(board) && !isEditing(board)) {\n                  updateAppState({\n                    linkState: null,\n                  });\n                }\n              }, 300);\n              target = null;\n            }\n          }\n        });\n      }\n      pointerMove(event);\n    };\n\n    return board;\n  };\n  return withTextLink;\n};\n"
  },
  {
    "path": "packages/drawnix/src/styles/index.scss",
    "content": "@import './../../../../node_modules/@plait/draw/styles/styles.scss';\n@import './../../../../node_modules/@plait/mind/styles/styles.scss';\n@import './theme.scss';\n\n.drawnix {\n    height: 100%;\n    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Noto Sans', 'Noto Sans CJK SC', 'Microsoft Yahei', 'Hiragino Sans GB', Arial, sans-serif;\n    .pencil-mode-toolbar {\n        position: absolute;\n        top: 82px;\n        left: 0;\n        .tool-icon__icon {\n            width: auto;\n            padding: 0 8px;\n            background-color: var(--color-surface-mid);\n        }\n    }\n    .draw-toolbar {\n        cursor: default;\n        position: absolute;\n        top: 36px;\n        left: 50%;\n        transform: translateX(-50%);\n        @include isMobile {\n            top: 20px;\n        }\n    }\n    .zoom-toolbar {\n        cursor: default;\n        position: absolute;\n        top: 36px;\n        right: 36px;\n        @include isMobile {\n            display: none;\n        }\n        .zoom-out-button {\n            border-top-right-radius: 0 !important;\n            border-bottom-right-radius: 0 !important;\n        }\n        .zoom-menu-trigger {\n            width: 56px;;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            color: var(--color-on-surface);\n            border-radius: var(--border-radius-sm);;\n            cursor: pointer;\n\n            &:hover, &.active {\n                --background: var(--color-surface-primary-container);\n                background-color: var(--background);\n            }\n        }\n        .zoom-in-button{\n            color: var(--color-on-surface);\n            border-top-left-radius: 0 !important;\n            border-bottom-left-radius: 0 !important;\n        }\n    }\n    .app-toolbar {\n        position: absolute;\n        top: 36px;\n        left: 36px;\n        @include isMobile {\n            bottom: 20px;\n            top: auto;\n            width: 86%;\n            left: 50%;\n            transform: translateX(-50%);\n        }\n        .stack {\n            @include isMobile {\n                display: flex;\n                align-items: center;\n                justify-content: space-between;\n            }\n        }\n    }\n    .theme-toolbar {\n        position: absolute;\n        bottom: 36px;\n        right: 36px;\n        @include isMobile {\n            display: none;\n        }\n        select {\n            width: 100px;\n            background-color: var(--color-surface-secondary-container);\n            color: var(--color-on-surface);\n            border-radius: var(--border-radius-sm);\n            padding: 4px 8px;\n            cursor: pointer;\n            border: none;\n            outline: none;\n            font-size: 14px;\n            &:hover {\n                background-color: var(--color-surface-primary-container);\n            }\n        }\n    }\n    .drawnix-link, a {\n        text-decoration: none;\n        color: var(--link-color);\n        user-select: none;\n        cursor: pointer;\n\n        &:hover {\n        text-decoration: underline;\n        }\n        &:active {\n        text-decoration: none;\n        }\n    }\n    .a {\n        font-weight: 500;\n        text-decoration: none;\n        color: var(--link-color);\n        -webkit-user-select: none;\n        user-select: none;\n        cursor: pointer;\n    }\n    textarea {\n        outline: none;\n        &:hover, &:focus {\n            border: 1px solid var(--color-primary);\n        }\n    }\n    .drawnix-button {\n        @include outlineButtonStyles;\n    }\n\n    [plait-mindmap=\"true\"] {\n        img.image-origin--focus:hover {\n            cursor: zoom-in;\n        }\n    }\n\n    .laser-pointer {\n        background: transparent;\n        position: fixed;\n        left: 0;\n        top: 0;\n        z-index: 2022;\n        width: 100vw;\n        height: 100vh;\n        &.mouse-course-hidden {\n            pointer-events: none;\n        }\n    }\n}\n\n.plait-board-container {\n    &.pointer-eraser {\n        .board-host-svg {\n            cursor: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMTAiIGN5PSIxMCIgcj0iNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNjY2IiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8L3N2Zz4=') 10 10, crosshair !important;\n        } \n    }\n    .slate-editable-container {\n        cursor: inherit !important;\n    }\n}\n\n"
  },
  {
    "path": "packages/drawnix/src/styles/theme.scss",
    "content": "@import \"open-color/open-color.scss\";\n@import \"./variables.module.scss\";\n\n.drawnix {\n  --focus-highlight-color: #{$oc-blue-2};\n  --icon-fill-color: var(--color-on-surface);\n  --island-bg-color: #ffffff;\n  --island-border-color: #eeeeee;\n  --keybinding-color: var(--color-gray-40);\n  --shadow-island: 0 0 16px #00000014;\n  --dialog-border-color: var(--color-gray-20);\n  --button-hover-bg: var(--color-surface-high);\n  --button-active-border: var(--color-brand-active);\n  --link-color: var(--color-primary);\n  --default-button-size: 2rem;\n  --default-icon-size: 1rem;\n  --lg-button-size: 2.25rem;\n  --lg-icon-size: 1.125rem;\n  --xlg-icon-size: 1.25rem;\n  --popup-label-size: 1.25rem;\n  --editor-container-padding: 1rem;\n\n  @media screen and (min-device-width: 1921px) {\n    --lg-button-size: 2.5rem;\n    --lg-icon-size: 1.25rem;\n    --default-button-size: 2.25rem;\n    --default-icon-size: 1.25rem;\n  }\n\n  --space-factor: 0.25rem;\n  --dx-space-1: 4px;\n  --dx-space-2: 8px;\n  --dx-space-3: 12px;\n  --dx-space-4: 16px;\n  --dx-space-5: 24px;\n  --dx-radius-1: 3px;\n  --dx-radius-2: 4px;\n  --dx-radius-3: 6px;\n  --dx-radius-4: 8px;\n  --dx-radius-full: 9999px;\n  --text-primary-color: var(--color-on-surface);\n\n  --color-icon-white: #{$oc-white};\n\n  --color-primary: #6698ff;\n  --color-primary-darker: #4a7ee6;    // 降低亮度和饱和度\n  --color-primary-darkest: #3366cc;   // 进一步降低亮度\n  --color-primary-light: #e6f0ff;     // 提高亮度，降低饱和度\n  --color-primary-light-darker: #cce0ff;  // light 的稍暗版本\n  --color-primary-hover: #80acff;     // 略微提高亮度的互动状态\n\n  --button-hover-bg: var(--color-surface-high);\n  --button-active-bg: var(--color-surface-high);\n  --button-active-border: var(--color-brand-active);\n  --default-border-color: var(--color-surface-high);\n\n  --color-gray-10: #f5f5f5;\n  --color-gray-20: #ebebeb;\n  --color-gray-30: #d6d6d6;\n  --color-gray-40: #b8b8b8;\n  --color-gray-50: #999999;\n  --color-gray-60: #7a7a7a;\n  --color-gray-70: #5c5c5c;\n  --color-gray-80: #3d3d3d;\n  --color-gray-85: #242424;\n  --color-gray-90: #1e1e1e;\n  --color-gray-100: #121212;\n\n  --color-disabled: var(--color-gray-40);\n\n  --color-promo: var(--color-primary);\n  --color-success: #268029;\n  --color-success-lighter: #cafccc;\n\n  --border-radius-sm: 0.25rem;\n  --border-radius-md: 0.375rem;\n  --border-radius-lg: 0.5rem;\n  --color-surface-high: hsl(220, 100%, 97%);\n  --color-surface-mid: hsl(220 25% 96%);\n  --color-surface-low: hsl(220 25% 94%);\n  --color-surface-lowest: #ffffff;\n  --color-on-surface: #666666;\n  --color-brand-hover: #6698ff;\n  --color-on-primary-container: #6698ff;\n  --color-surface-primary-container: rgba(102, 152, 255, .1);\n  --color-brand-active: #6698ff;\n  --color-border-outline: #767680;\n  --color-border-outline-variant: #c5c5d0;\n\n  --default-border-color: var(--color-surface-high);\n}\n"
  },
  {
    "path": "packages/drawnix/src/styles/variables.module.scss",
    "content": "@import \"open-color/open-color.scss\";\n\n@mixin isMobile() {\n  @at-root .drawnix--mobile#{&} {\n    @content;\n  }\n}\n\n@mixin toolbarButtonColorStates {\n  &.fillable {\n    .tool-icon_type_radio,\n    .tool-icon_type_checkbox {\n      &:checked + .tool-icon__icon {\n        --icon-fill-color: var(--color-on-primary-container);\n\n        svg {\n          fill: var(--icon-fill-color);\n        }\n      }\n    }\n  }\n\n  .tool-icon_type_radio,\n  .tool-icon_type_checkbox {\n    &:checked + .tool-icon__icon {\n      background: var(--color-surface-primary-container);\n      --keybinding-color: var(--color-on-primary-container);\n\n      svg {\n        color: var(--color-on-primary-container);\n      }\n    }\n  }\n\n  .tool-icon__keybinding {\n    bottom: 4px;\n    right: 4px;\n  }\n\n  .tool-icon__icon {\n    &:hover {\n      background-color: var(--color-surface-primary-container);\n      color: var(--color-primary);\n    }\n\n    &:active {\n      background-color: var(--color-surface-primary-container);\n      border: 1px solid var(--button-active-border);\n\n      svg {\n        color: var(--color-on-primary-container);\n      }\n    }\n\n    &[aria-disabled=\"true\"] {\n      background: initial;\n      border: none;\n\n      svg {\n        color: var(--color-disabled);\n      }\n    }\n  }\n}\n\n@mixin outlineButtonStyles {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  padding: 0.625rem;\n  width: var(--button-width, var(--default-button-size));\n  height: var(--button-height, var(--default-button-size));\n  box-sizing: border-box;\n  border: none;\n  border-style: none;\n  border-color: var(--button-border, var(--default-border-color));\n  border-radius: var(--border-radius-lg);\n  cursor: pointer;\n  background-color: var(--button-bg, var(--island-bg-color));\n  color: var(--icon-fill-color);\n  font-family: var(--ui-font);\n\n  svg {\n    width: var(--button-width, var(--lg-icon-size));\n    height: var(--button-height, var(--lg-icon-size));\n  }\n\n  &:hover {\n    background-color: var(--button-hover-bg, var(--island-bg-color));\n    border-color: var(\n      --button-hover-border,\n      var(--button-border, var(--default-border-color))\n    );\n  }\n\n  &:active {\n    background-color: var(--button-active-bg, var(--island-bg-color));\n    border-color: var(--button-active-border, var(--color-primary-darkest));\n  }\n\n  &.active {\n    background-color: var(\n      --button-selected-bg,\n      var(--color-surface-primary-container)\n    );\n    border-color: var(\n      --button-selected-border,\n      var(--color-surface-primary-container)\n    );\n\n    &:hover {\n      background-color: var(\n        --button-selected-hover-bg,\n        var(--color-surface-primary-container)\n      );\n    }\n\n    svg {\n      color: var(--button-color, var(--color-on-primary-container));\n    }\n  }\n}\n\n$theme-filter: \"invert(93%) hue-rotate(180deg)\";\n$right-sidebar-width: \"302px\";\n\n:export {\n  themeFilter: unquote($theme-filter);\n  rightSidebarWidth: unquote($right-sidebar-width);\n}\n"
  },
  {
    "path": "packages/drawnix/src/transforms/property.ts",
    "content": "import { PropertyTransforms } from '@plait/common';\nimport {\n  isNullOrUndefined,\n  Path,\n  PlaitBoard,\n  PlaitElement,\n  Transforms,\n} from '@plait/core';\nimport { getMemorizeKey } from '@plait/draw';\nimport {\n  applyOpacityToHex,\n  hexAlphaToOpacity,\n  isFullyOpaque,\n  isNoColor,\n  isValidColor,\n  removeHexAlpha,\n} from '../utils/color';\nimport {\n  getCurrentFill,\n  getCurrentStrokeColor,\n  isClosedElement,\n} from '../utils/property';\nimport { DEFAULT_FONT_SIZE, TextTransforms } from '@plait/text-plugins';\n\nexport const setFillColorOpacity = (board: PlaitBoard, fillOpacity: number) => {\n  PropertyTransforms.setFillColor(board, null, {\n    getMemorizeKey,\n    callback: (element: PlaitElement, path: Path) => {\n      if (!isClosedElement(board, element)) {\n        return;\n      }\n      const currentFill = getCurrentFill(board, element);\n      if (!isValidColor(currentFill)) {\n        return;\n      }\n      const currentFillColor = removeHexAlpha(currentFill);\n      const newFill = isFullyOpaque(fillOpacity)\n        ? currentFillColor\n        : applyOpacityToHex(currentFillColor, fillOpacity);\n      Transforms.setNode(board, { fill: newFill }, path);\n    },\n  });\n};\n\nexport const setFillColor = (board: PlaitBoard, fillColor: string) => {\n  PropertyTransforms.setFillColor(board, null, {\n    getMemorizeKey,\n    callback: (element: PlaitElement, path: Path) => {\n      if (!isClosedElement(board, element)) {\n        return;\n      }\n      const currentFill = getCurrentFill(board, element);\n      const currentOpacity = hexAlphaToOpacity(currentFill);\n      if (isNoColor(fillColor)) {\n        Transforms.setNode(board, { fill: null }, path);\n      } else {\n        if (\n          isNullOrUndefined(currentOpacity) ||\n          isFullyOpaque(currentOpacity)\n        ) {\n          Transforms.setNode(board, { fill: fillColor }, path);\n        } else {\n          Transforms.setNode(\n            board,\n            { fill: applyOpacityToHex(fillColor, currentOpacity) },\n            path\n          );\n        }\n      }\n    },\n  });\n};\n\nexport const setStrokeColorOpacity = (\n  board: PlaitBoard,\n  fillOpacity: number\n) => {\n  PropertyTransforms.setStrokeColor(board, null, {\n    getMemorizeKey,\n    callback: (element: PlaitElement, path: Path) => {\n      const currentStrokeColor = getCurrentStrokeColor(board, element);\n      const currentStrokeColorValue = removeHexAlpha(currentStrokeColor);\n      const newStrokeColor = isFullyOpaque(fillOpacity)\n        ? currentStrokeColorValue\n        : applyOpacityToHex(currentStrokeColorValue, fillOpacity);\n      Transforms.setNode(board, { strokeColor: newStrokeColor }, path);\n    },\n  });\n};\n\nexport const setStrokeColor = (board: PlaitBoard, newColor: string) => {\n  PropertyTransforms.setStrokeColor(board, null, {\n    getMemorizeKey,\n    callback: (element: PlaitElement, path: Path) => {\n      const currentStrokeColor = getCurrentStrokeColor(board, element);\n      const currentOpacity = hexAlphaToOpacity(currentStrokeColor);\n      if (isNoColor(newColor)) {\n        Transforms.setNode(board, { strokeColor: null }, path);\n      } else {\n        if (\n          isNullOrUndefined(currentOpacity) ||\n          isFullyOpaque(currentOpacity)\n        ) {\n          Transforms.setNode(board, { strokeColor: newColor }, path);\n        } else {\n          Transforms.setNode(\n            board,\n            { strokeColor: applyOpacityToHex(newColor, currentOpacity) },\n            path\n          );\n        }\n      }\n    },\n  });\n};\n\nexport const setTextColor = (\n  board: PlaitBoard,\n  currentColor: string,\n  newColor: string\n) => {\n  const currentOpacity = hexAlphaToOpacity(currentColor);\n  if (isNoColor(newColor)) {\n    TextTransforms.setTextColor(board, null);\n  } else {\n    TextTransforms.setTextColor(\n      board,\n      applyOpacityToHex(newColor, currentOpacity)\n    );\n  }\n};\n\nexport const setTextColorOpacity = (\n  board: PlaitBoard,\n  currentColor: string,\n  opacity: number\n) => {\n  const currentFontColorValue = removeHexAlpha(currentColor);\n  const newFontColor = isFullyOpaque(opacity)\n    ? currentFontColorValue\n    : applyOpacityToHex(currentFontColorValue, opacity);\n  TextTransforms.setTextColor(board, newFontColor);\n};\n\nexport const setTextFontSize = (board: PlaitBoard, size: number) => {\n  if (!Number.isFinite(size) || size <= 0) {\n    return;\n  }\n  TextTransforms.setFontSize(board, String(size) as any, DEFAULT_FONT_SIZE);\n};\n"
  },
  {
    "path": "packages/drawnix/src/types.ts",
    "content": "export type EventPointerType = 'mouse' | 'pen' | 'touch';\n\nexport type DataURL = string & { _brand: 'DataURL' };\n"
  },
  {
    "path": "packages/drawnix/src/utils/color.ts",
    "content": "import { DEFAULT_COLOR, PlaitBoard } from '@plait/core';\nimport { TRANSPARENT, NO_COLOR, WHITE } from '../constants/color';\n\n// 将 0-100 的透明度转换为 0-255 的整数\nfunction transparencyToAlpha255(transparency: number) {\n  return Math.round(((100 - transparency) / 100) * 255);\n}\n\n// 将 0-255 的 alpha 值转换为 0-100 的透明度\nfunction alpha255ToTransparency(alpha255: number) {\n  return Math.round((1 - alpha255 / 255) * 100);\n}\n\nexport function applyOpacityToHex(hexColor: string, opacity: number) {\n  const alpha = transparencyToAlpha255(100 - opacity);\n  const alphaHex = alpha.toString(16).padStart(2, '0');\n  return `${hexColor}${alphaHex}`;\n}\n\nexport function hexAlphaToOpacity(hexColor: string) {\n  // 移除可能存在的 # 前缀\n  hexColor = hexColor.replace(/^#/, '');\n\n  let alpha;\n  if (hexColor.length === 8) {\n    // 8位十六进制，提取最后两位作为 alpha 值\n    alpha = parseInt(hexColor.slice(6, 8), 16);\n  } else if (hexColor.length === 4) {\n    // 4位十六进制（简写形式），提取最后一位并重复\n    alpha = parseInt(hexColor.slice(3, 4).repeat(2), 16);\n  } else {\n    // 如果没有 alpha 通道，则认为是完全不透明\n    return 100;\n  }\n\n  return 100 - alpha255ToTransparency(alpha);\n}\n\nexport function isValidColor(color: string) {\n  if (color === 'none') {\n    return false;\n  }\n  return true;\n}\n\nexport function removeHexAlpha(hexColor: string) {\n  // 移除可能存在的 # 前缀，并转换为大写\n  const hexColorClone = hexColor.replace(/^#/, '').toUpperCase();\n\n  if (hexColorClone.length === 8) {\n    // 8位十六进制，移除最后两位\n    return '#' + hexColorClone.slice(0, 6);\n  } else if (hexColorClone.length === 4) {\n    // 4位十六进制（简写形式），移除最后一位\n    return '#' + hexColorClone.slice(0, 3);\n  } else if (hexColorClone.length === 6 || hexColorClone.length === 3) {\n    // 已经是标准的 6 位或 3 位形式，直接返回\n    return '#' + hexColorClone;\n  } else {\n    return hexColor;\n  }\n}\n\nexport function isTransparent(color?: string) {\n  return color === TRANSPARENT;\n}\n\nexport function isWhite(color?: string) {\n  return color === WHITE || color === WHITE.toLocaleLowerCase();\n}\n\nexport function isFullyTransparent(opacity: number) {\n  return opacity === 0;\n}\n\nexport function isFullyOpaque(opacity: number) {\n  return opacity === 100;\n}\n\nexport function isNoColor(value: string) {\n  return value === NO_COLOR;\n}\n\nexport function isDefaultStroke(color?: string) {\n  return !color || color === DEFAULT_COLOR;\n}\n\nexport function getBackgroundColor(board: PlaitBoard) {\n  const themeColors = PlaitBoard.getThemeColors(board);\n  const themeColor = themeColors.find(\n    (val) => val.mode === board.theme.themeColorMode\n  );\n  return themeColor?.boardBackground;\n}\n"
  },
  {
    "path": "packages/drawnix/src/utils/common.ts",
    "content": "import { IS_APPLE, IS_MAC, PlaitBoard, toImage, ToImageOptions } from '@plait/core';\nimport type { ResolutionType } from './utility-types';\n\nexport const isPromiseLike = (\n  value: any\n): value is Promise<ResolutionType<typeof value>> => {\n  return (\n    !!value &&\n    typeof value === 'object' &&\n    'then' in value &&\n    'catch' in value &&\n    'finally' in value\n  );\n};\n\n// taken from Radix UI\n// https://github.com/radix-ui/primitives/blob/main/packages/core/primitive/src/primitive.tsx\nexport const composeEventHandlers = <E>(\n  originalEventHandler?: (event: E) => void,\n  ourEventHandler?: (event: E) => void,\n  { checkForDefaultPrevented = true } = {}\n) => {\n  return function handleEvent(event: E) {\n    originalEventHandler?.(event);\n\n    if (\n      !checkForDefaultPrevented ||\n      !(event as unknown as Event)?.defaultPrevented\n    ) {\n      return ourEventHandler?.(event);\n    }\n  };\n};\n\nexport const base64ToBlob = (base64: string) => {\n  const arr = base64.split(',');\n  const fileType = arr[0].match(/:(.*?);/)![1];\n  const bstr = atob(arr[1]);\n  let l = bstr.length;\n  const u8Arr = new Uint8Array(l);\n\n  while (l--) {\n    u8Arr[l] = bstr.charCodeAt(l);\n  }\n  return new Blob([u8Arr], {\n    type: fileType,\n  });\n};\n\nexport const boardToImage = (\n  board: PlaitBoard,\n  options: ToImageOptions = {}\n) => {\n  return toImage(board, {\n    fillStyle: 'transparent',\n    inlineStyleClassNames: '.extend,.emojis,.text',\n    padding: 20,\n    ratio: 4,\n    ...options,\n  });\n};\n\nexport function download(blob: Blob | MediaSource, filename: string) {\n  const a = document.createElement('a');\n  const url = window.URL.createObjectURL(blob);\n  a.href = url;\n  a.download = filename;\n  document.body.append(a);\n  a.click();\n  window.URL.revokeObjectURL(url);\n  a.remove();\n}\n\nexport const splitRows = <T>(shapes: T[], cols: number) => {\n  const result = [];\n  for (let i = 0; i < shapes.length; i += cols) {\n    result.push(shapes.slice(i, i + cols));\n  }\n  return result;\n};\n\nexport const getShortcutKey = (shortcut: string): string => {\n  shortcut = shortcut\n    .replace(/\\bAlt\\b/i, \"Alt\")\n    .replace(/\\bShift\\b/i, \"Shift\")\n    .replace(/\\b(Enter|Return)\\b/i, \"Enter\");\n  if (IS_APPLE || IS_MAC) {\n    return shortcut\n      .replace(/\\bCtrlOrCmd\\b/gi, \"Cmd\")\n      .replace(/\\bAlt\\b/i, \"Option\");\n  }\n  return shortcut.replace(/\\bCtrlOrCmd\\b/gi, \"Ctrl\");\n};"
  },
  {
    "path": "packages/drawnix/src/utils/image.ts",
    "content": "import { getSelectedElements, PlaitBoard, toSvgData } from '@plait/core';\nimport { base64ToBlob, boardToImage, download } from './common';\nimport { fileOpen } from '../data/filesystem';\nimport { IMAGE_MIME_TYPES } from '../constants';\nimport { insertImage } from '../data/image';\nimport { getBackgroundColor, isWhite } from './color';\nimport { TRANSPARENT } from '../constants/color';\n\nexport const saveAsSvg = (board: PlaitBoard) => {\n  const selectedElements = getSelectedElements(board);\n  const backgroundColor = getBackgroundColor(board);\n\n  return toSvgData(board, {\n    fillStyle: isWhite(backgroundColor) ? TRANSPARENT : backgroundColor,\n    padding: 20,\n    ratio: 4,\n    elements: selectedElements.length > 0 ? selectedElements : undefined,\n    inlineStyleClassNames: '.plait-text-container',\n    styleNames: ['position'],\n  }).then((svgData) => {\n    const blob = new Blob([svgData], { type: 'image/svg+xml' });\n    const imageName = `drawnix-${new Date().getTime()}.svg`;\n    download(blob, imageName);\n  });\n};\n\nexport const saveAsImage = (board: PlaitBoard, isTransparent: boolean) => {\n  const selectedElements = getSelectedElements(board);\n  const backgroundColor = getBackgroundColor(board) || 'white';\n  boardToImage(board, {\n    elements: selectedElements.length > 0 ? selectedElements : undefined,\n    fillStyle: isTransparent ? 'transparent' : backgroundColor,\n  }).then((image) => {\n    if (image) {\n      const ext = isTransparent ? 'png' : 'jpg';\n      const pngImage = base64ToBlob(image);\n      const imageName = `drawnix-${new Date().getTime()}.${ext}`;\n      download(pngImage, imageName);\n    }\n  });\n};\n\nexport const addImage = async (board: PlaitBoard) => {\n  const imageFile = await fileOpen({\n    description: 'Image',\n    extensions: Object.keys(\n      IMAGE_MIME_TYPES\n    ) as (keyof typeof IMAGE_MIME_TYPES)[],\n  });\n  insertImage(board, imageFile);\n};\n"
  },
  {
    "path": "packages/drawnix/src/utils/index.ts",
    "content": "export * from './color';\nexport * from './common';\nexport * from './image';\nexport * from './property';\nexport * from './utility-types';"
  },
  {
    "path": "packages/drawnix/src/utils/laser-pointer.ts",
    "content": "import { PlaitBoard } from '@plait/core';\nimport {\n  drainPoints,\n  drawLaserPen,\n  IOriginalPointData,\n  setColor,\n  setDelay,\n  setMaxWidth,\n  setMinWidth,\n  setOpacity,\n  setRoundCap,\n} from 'laser-pen';\n\nexport const LASER_POINTER_CLASS_NAME = 'laser-pointer';\n\nconst calculateRatio = (context: any): number => {\n  const backingStore =\n    context.backingStorePixelRatio ||\n    context.webkitBackingStorePixelRatio ||\n    context.mozBackingStorePixelRatio ||\n    context.msBackingStorePixelRatio ||\n    context.oBackingStorePixelRatio ||\n    context.backingStorePixelRatio ||\n    1;\n  return (window.devicePixelRatio || 1) / backingStore;\n};\n\nexport class LaserPointer {\n  private mouseTrack: IOriginalPointData[] = [];\n  private mouseMoveHandler: ((event: MouseEvent) => void) | null = null;\n  private resizeHandler: (() => void) | null = null;\n  private cvsDom: HTMLCanvasElement | null = null;\n  private ctx: CanvasRenderingContext2D | null = null;\n  private canvasPos: DOMRect | null = null;\n  private drawing = false;\n  private container: HTMLElement | null = null;\n\n  public init(board: PlaitBoard): void {\n    this.container = PlaitBoard.getBoardContainer(board).closest(\n      '.drawnix'\n    ) as HTMLElement;\n    this.cvsDom = this.container.querySelector(\n      `.${LASER_POINTER_CLASS_NAME}`\n    ) as HTMLCanvasElement;\n    this.ctx = this.cvsDom.getContext('2d') as CanvasRenderingContext2D;\n    this.canvasPos = this.cvsDom.getBoundingClientRect();\n\n    this.mouseMoveHandler = (event: MouseEvent) => {\n      if (!this.canvasPos) return;\n      const relativeX = event.clientX - this.canvasPos.x;\n      const relativeY = event.clientY - this.canvasPos.y;\n      this.mouseTrack.push({\n        x: relativeX,\n        y: relativeY,\n        time: Date.now(),\n      });\n      this.ctx && this.startDraw();\n    };\n\n    this.resizeHandler = () => this.setCanvasSize();\n    this.container.addEventListener('pointermove', this.mouseMoveHandler);\n    window.addEventListener('resize', this.resizeHandler);\n\n    this.setCanvasSize();\n  }\n\n  public destroy(): void {\n    if (this.mouseMoveHandler && this.container) {\n      this.container.removeEventListener('pointermove', this.mouseMoveHandler);\n      this.mouseMoveHandler = null;\n    }\n\n    if (this.resizeHandler) {\n      window.removeEventListener('resize', this.resizeHandler);\n      this.resizeHandler = null;\n    }\n\n    if (this.ctx && this.cvsDom) {\n      this.ctx.clearRect(0, 0, this.cvsDom.width, this.cvsDom.height);\n    }\n\n    this.cvsDom = null;\n    this.ctx = null;\n    this.canvasPos = null;\n    this.drawing = false;\n  }\n\n  private startDraw(): void {\n    if (!this.drawing) {\n      this.drawing = true;\n      this.draw();\n    }\n  }\n\n  private draw(): void {\n    if (!this.ctx || !this.cvsDom) return;\n\n    this.ctx.clearRect(0, 0, this.cvsDom.width, this.cvsDom.height);\n    let needDrawInNextFrame = false;\n\n    this.mouseTrack = drainPoints(this.mouseTrack);\n    if (this.mouseTrack.length >= 3) {\n      setColor(211, 211, 211);\n      setDelay(180);\n      setRoundCap(true);\n      setMaxWidth(10);\n      setMinWidth(0);\n      setOpacity(0.6);\n      drawLaserPen(this.ctx, this.mouseTrack);\n      needDrawInNextFrame = true;\n    } else {\n      const centerPoint = this.mouseTrack[this.mouseTrack.length - 1];\n      if (!centerPoint) return;\n\n      this.ctx.save();\n      this.ctx.beginPath();\n      this.ctx.fillStyle = `rgba(211, 211, 211)`;\n      this.ctx.arc(centerPoint.x, centerPoint.y, 5, 0, Math.PI * 2, false);\n      this.ctx.closePath();\n      this.ctx.fill();\n      this.ctx.restore();\n    }\n\n    if (needDrawInNextFrame) {\n      requestAnimationFrame(() => this.draw());\n    } else {\n      this.drawing = false;\n    }\n  }\n\n  private setCanvasSize(): void {\n    if (!this.cvsDom || !this.ctx) return;\n\n    const rect = this.cvsDom.getBoundingClientRect();\n    const ratio = calculateRatio(this.ctx);\n\n    this.cvsDom.setAttribute('width', `${rect.width * ratio}px`);\n    this.cvsDom.setAttribute('height', `${rect.height * ratio}px`);\n    this.ctx.scale(ratio, ratio);\n\n    this.canvasPos = this.cvsDom.getBoundingClientRect();\n  }\n}\n"
  },
  {
    "path": "packages/drawnix/src/utils/property.ts",
    "content": "import { PlaitBoard, PlaitElement } from '@plait/core';\nimport {\n  isClosedCustomGeometry,\n  isClosedDrawElement,\n  PlaitDrawElement,\n} from '@plait/draw';\nimport {\n  getFillByElement,\n  getStrokeColorByElement,\n  MindElement,\n} from '@plait/mind';\nimport {\n  getFillByElement as getFillByDrawElement,\n  getStrokeColorByElement as getStrokeColorByDrawElement,\n} from '@plait/draw';\nimport { getTextMarksByElement } from '@plait/text-plugins';\n\nexport const isClosedElement = (board: PlaitBoard, element: PlaitElement) => {\n  return (\n    MindElement.isMindElement(board, element) ||\n    (PlaitDrawElement.isDrawElement(element) && isClosedDrawElement(element)) ||\n    isClosedCustomGeometry(board, element)\n  );\n};\n\nexport const getCurrentFill = (board: PlaitBoard, element: PlaitElement) => {\n  let currentFill: string | null = element.fill;\n  if (!currentFill) {\n    if (MindElement.isMindElement(board, element)) {\n      currentFill = getFillByElement(board, element);\n    }\n    if (\n      PlaitDrawElement.isDrawElement(element) ||\n      PlaitDrawElement.isCustomGeometryElement(board, element)\n    ) {\n      currentFill = getFillByDrawElement(board, element);\n    }\n  }\n  return currentFill as string;\n};\n\nexport const getCurrentStrokeColor = (\n  board: PlaitBoard,\n  element: PlaitElement\n) => {\n  let strokeColor: string | null = element.strokeColor;\n  if (!strokeColor) {\n    if (MindElement.isMindElement(board, element)) {\n      strokeColor = getStrokeColorByElement(board, element);\n    }\n    if (\n      PlaitDrawElement.isDrawElement(element) ||\n      PlaitDrawElement.isCustomGeometryElement(board, element)\n    ) {\n      strokeColor = getStrokeColorByDrawElement(board, element);\n    }\n  }\n  return strokeColor as string;\n};\n\nexport const getCurrentFontColor = (\n  board: PlaitBoard,\n  element: PlaitElement\n) => {\n  const marks = getTextMarksByElement(element);\n  return marks.color;\n};\n"
  },
  {
    "path": "packages/drawnix/src/utils/utility-types.ts",
    "content": "export type ResolutionType<T extends (...args: any) => any> = T extends (\n  ...args: any\n) => Promise<infer R>\n  ? R\n  : any;\n\nexport type ValueOf<T> = T[keyof T];\n"
  },
  {
    "path": "packages/drawnix/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"jsx\": \"react-jsx\",\n    \"allowJs\": false,\n    \"esModuleInterop\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"types\": [\"vite/client\"]\n  },\n  \"files\": [],\n  \"include\": [],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.lib.json\"\n    },\n    {\n      \"path\": \"./tsconfig.spec.json\"\n    }\n  ],\n  \"extends\": \"../../tsconfig.base.json\"\n}\n"
  },
  {
    "path": "packages/drawnix/tsconfig.lib.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../dist/out-tsc\",\n    \"types\": [\n      \"node\",\n      \"@nx/react/typings/cssmodule.d.ts\",\n      \"@nx/react/typings/image.d.ts\",\n      \"vite/client\"\n    ]\n  },\n  \"exclude\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"**/*.spec.tsx\",\n    \"**/*.test.tsx\",\n    \"**/*.spec.js\",\n    \"**/*.test.js\",\n    \"**/*.spec.jsx\",\n    \"**/*.test.jsx\"\n  ],\n  \"include\": [\"src/**/*.js\", \"src/**/*.jsx\", \"src/**/*.ts\", \"src/**/*.tsx\"]\n}\n"
  },
  {
    "path": "packages/drawnix/tsconfig.spec.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../dist/out-tsc\",\n    \"module\": \"commonjs\",\n    \"types\": [\"jest\", \"node\"]\n  },\n  \"include\": [\n    \"jest.config.ts\",\n    \"src/**/*.test.ts\",\n    \"src/**/*.spec.ts\",\n    \"src/**/*.test.tsx\",\n    \"src/**/*.spec.tsx\",\n    \"src/**/*.test.js\",\n    \"src/**/*.spec.js\",\n    \"src/**/*.test.jsx\",\n    \"src/**/*.spec.jsx\",\n    \"src/**/*.d.ts\"\n  ]\n}\n"
  },
  {
    "path": "packages/drawnix/vite.config.ts",
    "content": "/// <reference types='vitest' />\nimport { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport dts from 'vite-plugin-dts';\nimport * as path from 'path';\nimport { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';\n\nexport default defineConfig({\n  root: __dirname,\n  cacheDir: '../../node_modules/.vite/packages/drawnix',\n\n  plugins: [\n    react(),\n    nxViteTsPaths(),\n    dts({\n      entryRoot: 'src',\n      tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'),\n    }),\n  ],\n\n  // Uncomment this if you are using workers.\n  // worker: {\n  //  plugins: [ nxViteTsPaths() ],\n  // },\n\n  // Configuration for building your library.\n  // See: https://vitejs.dev/guide/build.html#library-mode\n  build: {\n    outDir: '../../dist/drawnix',\n    emptyOutDir: true,\n    reportCompressedSize: true,\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n    lib: {\n      // Could also be a dictionary or array of multiple entry points.\n      entry: 'src/index.ts',\n      name: 'drawnix',\n      fileName: 'index',\n      // Change this to the formats you want to support.\n      // Don't forget to update your package.json as well.\n      formats: ['es', 'cjs'],\n    },\n    rollupOptions: {\n      // External packages that should not be bundled into your library.\n      external: [\n        'react',\n        'react-dom',\n        'react-dom/client',\n        'react/jsx-runtime',\n        '@plait-board/react-board',\n        '@plait-board/mermaid-to-drawnix',\n        '@plait-board/markdown-to-drawnix',\n        'classnames',\n        'open-color',\n        'mobile-detect',\n        '@floating-ui/react',\n        '@plait/core',\n        '@plait/common',\n        '@plait/draw',\n        '@plait/mind',\n        '@plait/mind',\n        'roughjs/bin/core',\n        '@plait/text-plugins',\n        'lodash',\n        'slate',\n        'slate-react',\n        'slate-dom',\n        'slate-history',\n        'laser-pen',\n      ],\n    },\n  },\n});\n"
  },
  {
    "path": "packages/react-board/.babelrc",
    "content": "{\n  \"presets\": [\n    [\n      \"@nx/react/babel\",\n      {\n        \"runtime\": \"automatic\",\n        \"useBuiltIns\": \"usage\"\n      }\n    ]\n  ],\n  \"plugins\": []\n}\n"
  },
  {
    "path": "packages/react-board/.eslintrc.json",
    "content": "{\n  \"extends\": [\"plugin:@nx/react\", \"../../.eslintrc.json\"],\n  \"ignorePatterns\": [\"!**/*\"],\n  \"overrides\": [\n    {\n      \"files\": [\"*.ts\", \"*.tsx\", \"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.ts\", \"*.tsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/react-board/README.md",
    "content": "# react-board\n\nThis library was generated with [Nx](https://nx.dev).\n\n## Running unit tests\n\nRun `nx test react-board` to execute the unit tests via [Vitest](https://vitest.dev/).\n"
  },
  {
    "path": "packages/react-board/jest.config.ts",
    "content": "/* eslint-disable */\nexport default {\n  displayName: 'react-board',\n  preset: '../../jest.preset.js',\n  transform: {\n    '^(?!.*\\\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',\n    '^.+\\\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }],\n  },\n  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],\n  coverageDirectory: '../../coverage/packages/react-board',\n};\n"
  },
  {
    "path": "packages/react-board/package.json",
    "content": "{\n  \"name\": \"@plait-board/react-board\",\n  \"version\": \"0.4.0-2\",\n  \"main\": \"./index.js\",\n  \"types\": \"./index.d.ts\",\n  \"private\": false,\n  \"dependencies\": {\n    \"@plait/common\": \"^0.92.1\",\n    \"@plait/core\": \"^0.92.1\",\n    \"@plait/draw\": \"^0.92.1\",\n    \"@plait/layouts\": \"^0.92.1\",\n    \"@plait/mind\": \"^0.92.1\",\n    \"@plait/text-plugins\": \"^0.92.1\",\n    \"ahooks\": \"^3.8.0\",\n    \"classnames\": \"^2.5.1\"\n  },\n  \"exports\": {\n    \".\": {\n      \"import\": \"./index.mjs\",\n      \"require\": \"./index.js\",\n      \"types\": \"./index.d.ts\"\n    }\n  }\n}\n"
  },
  {
    "path": "packages/react-board/project.json",
    "content": "{\n  \"name\": \"react-board\",\n  \"$schema\": \"../../node_modules/nx/schemas/project-schema.json\",\n  \"sourceRoot\": \"packages/react-board/src\",\n  \"projectType\": \"library\",\n  \"tags\": [],\n  \"// targets\": \"to see all targets run: nx show project react-board --web\",\n  \"targets\": {}\n}\n"
  },
  {
    "path": "packages/react-board/src/board.spec.tsx",
    "content": "import { render } from '@testing-library/react';\n\nimport {Board} from './board';\n\ndescribe('ReactBoard', () => {\n  it('should render successfully', () => {\n    // const { baseElement } = render(<Board options={} />);\n    // expect(baseElement).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "packages/react-board/src/board.tsx",
    "content": "import rough from 'roughjs/bin/rough';\nimport {\n  BOARD_TO_AFTER_CHANGE,\n  BOARD_TO_CONTEXT,\n  BOARD_TO_ELEMENT_HOST,\n  BOARD_TO_HOST,\n  BOARD_TO_ON_CHANGE,\n  BOARD_TO_ROUGH_SVG,\n  HOST_CLASS_NAME,\n  IS_BOARD_ALIVE,\n  IS_CHROME,\n  IS_FIREFOX,\n  IS_SAFARI,\n  PlaitBoardContext,\n  initializeViewBox,\n  initializeViewportContainer,\n  initializeViewportOffset,\n  PlaitBoard,\n  KEY_TO_ELEMENT_MAP,\n} from '@plait/core';\nimport { useRef, useEffect } from 'react';\nimport React from 'react';\nimport classNames from 'classnames';\nimport useBoardPluginEvent from './hooks/use-plugin-event';\nimport useBoardEvent from './hooks/use-board-event';\n\nimport './styles/index.scss';\nimport { useBoard, useListRender } from './hooks/use-board';\n\nexport type PlaitBoardProps = {\n  style?: React.CSSProperties;\n  className?: string;\n  children?: React.ReactNode;\n  afterInit?: (board: PlaitBoard) => void;\n};\n\nexport const Board: React.FC<PlaitBoardProps> = ({\n  style,\n  className,\n  children,\n  afterInit,\n}) => {\n  const hostRef = useRef<SVGSVGElement>(null);\n  const elementLowerHostRef = useRef<SVGGElement>(null);\n  const elementHostRef = useRef<SVGGElement>(null);\n  const elementUpperHostRef = useRef<SVGGElement>(null);\n  const elementTopHostRef = useRef<SVGGElement>(null);\n  const activeHostRef = useRef<SVGGElement>(null);\n  const viewportContainerRef = useRef<HTMLDivElement>(null);\n  const boardContainerRef = useRef<HTMLDivElement>(null);\n\n  const board = useBoard();\n  const listRender = useListRender();\n\n  useEffect(() => {\n    const roughSVG = rough.svg(hostRef.current!, {\n      options: { roughness: 0, strokeWidth: 1 },\n    });\n    BOARD_TO_ROUGH_SVG.set(board, roughSVG);\n    BOARD_TO_HOST.set(board, hostRef.current!);\n    IS_BOARD_ALIVE.set(board, true);\n    BOARD_TO_ELEMENT_HOST.set(board, {\n      lowerHost: elementLowerHostRef.current!,\n      host: elementHostRef.current!,\n      upperHost: elementUpperHostRef.current!,\n      topHost: elementTopHostRef.current!,\n      activeHost: activeHostRef.current!,\n      container: boardContainerRef.current!,\n      viewportContainer: viewportContainerRef.current!,\n    });\n    const context = new PlaitBoardContext();\n    BOARD_TO_CONTEXT.set(board, context);\n    KEY_TO_ELEMENT_MAP.set(board, new Map());\n\n    if (!listRender.initialized) {\n      listRender.initialize(board.children, {\n        board: board,\n        parent: board,\n        parentG: PlaitBoard.getElementHost(board),\n      });\n      if (afterInit) {\n        afterInit(board);\n      }\n    }\n\n    initializeViewportContainer(board);\n    initializeViewBox(board);\n    initializeViewportOffset(board);\n\n    return () => {\n      BOARD_TO_CONTEXT.delete(board);\n      BOARD_TO_AFTER_CHANGE.delete(board);\n      BOARD_TO_ON_CHANGE.delete(board);\n      BOARD_TO_ELEMENT_HOST.delete(board);\n      IS_BOARD_ALIVE.delete(board);\n      BOARD_TO_HOST.delete(board);\n      BOARD_TO_ROUGH_SVG.delete(board);\n      KEY_TO_ELEMENT_MAP.delete(board);\n    };\n  }, []);\n\n  useBoardPluginEvent(board, viewportContainerRef, hostRef);\n  useBoardEvent(board, viewportContainerRef);\n\n  return (\n    <div\n      className={classNames(\n        className,\n        HOST_CLASS_NAME,\n        `${getBrowserClassName()}`,\n        `theme-${board.theme?.themeColorMode}`,\n        `pointer-${board.pointer}`,\n        {\n          focused: PlaitBoard.isFocus(board),\n          readonly: PlaitBoard.isReadonly(board),\n          'disabled-scroll':\n            board.options?.disabledScrollOnNonFocus &&\n            !PlaitBoard.isFocus(board),\n        }\n      )}\n      ref={boardContainerRef}\n      style={style}\n    >\n      <div\n        className=\"viewport-container\"\n        ref={viewportContainerRef}\n        style={{ width: '100%', height: '100%', overflow: 'auto' }}\n      >\n        <svg\n          ref={hostRef}\n          width=\"100%\"\n          height=\"100%\"\n          style={{ position: 'relative' }}\n          className=\"board-host-svg\"\n        >\n          <g className=\"element-lower-host\" ref={elementLowerHostRef}></g>\n          <g className=\"element-host\" ref={elementHostRef}></g>\n          <g className=\"element-upper-host\" ref={elementUpperHostRef}></g>\n          <g className=\"element-top-host\" ref={elementTopHostRef}></g>\n        </svg>\n        <svg width=\"100%\" height=\"100%\" className=\"board-active-svg\">\n          <g className=\"active-host-g\" ref={activeHostRef}></g>\n        </svg>\n        {children}\n      </div>\n    </div>\n  );\n};\n\nconst getBrowserClassName = () => {\n  if (IS_SAFARI) {\n    return 'safari';\n  }\n  if (IS_CHROME) {\n    return 'chrome';\n  }\n  if (IS_FIREFOX) {\n    return 'firefox';\n  }\n  return '';\n};\n"
  },
  {
    "path": "packages/react-board/src/hooks/use-board-event.ts",
    "content": "import {\n  BoardTransforms,\n  PlaitBoard,\n  ZOOM_STEP,\n  initializeViewBox,\n  initializeViewportContainer,\n  isFromViewportChange,\n  setIsFromViewportChange,\n  updateViewportByScrolling,\n  updateViewportOffset,\n} from '@plait/core';\nimport { useEventListener } from 'ahooks';\nimport { useEffect, useRef } from 'react';\n\nconst useBoardEvent = (\n  board: PlaitBoard,\n  viewportContainerRef: React.RefObject<HTMLDivElement>\n) => {\n  useEventListener(\n    'scroll',\n    (event) => {\n      if (isFromViewportChange(board)) {\n        setIsFromViewportChange(board, false);\n      } else {\n        const { scrollLeft, scrollTop } = event.target as HTMLElement;\n        updateViewportByScrolling(board, scrollLeft, scrollTop);\n      }\n    },\n    { target: viewportContainerRef }\n  );\n\n  useEventListener(\n    'contextmenu',\n    (event) => {\n      event.preventDefault();\n    },\n    { target: viewportContainerRef }\n  );\n\n  useEventListener(\n    'wheel',\n    (event) => {\n      // Credits to excalidraw\n      // https://github.com/excalidraw/excalidraw/blob/b7d7ccc929696cc17b4cc34452e4afd846d59f4f/src/components/App.tsx#L9060\n      if (event.metaKey || event.ctrlKey) {\n        event.preventDefault();\n        const { deltaX, deltaY } = event;\n        const zoom = board.viewport.zoom;\n        const sign = Math.sign(deltaY);\n        const MAX_STEP = ZOOM_STEP * 100;\n        const absDelta = Math.abs(deltaY);\n        let delta = deltaY;\n        if (absDelta > MAX_STEP) {\n          delta = MAX_STEP * sign;\n        }\n        let newZoom = zoom - delta / 100;\n        // increase zoom steps the more zoomed-in we are (applies to >100% only)\n        newZoom +=\n          Math.log10(Math.max(1, zoom)) *\n          -sign *\n          // reduced amplification for small deltas (small movements on a trackpad)\n          Math.min(1, absDelta / 20);\n        BoardTransforms.updateZoom(\n          board,\n          newZoom,\n          PlaitBoard.getMovingPointInBoard(board)\n        );\n      }\n    },\n    { target: viewportContainerRef, passive: false }\n  );\n\n  const isInitialized = useRef(false);\n\n  useEffect(() => {\n    const resizeObserver = new ResizeObserver(() => {\n      if (!isInitialized.current) {\n        isInitialized.current = true;\n        return;\n      }\n      initializeViewportContainer(board);\n      initializeViewBox(board);\n      updateViewportOffset(board);\n    });\n    resizeObserver.observe(PlaitBoard.getBoardContainer(board));\n    return () => {\n      resizeObserver && (resizeObserver as ResizeObserver).disconnect();\n    };\n  }, []);\n};\n\nexport default useBoardEvent;\n"
  },
  {
    "path": "packages/react-board/src/hooks/use-board.tsx",
    "content": "/**\n * A React context for sharing the board object, in a way that re-renders the\n * context whenever changes occur.\n */\n\nimport { ListRender, PlaitBoard } from '@plait/core';\nimport { createContext, useContext } from 'react';\n\nexport interface BoardContextValue {\n  v: number;\n  board: PlaitBoard;\n  listRender: ListRender;\n}\n\nexport const BoardContext = createContext<{\n  v: number;\n  board: PlaitBoard;\n  listRender: ListRender;\n} | null>(null);\n\nexport const useBoard = (): PlaitBoard => {\n  const context = useContext(BoardContext);\n\n  if (!context) {\n    throw new Error(\n      `The \\`useBoard\\` hook must be used inside the <Plait> component's context.`\n    );\n  }\n\n  const { board } = context;\n  return board;\n};\n\nexport const useListRender = (): ListRender => {\n  const context = useContext(BoardContext);\n\n  if (!context) {\n    throw new Error(\n      `The \\`useBoard\\` hook must be used inside the <Plait> component's context.`\n    );\n  }\n\n  const { listRender } = context;\n  return listRender;\n};\n"
  },
  {
    "path": "packages/react-board/src/hooks/use-plugin-event.tsx",
    "content": "import {\n  BOARD_TO_MOVING_POINT,\n  BOARD_TO_MOVING_POINT_IN_BOARD,\n  PlaitBoard,\n  WritableClipboardOperationType,\n  deleteFragment,\n  getClipboardData,\n  hasInputOrTextareaTarget,\n  setFragment,\n  toHostPoint,\n  toViewBoxPoint,\n} from '@plait/core';\nimport { useEventListener } from 'ahooks';\n\nconst useBoardPluginEvent = (\n  board: PlaitBoard,\n  viewportContainerRef: React.RefObject<HTMLDivElement>,\n  hostRef: React.RefObject<SVGSVGElement>\n) => {\n  useEventListener(\n    'pointerdown',\n    (event) => {\n      board.pointerDown(event);\n    },\n    { target: hostRef }\n  );\n\n  useEventListener(\n    'pointermove',\n    (event) => {\n      BOARD_TO_MOVING_POINT_IN_BOARD.set(board, [event.x, event.y]);\n      board.pointerMove(event);\n    },\n    { target: viewportContainerRef }\n  );\n\n  useEventListener(\n    'pointerleave',\n    (event) => {\n      BOARD_TO_MOVING_POINT_IN_BOARD.delete(board);\n      board.pointerLeave(event);\n    },\n    { target: viewportContainerRef }\n  );\n\n  useEventListener(\n    'pointerup',\n    (event) => {\n      board.pointerUp(event);\n    },\n    { target: viewportContainerRef }\n  );\n\n  useEventListener(\n    'touchstart',\n    (event) => {\n      board.touchStart(event);\n    },\n    { target: viewportContainerRef }\n  );\n\n  useEventListener(\n    'touchmove',\n    (event) => {\n      board.touchMove(event);\n    },\n    { target: viewportContainerRef }\n  );\n\n  useEventListener(\n    'touchend',\n    (event) => {\n      board.touchEnd(event);\n    },\n    { target: viewportContainerRef }\n  );\n\n  useEventListener(\n    'dblclick',\n    (event) => {\n      if (PlaitBoard.isFocus(board) && !PlaitBoard.hasBeenTextEditing(board)) {\n        board.dblClick(event);\n      }\n    },\n    { target: hostRef }\n  );\n\n  useEventListener('pointermove', (event) => {\n    BOARD_TO_MOVING_POINT.set(board, [event.x, event.y]);\n    board.globalPointerMove(event);\n  });\n\n  useEventListener('pointerup', (event) => {\n    board.globalPointerUp(event);\n  });\n\n  useEventListener('keydown', (event) => {\n    board.globalKeyDown(event);\n    if (\n      PlaitBoard.isFocus(board) &&\n      !PlaitBoard.hasBeenTextEditing(board) &&\n      !hasInputOrTextareaTarget(event.target)\n    ) {\n      board.keyDown(event);\n    }\n  });\n\n  useEventListener('keyup', (event) => {\n    if (PlaitBoard.isFocus(board) && !PlaitBoard.hasBeenTextEditing(board)) {\n      board?.keyUp(event);\n    }\n  });\n\n  useEventListener('copy', (event) => {\n    if (PlaitBoard.isFocus(board) && !PlaitBoard.hasBeenTextEditing(board)) {\n      event.preventDefault();\n      setFragment(\n        board,\n        WritableClipboardOperationType.copy,\n        event.clipboardData\n      );\n    }\n  });\n\n  useEventListener('paste', async (clipboardEvent) => {\n    if (\n      PlaitBoard.isFocus(board) &&\n      !PlaitBoard.isReadonly(board) &&\n      !PlaitBoard.hasBeenTextEditing(board)\n    ) {\n      const mousePoint = PlaitBoard.getMovingPointInBoard(board);\n      if (mousePoint) {\n        const targetPoint = toViewBoxPoint(\n          board,\n          toHostPoint(board, mousePoint[0], mousePoint[1])\n        );\n        const clipboardData = await getClipboardData(\n          clipboardEvent.clipboardData\n        );\n        board.insertFragment(\n          clipboardData,\n          targetPoint,\n          WritableClipboardOperationType.paste\n        );\n      }\n    }\n  });\n\n  useEventListener('cut', (event) => {\n    if (\n      PlaitBoard.isFocus(board) &&\n      !PlaitBoard.isReadonly(board) &&\n      !PlaitBoard.hasBeenTextEditing(board)\n    ) {\n      event.preventDefault();\n      setFragment(\n        board,\n        WritableClipboardOperationType.cut,\n        event.clipboardData\n      );\n      deleteFragment(board);\n    }\n  });\n\n  useEventListener(\n    'drop',\n    (event) => {\n      if (!PlaitBoard.isReadonly(board)) {\n        event.preventDefault();\n        board.drop(event);\n      }\n    },\n    { target: viewportContainerRef }\n  );\n\n  useEventListener(\n    'dragover',\n    (event) => {\n      event.preventDefault();\n    },\n    { target: viewportContainerRef }\n  );\n};\n\nexport default useBoardPluginEvent;\n"
  },
  {
    "path": "packages/react-board/src/index.ts",
    "content": "export * from './board';\nexport * from './plugins/board';\nexport * from './wrapper';\nexport * from './hooks/use-board';\nexport * from './plugins/with-pinch-zoom-plugin';\n"
  },
  {
    "path": "packages/react-board/src/plugins/board.ts",
    "content": "import type { RenderComponentRef } from '@plait/common';\nimport {\n  PlaitElement,\n  PlaitOperation,\n  Viewport,\n  Selection,\n  type PlaitTheme\n} from '@plait/core';\n\nexport interface ReactBoard {\n  renderComponent: <T extends object>(\n    children: React.ReactNode,\n    container: Element | DocumentFragment,\n    props: T\n  ) => RenderComponentRef<T>;\n}\n\nexport interface BoardChangeData {\n  children: PlaitElement[];\n  operations: PlaitOperation[];\n  viewport: Viewport;\n  selection: Selection | null;\n  theme: PlaitTheme;\n}\n"
  },
  {
    "path": "packages/react-board/src/plugins/with-pinch-zoom-plugin.ts",
    "content": "import {\n  BoardTransforms,\n  distanceBetweenPointAndPoint,\n  getPointBetween,\n  getViewportOrigination,\n  MAX_ZOOM,\n  MIN_ZOOM,\n  PlaitBoard,\n  Point,\n} from '@plait/core';\n\ninterface PointerRecord {\n  pointerId: number;\n  lastPoint: Point;\n  currentPoint: Point;\n  hasMoved: boolean;\n}\n\nexport const TOUCH_RECORDS = new WeakMap<PlaitBoard, PointerRecord[]>();\n\nexport const isTwoFingerMode = (board: PlaitBoard) => {\n  const pointerRecords = TOUCH_RECORDS.get(board);\n  return pointerRecords?.length === 2;\n};\n\nexport const withPinchZoom = (board: PlaitBoard) => {\n  const { touchStart, touchMove, touchEnd } = board;\n\n  let pointerRecords: PointerRecord[] = [];\n  let initializeZoom = 0;\n  let isPinching = false;\n\n  board.touchStart = (event: TouchEvent) => {\n    pointerRecords = Array.from(event.touches).map((touch) => ({\n      pointerId: touch.identifier,\n      lastPoint: [touch.clientX, touch.clientY],\n      currentPoint: [touch.clientX, touch.clientY],\n      hasMoved: false,\n    }));\n    TOUCH_RECORDS.set(board, pointerRecords);\n    if (pointerRecords.length >= 2) {\n      initializeZoom = board.viewport.zoom;\n    }\n    touchStart(event);\n  };\n\n  board.touchMove = (event: TouchEvent) => {\n    Array.from(event.changedTouches).forEach((touch) => {\n      const record = pointerRecords.find(\n        (record) => record.pointerId === touch.identifier\n      );\n      if (record) {\n        record.lastPoint = record.currentPoint;\n        record.currentPoint = [touch.clientX, touch.clientY];\n        record.hasMoved = true;\n      }\n    });\n    if (pointerRecords.length === 2) {\n      event.preventDefault();\n    }\n    if (\n      pointerRecords.length === 2 &&\n      pointerRecords.every((record) => record.hasMoved)\n    ) {\n      const [p1, p2] = pointerRecords;\n      const pinchCenter = getPointBetween(\n        ...p1.lastPoint,\n        ...p2.lastPoint\n      ) as Point;\n      const newPinchCenter = getPointBetween(\n        ...p1.currentPoint,\n        ...p2.currentPoint\n      ) as Point;\n      const dx = newPinchCenter[0] - pinchCenter[0];\n      const dy = newPinchCenter[1] - pinchCenter[1];\n\n      // hand moving\n      const boardContainerRect =\n        PlaitBoard.getBoardContainer(board).getBoundingClientRect();\n      const halfOfWidth = boardContainerRect.width / 2;\n      const halfOfHeight = boardContainerRect.height / 2;\n      const zoom = board.viewport.zoom;\n      const origination = getViewportOrigination(board);\n      let centerX = origination![0] + halfOfWidth / zoom - dx / zoom;\n      let centerY = origination![1] + halfOfHeight / zoom - dy / zoom;\n      let newOrigination = [\n        centerX - boardContainerRect.width / 2 / zoom,\n        centerY - boardContainerRect.height / 2 / zoom,\n      ] as Point;\n\n      let newZoom = zoom;\n      const lastDistance = distanceBetweenPointAndPoint(\n        ...p1.lastPoint,\n        ...p2.lastPoint\n      );\n      const currentDistance = distanceBetweenPointAndPoint(\n        ...p1.currentPoint,\n        ...p2.currentPoint\n      );\n      // zoom 处理\n      const scale = currentDistance / lastDistance;\n\n      const v1 = [\n        p1.currentPoint[0] - p1.lastPoint[0],\n        p1.currentPoint[1] - p1.lastPoint[1],\n      ] as Point;\n      const v2 = [\n        p2.currentPoint[0] - p2.lastPoint[0],\n        p2.currentPoint[1] - p2.lastPoint[1],\n      ] as Point;\n\n      const dotProduct = v1[0] * v2[0] + v1[1] * v2[1];\n      const v1Magnitude = Math.sqrt(v1[0] * v1[0] + v1[1] * v1[1]);\n      const v2Magnitude = Math.sqrt(v2[0] * v2[0] + v2[1] * v2[1]);\n\n      const cosTheta = dotProduct / (v1Magnitude * v2Magnitude || 1);\n\n      // 控制缩放\n      // 基于余弦相似度（Cosine Similarity）\n      // 余弦值 > 0.8：判定为平移手势（向量基本同向）\n      // 余弦值 < -0.7：判定为缩放手势（向量基本反向）\n      // 其他情况：未知手势\n      if (cosTheta < -0.7 || (cosTheta <= 0.8 && isPinching && scale >= 0.01)) {\n        isPinching = true;\n      } else {\n        isPinching = false;\n      }\n      if (isPinching) {\n        newZoom = Math.min(\n          Math.max(board.viewport.zoom * scale, MIN_ZOOM),\n          MAX_ZOOM\n        );\n        const nativeElement = PlaitBoard.getBoardContainer(board);\n        const nativeElementRect = nativeElement.getBoundingClientRect();\n        const zoomCenterWidth = newPinchCenter[0] - nativeElementRect.x;\n        const zoomCenterHeight = newPinchCenter[1] - nativeElementRect.y;\n        centerX = newOrigination[0] + zoomCenterWidth / zoom;\n        centerY = newOrigination[1] + zoomCenterHeight / zoom;\n        newOrigination = [\n          centerX - zoomCenterWidth / newZoom,\n          centerY - zoomCenterHeight / newZoom,\n        ] as Point;\n      }\n      BoardTransforms.updateViewport(board, newOrigination, newZoom);\n      pointerRecords[0].lastPoint = p1.currentPoint;\n      pointerRecords[1].lastPoint = p2.currentPoint;\n      pointerRecords[0].hasMoved = false;\n      pointerRecords[1].hasMoved = false;\n      return;\n    }\n    touchMove(event);\n  };\n\n  board.touchEnd = (event: TouchEvent) => {\n    const index = pointerRecords.findIndex(\n      (r) => r.pointerId === event.changedTouches[0].identifier\n    );\n    if (index !== -1) {\n      pointerRecords.splice(index, 1);\n    }\n    TOUCH_RECORDS.set(board, pointerRecords);\n    touchEnd(event);\n  };\n\n  return board;\n};\n"
  },
  {
    "path": "packages/react-board/src/plugins/with-react.tsx",
    "content": "import {\n  type PlaitTextBoard,\n  type RenderComponentRef,\n  type TextProps,\n} from '@plait/common';\nimport type { PlaitBoard } from '@plait/core';\nimport { createRoot } from 'react-dom/client';\nimport { Text } from '@plait-board/react-text';\nimport { ReactEditor } from 'slate-react';\nimport type { ReactBoard } from './board';\n\nexport const withReact = (board: PlaitBoard & PlaitTextBoard) => {\n  const newBoard = board as PlaitBoard & PlaitTextBoard & ReactBoard;\n\n  newBoard.renderText = (\n    container: Element | DocumentFragment,\n    props: TextProps\n  ) => {\n    const root = createRoot(container);\n    let currentEditor: ReactEditor;\n    const text = (\n      <Text\n        {...props}\n        afterInit={(editor) => {\n          currentEditor = editor as ReactEditor;\n          props.afterInit && props.afterInit(editor);\n        }}\n      ></Text>\n    );\n    root.render(text);\n    let newProps = { ...props };\n    const ref: RenderComponentRef<TextProps> = {\n      destroy: () => {\n        setTimeout(() => {\n          root.unmount();\n        }, 0);\n      },\n      update: (updatedProps: Partial<TextProps>) => {\n        const hasUpdated =\n          updatedProps &&\n          newProps &&\n          !Object.keys(updatedProps).every(\n            (key) =>\n              updatedProps[key as keyof TextProps] ===\n              newProps[key as keyof TextProps]\n          );\n        if (!hasUpdated) {\n          return;\n        }\n        const readonly = ReactEditor.isReadOnly(currentEditor);\n        newProps = { ...newProps, ...updatedProps };\n        root.render(<Text {...newProps}></Text>);\n\n        if (readonly === true && newProps.readonly === false) {\n          setTimeout(() => {\n            ReactEditor.focus(currentEditor);\n          }, 100);\n        } else if (readonly === false && newProps.readonly === true) {\n          ReactEditor.blur(currentEditor);\n          ReactEditor.deselect(currentEditor);\n        }\n      },\n    };\n    return ref;\n  };\n\n  return newBoard;\n};\n"
  },
  {
    "path": "packages/react-board/src/styles/index.scss",
    "content": "@use './mixins.scss' as mixins;\n\n.plait-board-container {\n    display: block;\n    width: 100%;\n    height: 100%;\n    position: relative;\n    overflow: hidden;\n    foreignObject {\n        outline: none;\n    }\n    // safari can not set this style, it will prevent text being from selected in edit mode\n    // resolve the issue text being selected when user drag and move on board in firefox\n    &.firefox {\n        user-select: none;\n    }\n\n    .viewport-container {\n        width: 100%;\n        height: 100%;\n        overflow: auto;\n    }\n\n    &.disabled-scroll {\n        .viewport-container {\n            overflow: hidden;\n        }\n    }\n\n    svg {\n        transform: matrix(1, 0, 0, 1, 0, 0);\n    }\n\n    // https://stackoverflow.com/questions/51313873/svg-foreignobject-not-working-properly-on-safari\n    .plait-text-container {\n        // chrome show position is not correct, safari not working when don't assigned position property\n        // can not assign absolute, because safari can not show correctly position\n        position: initial !important;\n    }\n\n    .text {\n        foreignObject {\n            outline: none;\n        }\n        .slate-editable-container {\n            outline: none;\n        }\n    }\n\n    .plait-toolbar {\n        position: absolute;\n        display: flex;\n        height: 30px;\n        z-index: 100;\n    }\n\n    &.element-moving {\n        .element-active-host {\n            & > g:not(.active-with-moving) {\n                display: none;\n            }\n        }\n    }\n\n    &.element-rotating {\n        .element-active-host {\n            g.resize-handle,\n            g[class^='line-auto-complete-'] {\n                display: none;\n            }\n        }\n    }\n\n    &.pointer-selection {\n        cursor: default;\n    }\n\n    &.ns-resize {\n        cursor: ns-resize;\n    }\n    &.ew-resize {\n        cursor: ew-resize;\n    }\n    &.nwse-resize {\n        cursor: nwse-resize;\n    }\n    &.nesw-resize {\n        cursor: nesw-resize;\n    }\n    &.crosshair {\n        cursor: crosshair;\n    }\n\n    foreignObject[class^='foreign-object-'] {\n        user-select: none;\n    }\n\n    .board-active-svg {\n        position: absolute;\n        left: 0;\n        top: 0;\n        pointer-events: none;\n    }\n\n    @include mixins.board-background-color();\n}\n"
  },
  {
    "path": "packages/react-board/src/styles/mixins.scss",
    "content": "@mixin board-background-color {\n    &.theme-colorful .board-host-svg,\n    &.theme-default .board-host-svg {\n        background-color: #ffffff;\n    }\n    &.theme-soft .board-host-svg {\n        background-color: #f5f5f5;\n    }\n    &.theme-retro .board-host-svg {\n        background-color: #f9f8ed;\n    }\n    &.theme-dark .board-host-svg {\n        background-color: #141414;\n    }\n    &.theme-starry .board-host-svg {\n        background-color: #0d2537;\n    }\n}\n"
  },
  {
    "path": "packages/react-board/src/wrapper.tsx",
    "content": "import {\n  BOARD_TO_ON_CHANGE,\n  ListRender,\n  PlaitElement,\n  Viewport,\n  createBoard,\n  withBoard,\n  withHandPointer,\n  withHistory,\n  withHotkey,\n  withMoving,\n  withOptions,\n  withRelatedFragment,\n  withSelection,\n  PlaitBoard,\n  type PlaitPlugin,\n  type PlaitBoardOptions,\n  type Selection,\n  ThemeColorMode,\n  BOARD_TO_AFTER_CHANGE,\n  PlaitOperation,\n  PlaitTheme,\n  isFromScrolling,\n  setIsFromScrolling,\n  getSelectedElements,\n  updateViewportOffset,\n  initializeViewBox,\n  withI18n,\n  updateViewBox,\n  FLUSHING,\n  BoardTransforms,\n} from '@plait/core';\nimport { BoardChangeData } from './plugins/board';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { withReact } from './plugins/with-react';\nimport { PlaitCommonElementRef, withImage, withText } from '@plait/common';\nimport { BoardContext, BoardContextValue } from './hooks/use-board';\nimport React from 'react';\nimport { withPinchZoom } from './plugins/with-pinch-zoom-plugin';\n\nexport type WrapperProps = {\n  value: PlaitElement[];\n  children: React.ReactNode;\n  options: PlaitBoardOptions;\n  plugins: PlaitPlugin[];\n  viewport?: Viewport;\n  theme?: PlaitTheme;\n  onChange?: (data: BoardChangeData) => void;\n  onSelectionChange?: (selection: Selection | null) => void;\n  onValueChange?: (value: PlaitElement[]) => void;\n  onViewportChange?: (value: Viewport) => void;\n  onThemeChange?: (value: ThemeColorMode) => void;\n};\n\nexport const Wrapper: React.FC<WrapperProps> = ({\n  value,\n  children,\n  options,\n  plugins,\n  viewport,\n  theme,\n  onChange,\n  onSelectionChange,\n  onValueChange,\n  onViewportChange,\n  onThemeChange,\n}) => {\n  const [context, setContext] = useState<BoardContextValue>(() => {\n    const board = initializeBoard(value, options, plugins, viewport, theme);\n    const listRender = initializeListRender(board);\n    return {\n      v: 0,\n      board,\n      listRender,\n    };\n  });\n\n  const { board, listRender } = context;\n\n  const onContextChange = useCallback(() => {\n    if (onChange) {\n      const data: BoardChangeData = {\n        children: board.children,\n        operations: board.operations,\n        viewport: board.viewport,\n        selection: board.selection,\n        theme: board.theme,\n      };\n      onChange(data);\n    }\n\n    const hasSelectionChanged = board.operations.some((o) =>\n      PlaitOperation.isSetSelectionOperation(o)\n    );\n    const hasViewportChanged = board.operations.some((o) =>\n      PlaitOperation.isSetViewportOperation(o)\n    );\n    const hasThemeChanged = board.operations.some((o) =>\n      PlaitOperation.isSetThemeOperation(o)\n    );\n    const hasChildrenChanged =\n      board.operations.length > 0 &&\n      !board.operations.every(\n        (o) =>\n          PlaitOperation.isSetSelectionOperation(o) ||\n          PlaitOperation.isSetViewportOperation(o) ||\n          PlaitOperation.isSetThemeOperation(o)\n      );\n\n    if (onValueChange && hasChildrenChanged) {\n      onValueChange(board.children);\n    }\n\n    if (onSelectionChange && hasSelectionChanged) {\n      onSelectionChange(board.selection);\n    }\n\n    if (onViewportChange && hasViewportChanged) {\n      onViewportChange(board.viewport);\n    }\n\n    if (onThemeChange && hasThemeChanged) {\n      onThemeChange(board.theme.themeColorMode);\n    }\n\n    setContext((prevContext) => ({\n      v: prevContext.v + 1,\n      board,\n      listRender,\n    }));\n  }, [board, onChange, onSelectionChange, onValueChange]);\n\n  useEffect(() => {\n    BOARD_TO_ON_CHANGE.set(board, () => {\n      const isOnlySetSelection =\n        board.operations.length &&\n        board.operations.every((op) => op.type === 'set_selection');\n      if (isOnlySetSelection) {\n        listRender.update(board.children, {\n          board: board,\n          parent: board,\n          parentG: PlaitBoard.getElementHost(board),\n        });\n        return;\n      }\n      const isSetViewport =\n        board.operations.length &&\n        board.operations.some((op) => op.type === 'set_viewport');\n      if (isSetViewport && isFromScrolling(board)) {\n        setIsFromScrolling(board, false);\n        listRender.update(board.children, {\n          board: board,\n          parent: board,\n          parentG: PlaitBoard.getElementHost(board),\n        });\n        return;\n      }\n      listRender.update(board.children, {\n        board: board,\n        parent: board,\n        parentG: PlaitBoard.getElementHost(board),\n      });\n      if (isSetViewport) {\n        initializeViewBox(board);\n      } else {\n        updateViewBox(board);\n      }\n      updateViewportOffset(board);\n      const selectedElements = getSelectedElements(board);\n      selectedElements.forEach((element) => {\n        const elementRef =\n          PlaitElement.getElementRef<PlaitCommonElementRef>(element);\n        elementRef.updateActiveSection();\n      });\n    });\n\n    BOARD_TO_AFTER_CHANGE.set(board, () => {\n      onContextChange();\n    });\n\n    return () => {\n      BOARD_TO_ON_CHANGE.delete(board);\n      BOARD_TO_AFTER_CHANGE.delete(board);\n    };\n  }, [board]);\n\n  const isFirstRender = useRef(true);\n\n  useEffect(() => {\n    if (isFirstRender.current) {\n      isFirstRender.current = false;\n      return;\n    }\n\n    if (value !== context.board.children && !FLUSHING.get(board)) {\n      board.children = value;\n      if (theme) {\n        board.theme = theme;\n      }\n      listRender.update(board.children, {\n        board: board,\n        parent: board,\n        parentG: PlaitBoard.getElementHost(board),\n      });\n      BoardTransforms.fitViewport(board);\n    }\n  }, [value]);\n\n  return (\n    <BoardContext.Provider value={context}>{children}</BoardContext.Provider>\n  );\n};\n\nconst initializeBoard = (\n  value: PlaitElement[],\n  options: PlaitBoardOptions,\n  plugins: PlaitPlugin[],\n  viewport?: Viewport,\n  theme?: PlaitTheme\n) => {\n  let board = withRelatedFragment(\n    withHotkey(\n      withHandPointer(\n        withHistory(\n          withSelection(\n            withMoving(\n              withBoard(\n                withI18n(\n                  withOptions(\n                    withReact(withImage(withText(createBoard(value, options))))\n                  )\n                )\n              )\n            )\n          )\n        )\n      )\n    )\n  );\n  plugins.forEach((plugin: any) => {\n    board = plugin(board);\n  });\n\n  withPinchZoom(board);\n\n  if (viewport) {\n    board.viewport = viewport;\n  }\n\n  if (theme) {\n    board.theme = theme;\n  }\n\n  return board;\n};\n\nconst initializeListRender = (board: PlaitBoard) => {\n  const listRender = new ListRender(board);\n  return listRender;\n};\n"
  },
  {
    "path": "packages/react-board/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"jsx\": \"react-jsx\",\n    \"allowJs\": false,\n    \"esModuleInterop\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"types\": [\"vite/client\"]\n  },\n  \"files\": [],\n  \"include\": [],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.lib.json\"\n    },\n    {\n      \"path\": \"./tsconfig.spec.json\"\n    }\n  ],\n  \"extends\": \"../../tsconfig.base.json\"\n}\n"
  },
  {
    "path": "packages/react-board/tsconfig.lib.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../dist/out-tsc\",\n    \"types\": [\n      \"node\",\n      \"@nx/react/typings/cssmodule.d.ts\",\n      \"@nx/react/typings/image.d.ts\",\n      \"vite/client\"\n    ]\n  },\n  \"exclude\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"**/*.spec.tsx\",\n    \"**/*.test.tsx\",\n    \"**/*.spec.js\",\n    \"**/*.test.js\",\n    \"**/*.spec.jsx\",\n    \"**/*.test.jsx\"\n  ],\n  \"include\": [\"src/**/*.js\", \"src/**/*.jsx\", \"src/**/*.ts\", \"src/**/*.tsx\"]\n}\n"
  },
  {
    "path": "packages/react-board/tsconfig.spec.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../dist/out-tsc\",\n    \"module\": \"commonjs\",\n    \"types\": [\"jest\", \"node\"]\n  },\n  \"include\": [\n    \"jest.config.ts\",\n    \"src/**/*.test.ts\",\n    \"src/**/*.spec.ts\",\n    \"src/**/*.test.tsx\",\n    \"src/**/*.spec.tsx\",\n    \"src/**/*.test.js\",\n    \"src/**/*.spec.js\",\n    \"src/**/*.test.jsx\",\n    \"src/**/*.spec.jsx\",\n    \"src/**/*.d.ts\"\n  ]\n}\n"
  },
  {
    "path": "packages/react-board/vite.config.ts",
    "content": "/// <reference types='vitest' />\nimport { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport dts from 'vite-plugin-dts';\nimport * as path from 'path';\nimport { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';\n\nexport default defineConfig({\n  root: __dirname,\n  cacheDir: '../../node_modules/.vite/packages/react-board',\n\n  plugins: [\n    react(),\n    nxViteTsPaths(),\n    dts({\n      entryRoot: 'src',\n      tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'),\n    }),\n  ],\n\n  // Uncomment this if you are using workers.\n  // worker: {\n  //  plugins: [ nxViteTsPaths() ],\n  // },\n\n  // Configuration for building your library.\n  // See: https://vitejs.dev/guide/build.html#library-mode\n  build: {\n    outDir: '../../dist/react-board',\n    emptyOutDir: true,\n    reportCompressedSize: true,\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n    lib: {\n      // Could also be a dictionary or array of multiple entry points.\n      entry: 'src/index.ts',\n      name: 'react-board',\n      fileName: 'index',\n      // Change this to the formats you want to support.\n      // Don't forget to update your package.json as well.\n      formats: ['es', 'cjs'],\n    },\n    rollupOptions: {\n      // External packages that should not be bundled into your library.\n      external: [\n        'react',\n        'react-dom',\n        'react-dom/client',\n        'react/jsx-runtime',\n        '@plait/common',\n        '@plait/core',\n        '@plait/draw',\n        '@plait/layouts',\n        '@plait/mind',\n        '@plait/text-plugins',\n        'ahooks',\n        'classnames',\n        '@plait-board/react-text',\n        'roughjs/bin/rough',\n        'slate-react',\n      ],\n    },\n  },\n});\n"
  },
  {
    "path": "packages/react-text/.babelrc",
    "content": "{\n  \"presets\": [\n    [\n      \"@nx/react/babel\",\n      {\n        \"runtime\": \"automatic\",\n        \"useBuiltIns\": \"usage\"\n      }\n    ]\n  ],\n  \"plugins\": []\n}\n"
  },
  {
    "path": "packages/react-text/.eslintrc.json",
    "content": "{\n  \"extends\": [\"plugin:@nx/react\", \"../../.eslintrc.json\"],\n  \"ignorePatterns\": [\"!**/*\"],\n  \"overrides\": [\n    {\n      \"files\": [\"*.ts\", \"*.tsx\", \"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.ts\", \"*.tsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/react-text/README.md",
    "content": "# react-text\n\nThis library was generated with [Nx](https://nx.dev).\n\n## Running unit tests\n\nRun `nx test react-text` to execute the unit tests via [Vitest](https://vitest.dev/).\n"
  },
  {
    "path": "packages/react-text/jest.config.ts",
    "content": "/* eslint-disable */\nexport default {\n  displayName: 'react-text',\n  preset: '../../jest.preset.js',\n  transform: {\n    '^(?!.*\\\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',\n    '^.+\\\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }],\n  },\n  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],\n  coverageDirectory: '../../coverage/packages/react-text',\n};\n"
  },
  {
    "path": "packages/react-text/package.json",
    "content": "{\n  \"name\": \"@plait-board/react-text\",\n  \"version\": \"0.4.0-2\",\n  \"main\": \"./index.js\",\n  \"types\": \"./index.d.ts\",\n  \"private\": false,\n  \"dependencies\": {\n    \"slate\": \"^0.116.0\",\n    \"slate-dom\": \"^0.116.0\",\n    \"slate-history\": \"^0.115.0\",\n    \"slate-react\": \"^0.116.0\",\n    \"@plait/text-plugins\": \"^0.92.1\",\n    \"@plait/common\": \"^0.92.1\",\n    \"is-hotkey\": \"^0.2.0\"\n  },\n  \"exports\": {\n    \".\": {\n      \"import\": \"./index.mjs\",\n      \"require\": \"./index.js\",\n      \"types\": \"./index.d.ts\"\n    }\n  }\n}\n"
  },
  {
    "path": "packages/react-text/project.json",
    "content": "{\n  \"name\": \"react-text\",\n  \"$schema\": \"../../node_modules/nx/schemas/project-schema.json\",\n  \"sourceRoot\": \"packages/react-text/src\",\n  \"projectType\": \"library\",\n  \"tags\": [],\n  \"// targets\": \"to see all targets run: nx show project react-text --web\",\n  \"targets\": {}\n}\n"
  },
  {
    "path": "packages/react-text/src/custom-types.ts",
    "content": "import { BaseEditor, BaseRange, Range, Element } from 'slate';\nimport { ReactEditor, RenderElementProps } from 'slate-react';\nimport { HistoryEditor } from 'slate-history';\nimport { CustomElement, CustomText } from '@plait/common';\n\nexport type CustomEditor = BaseEditor &\n  ReactEditor &\n  HistoryEditor & {\n    nodeToDecorations?: Map<Element, Range[]>;\n  };\n\nexport type RenderElementPropsFor<T> = RenderElementProps & {\n  element: T;\n};\n\ndeclare module 'slate' {\n  interface CustomTypes {\n    Editor: CustomEditor;\n    Element: CustomElement;\n    Text: CustomText;\n    Range: BaseRange & {\n      [key: string]: unknown;\n    };\n  }\n}\n"
  },
  {
    "path": "packages/react-text/src/index.ts",
    "content": "export * from './text';\nexport * from './custom-types';\n"
  },
  {
    "path": "packages/react-text/src/plugins/index.ts",
    "content": "export * from './with-text';\nexport * from './with-link';\n"
  },
  {
    "path": "packages/react-text/src/plugins/with-link.tsx",
    "content": "import { CustomElement, LinkElement } from '@plait/common';\nimport { CustomEditor, RenderElementPropsFor } from '../custom-types';\nimport { isUrl, LinkEditor } from '@plait/text-plugins';\n\n// Put this at the start and end of an inline component to work around this Chromium bug:\n// https://bugs.chromium.org/p/chromium/issues/detail?id=1249405\nexport const InlineChromiumBugfix = () => (\n  <span contentEditable={false} style={{ fontSize: 0 }}>\n    {String.fromCodePoint(160) /* Non-breaking space */}\n  </span>\n);\n\nexport const LinkComponent = ({\n  attributes,\n  children,\n  element,\n}: RenderElementPropsFor<LinkElement>) => {\n  return (\n    <a\n      {...attributes}\n      style={{\n        textDecoration: 'none',\n        cursor: 'inherit',\n      }}\n      data-url={element.url}\n      className=\"plait-board-link\"\n    >\n      <InlineChromiumBugfix />\n      {children}\n      <InlineChromiumBugfix />\n    </a>\n  );\n};\n\nexport const withInlineLink = (editor: CustomEditor) => {\n  const { insertData, insertText, isInline } = editor;\n\n  editor.isInline = (element: CustomElement) => {\n    return (\n      ((element as LinkElement).type &&\n        ['link'].includes((element as LinkElement).type)) ||\n      isInline(element)\n    );\n  };\n\n  editor.insertText = (text) => {\n    if (text && isUrl(text)) {\n      LinkEditor.wrapLink(editor, text, text);\n    } else {\n      insertText(text);\n    }\n  };\n\n  editor.insertData = (data) => {\n    const text = data.getData('text/plain');\n\n    if (text && isUrl(text)) {\n      LinkEditor.wrapLink(editor, text, text);\n    } else {\n      insertData(data);\n    }\n  };\n\n  return editor;\n};\n"
  },
  {
    "path": "packages/react-text/src/plugins/with-text.ts",
    "content": "import { ReactEditor } from 'slate-react';\n\nexport const withText = <T extends ReactEditor>(editor: T) => {\n  const e = editor as T;\n  const { insertData } = e;\n\n  e.insertBreak = () => {\n    editor.insertText('\\n');\n  };\n\n  e.insertSoftBreak = () => {\n    editor.insertText('\\n');\n  };\n\n  e.insertData = (data: DataTransfer) => {\n    let text = data.getData('text/plain');\n    const plaitData = data.getData(`application/x-slate-fragment`);\n    if (!plaitData && text) {\n      if (text.endsWith('\\n')) {\n        text = text.substring(0, text.length - 1);\n      }\n      text = text.trim().replace(/\\t+/g, ' ');\n      e.insertText(text);\n      return;\n    }\n    insertData(data);\n  };\n\n  return e;\n};\n"
  },
  {
    "path": "packages/react-text/src/styles/index.scss",
    "content": ".plait-board-container {\n    .text {\n        foreignObject {\n            overflow-y: auto;\n            &::-webkit-scrollbar {\n                display: none;\n            }\n            scrollbar-width: none;\n        }\n    }\n}\n\n.plait-text-container {\n    font-size: 14px;\n    min-height: 20px;\n    line-height: 20px;\n    display: block;\n    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Noto Sans', 'Noto Sans CJK SC', 'Microsoft Yahei', 'Hiragino Sans GB', Arial, sans-serif;\n}\n\n.slate-editable-container {\n    outline: none;\n    padding: 0;\n    cursor: default;\n    & [data-slate-node='element'] {\n        user-select: none;\n    }\n    &[contenteditable=\"true\"] {\n        cursor: text;\n        & [data-slate-node='element'] {\n            user-select: text;\n        }\n    }\n    [plait-underlined][plait-strike] {\n        text-decoration: underline line-through;\n    }\n    [plait-strike] {\n        text-decoration: line-through;\n    }\n    [plait-underlined] {\n        text-decoration: underline;\n    }\n    [plait-italic] {\n        font-style: italic;\n    }\n    [plait-bold] {\n        font-weight: bold;\n    }\n    @for $size from 8 through 78 {\n        [plait-font-size='#{$size}'] {\n            font-size: #{$size}px;\n            line-height: 1.5;\n        }\n    }\n}\n"
  },
  {
    "path": "packages/react-text/src/text.spec.tsx",
    "content": "import { render } from '@testing-library/react';\n\nimport { Text } from './text';\nimport { Element } from 'slate';\n\ndescribe('Text', () => {\n  it('should render successfully', () => {\n    // const ele: Element = { children: [{ text: '' }], type: 'paragraph' };\n    // const { baseElement } = render(<Text text={ele} board={{} as any} />);\n    // expect(baseElement).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "packages/react-text/src/text.tsx",
    "content": "import { createEditor, type Descendant, Range, Transforms } from 'slate';\nimport { isKeyHotkey } from 'is-hotkey';\nimport {\n  Editable,\n  RenderElementProps,\n  RenderLeafProps,\n  Slate,\n  withReact,\n} from 'slate-react';\nimport {\n  type CustomElement,\n  type CustomText,\n  type LinkElement,\n  type ParagraphElement,\n  type TextProps,\n} from '@plait/common';\nimport React, { useMemo, useCallback, useEffect, CSSProperties } from 'react';\nimport { withHistory } from 'slate-history';\nimport { isUrl, LinkEditor } from '@plait/text-plugins';\nimport { withText } from './plugins/with-text';\nimport { CustomEditor, RenderElementPropsFor } from './custom-types';\n\nimport './styles/index.scss';\nimport { LinkComponent, withInlineLink } from './plugins/with-link';\n\nexport type TextComponentProps = TextProps;\n\nexport const Text: React.FC<TextComponentProps> = (\n  props: TextComponentProps\n) => {\n  const { text, readonly, onChange, onComposition, afterInit } = props;\n\n  const renderLeaf = useCallback(\n    (props: RenderLeafProps) => <Leaf {...props} />,\n    []\n  );\n\n  const initialValue: Descendant[] = [text];\n\n  const editor = useMemo(() => {\n    const editor = withInlineLink(\n      withText(withHistory(withReact(createEditor())))\n    );\n    afterInit && afterInit(editor);\n    return editor;\n  }, []);\n\n  useEffect(() => {\n    if (text === editor.children[0]) {\n      return;\n    }\n    editor.children = [text];\n    editor.onChange();\n  }, [text, editor]);\n\n  const onKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (event) => {\n    const { selection } = editor;\n\n    // Default left/right behavior is unit:'character'.\n    // This fails to distinguish between two cursor positions, such as\n    // <inline>foo<cursor/></inline> vs <inline>foo</inline><cursor/>.\n    // Here we modify the behavior to unit:'offset'.\n    // This lets the user step into and out of the inline without stepping over characters.\n    // You may wish to customize this further to only use unit:'offset' in specific cases.\n    if (selection && Range.isCollapsed(selection)) {\n      const { nativeEvent } = event;\n      if (isKeyHotkey('left', nativeEvent)) {\n        event.preventDefault();\n        Transforms.move(editor, { unit: 'offset', reverse: true });\n        return;\n      }\n      if (isKeyHotkey('right', nativeEvent)) {\n        event.preventDefault();\n        Transforms.move(editor, { unit: 'offset' });\n        return;\n      }\n    }\n  };\n\n  return (\n    <Slate\n      editor={editor}\n      initialValue={initialValue}\n      onChange={(value: Descendant[]) => {\n        onChange &&\n          onChange({\n            newText: editor.children[0] as ParagraphElement,\n            operations: editor.operations,\n          });\n      }}\n    >\n      <Editable\n        className=\"slate-editable-container plait-text-container\"\n        renderElement={(props) => <Element {...props} />}\n        renderLeaf={renderLeaf}\n        readOnly={readonly === undefined ? true : readonly}\n        onCompositionStart={(event) => {\n          if (onComposition) {\n            onComposition(event as unknown as CompositionEvent);\n          }\n        }}\n        onCompositionUpdate={(event) => {\n          if (onComposition) {\n            onComposition(event as unknown as CompositionEvent);\n          }\n        }}\n        onCompositionEnd={(event) => {\n          if (onComposition) {\n            onComposition(event as unknown as CompositionEvent);\n          }\n        }}\n        onKeyDown={onKeyDown}\n      />\n    </Slate>\n  );\n};\n\nconst Element = (props: RenderElementProps) => {\n  const { attributes, children, element } = props as RenderElementPropsFor<\n    CustomElement & { type: string }\n  >;\n  switch (element.type) {\n    case 'link':\n      return (\n        <LinkComponent {...(props as RenderElementPropsFor<LinkElement>)} />\n      );\n    default:\n      return (\n        <ParagraphComponent\n          {...(props as RenderElementPropsFor<ParagraphElement>)}\n        />\n      );\n  }\n};\n\nconst ParagraphComponent = ({\n  attributes,\n  children,\n  element,\n}: RenderElementPropsFor<ParagraphElement>) => {\n  const style = { textAlign: element.align } as CSSProperties;\n  return (\n    <div style={style} {...attributes}>\n      {children}\n    </div>\n  );\n};\n\nconst Leaf: React.FC<RenderLeafProps> = ({ children, leaf, attributes }) => {\n  if ((leaf as CustomText).bold) {\n    children = <strong>{children}</strong>;\n  }\n\n  if ((leaf as CustomText).code) {\n    children = <code>{children}</code>;\n  }\n\n  if ((leaf as CustomText).italic) {\n    children = <em>{children}</em>;\n  }\n\n  if ((leaf as CustomText).underlined) {\n    children = <u>{children}</u>;\n  }\n\n  const fontSizeValue = (leaf as CustomText)['font-size'];\n  const style: CSSProperties = {\n    color: (leaf as CustomText).color\n  };\n\n  return (\n    <span\n      style={style}\n      {...attributes}\n      {...({ 'plait-font-size': fontSizeValue } as any)}\n    >\n      {children}\n    </span>\n  );\n};\n"
  },
  {
    "path": "packages/react-text/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"jsx\": \"react-jsx\",\n    \"allowJs\": false,\n    \"esModuleInterop\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"types\": [\"vite/client\"]\n  },\n  \"files\": [],\n  \"include\": [],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.lib.json\"\n    },\n    {\n      \"path\": \"./tsconfig.spec.json\"\n    }\n  ],\n  \"extends\": \"../../tsconfig.base.json\"\n}\n"
  },
  {
    "path": "packages/react-text/tsconfig.lib.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../dist/out-tsc\",\n    \"types\": [\n      \"node\",\n      \"@nx/react/typings/cssmodule.d.ts\",\n      \"@nx/react/typings/image.d.ts\",\n      \"vite/client\"\n    ]\n  },\n  \"exclude\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"**/*.spec.tsx\",\n    \"**/*.test.tsx\",\n    \"**/*.spec.js\",\n    \"**/*.test.js\",\n    \"**/*.spec.jsx\",\n    \"**/*.test.jsx\"\n  ],\n  \"include\": [\"src/**/*.js\", \"src/**/*.jsx\", \"src/**/*.ts\", \"src/**/*.tsx\"]\n}\n"
  },
  {
    "path": "packages/react-text/tsconfig.spec.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../dist/out-tsc\",\n    \"module\": \"commonjs\",\n    \"types\": [\"jest\", \"node\"]\n  },\n  \"include\": [\n    \"jest.config.ts\",\n    \"src/**/*.test.ts\",\n    \"src/**/*.spec.ts\",\n    \"src/**/*.test.tsx\",\n    \"src/**/*.spec.tsx\",\n    \"src/**/*.test.js\",\n    \"src/**/*.spec.js\",\n    \"src/**/*.test.jsx\",\n    \"src/**/*.spec.jsx\",\n    \"src/**/*.d.ts\"\n, \"src/text.tsx\"  ]\n}\n"
  },
  {
    "path": "packages/react-text/vite.config.ts",
    "content": "/// <reference types='vitest' />\nimport { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport dts from 'vite-plugin-dts';\nimport * as path from 'path';\nimport { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';\n\nexport default defineConfig({\n  root: __dirname,\n  cacheDir: '../../node_modules/.vite/packages/react-text',\n\n  plugins: [\n    react(),\n    nxViteTsPaths(),\n    dts({\n      entryRoot: 'src',\n      tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'),\n    }),\n  ],\n\n  // Uncomment this if you are using workers.\n  // worker: {\n  //  plugins: [ nxViteTsPaths() ],\n  // },\n\n  // Configuration for building your library.\n  // See: https://vitejs.dev/guide/build.html#library-mode\n  build: {\n    outDir: '../../dist/react-text',\n    emptyOutDir: true,\n    reportCompressedSize: true,\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n    lib: {\n      // Could also be a dictionary or array of multiple entry points.\n      entry: 'src/index.ts',\n      name: 'react-text',\n      fileName: 'index',\n      // Change this to the formats you want to support.\n      // Don't forget to update your package.json as well.\n      formats: ['es', 'cjs'],\n    },\n    rollupOptions: {\n      // External packages that should not be bundled into your library.\n      external: ['react', 'react-dom', 'react/jsx-runtime', 'slate', 'slate-react', 'slate-history', 'is-hotkey', '@plait/text-plugins', '@plait/common'],\n    },\n  },\n  resolve: {\n    alias: {\n      '@plait': path.resolve(__dirname, 'src'), // 根据项目结构调整路径\n      'react-text': path.resolve(__dirname, 'packages/react-text/src'), // 配置 lib 包的别名\n    },\n  },\n});\n"
  },
  {
    "path": "scripts/publish.js",
    "content": "const { execSync } = require('child_process');\nconst path = require('path');\nconst fs = require('fs');\n\nconst libraries = ['react-board', 'react-text', 'drawnix'];\n\nlibraries.forEach(lib => {\n  const libPath = path.resolve(__dirname, '../dist', lib);\n  \n  if (fs.existsSync(libPath)) {\n    const pkgPath = path.join(libPath, 'package.json');\n    let publishCmd = 'npm publish --access public';\n    let versionInfo = '';\n\n    if (fs.existsSync(pkgPath)) {\n      try {\n        const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));\n        const version = pkg.version || '';\n        versionInfo = version ? ` (version ${version})` : '';\n        // 识别带有预发布标识的版本（包含 '-'），使用 next 标签\n        if (typeof version === 'string' && version.includes('-')) {\n          publishCmd += ' --tag next';\n        }\n      } catch (e) {\n        console.warn(`Unable to read version for ${lib}:`, e.message);\n      }\n    } else {\n      console.warn(`package.json not found in ${libPath}`);\n    }\n\n    console.log(`Publishing ${lib}${versionInfo} with: ${publishCmd}`);\n    try {\n      execSync(publishCmd, { \n        cwd: libPath,\n        stdio: 'inherit'\n      });\n      console.log(`Successfully published ${lib}`);\n    } catch (error) {\n      console.error(`Failed to publish ${lib}:`, error);\n    }\n  } else {\n    console.error(`Library path not found: ${libPath}`);\n  }\n});"
  },
  {
    "path": "scripts/release-version.js",
    "content": "const { execSync } = require('child_process');\n\n// Run nx release version\nexecSync('nx release version', { stdio: 'inherit' });\n\n// Get the new version from react-text/package.json\nconst version = require('../packages/react-text/package.json').version;\n\n// Run nx release changelog\nexecSync(`nx release changelog ${version}`, { stdio: 'inherit' });\n\n// Commit all changes with a single commit message\nexecSync('git add .', { stdio: 'inherit' });\nexecSync(`git commit -m \"chore(release): publish ${version}\"`, { stdio: 'inherit' });\n\n// Create a Git tag for the release\nexecSync(`git tag -a v${version} -m \"v${version}\"`, { stdio: 'inherit' });\n"
  },
  {
    "path": "tsconfig.base.json",
    "content": "{\n  \"compileOnSave\": false,\n  \"compilerOptions\": {\n    \"rootDir\": \".\",\n    \"sourceMap\": true,\n    \"declaration\": false,\n    \"moduleResolution\": \"node\",\n    \"emitDecoratorMetadata\": true,\n    \"experimentalDecorators\": true,\n    \"importHelpers\": true,\n    \"target\": \"es2015\",\n    \"module\": \"esnext\",\n    \"lib\": [\"es2020\", \"dom\"],\n    \"skipLibCheck\": true,\n    \"skipDefaultLibCheck\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@drawnix/drawnix\": [\"packages/drawnix/src/index.ts\"],\n      \"@plait-board/react-board\": [\"packages/react-board/src/index.ts\"],\n      \"@plait-board/react-text\": [\"packages/react-text/src/index.ts\"]\n    }\n  },\n  \"exclude\": [\"node_modules\", \"tmp\"]\n}\n"
  }
]