Showing preview only (522K chars total). Download the full file or copy to clipboard to get everything.
Repository: plait-board/drawnix
Branch: develop
Commit: f7b818bee1c2
Files: 216
Total size: 470.8 KB
Directory structure:
gitextract_iabkvbp0/
├── .dockerignore
├── .editorconfig
├── .eslintignore
├── .eslintrc.json
├── .github/
│ └── workflows/
│ ├── ci.yml
│ └── publish.yml
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .vscode/
│ ├── extensions.json
│ └── settings.json
├── CFPAGE-DEPLOY.md
├── CHANGELOG.md
├── Dockerfile
├── LICENSE
├── README.md
├── README_en.md
├── SECURITY.md
├── apps/
│ ├── web/
│ │ ├── .eslintrc.json
│ │ ├── index.html
│ │ ├── jest.config.ts
│ │ ├── project.json
│ │ ├── public/
│ │ │ ├── _headers
│ │ │ ├── _redirects
│ │ │ ├── robots.txt
│ │ │ └── sitemap.xml
│ │ ├── src/
│ │ │ ├── app/
│ │ │ │ ├── app.module.scss
│ │ │ │ ├── app.spec.tsx
│ │ │ │ └── app.tsx
│ │ │ ├── assets/
│ │ │ │ └── .gitkeep
│ │ │ ├── main.tsx
│ │ │ └── styles.scss
│ │ ├── tsconfig.app.json
│ │ ├── tsconfig.json
│ │ ├── tsconfig.spec.json
│ │ └── vite.config.ts
│ └── web-e2e/
│ ├── .eslintrc.json
│ ├── playwright.config.ts
│ ├── project.json
│ ├── src/
│ │ └── example.spec.ts
│ └── tsconfig.json
├── jest.config.ts
├── jest.preset.js
├── nx.json
├── package.json
├── packages/
│ ├── drawnix/
│ │ ├── .babelrc
│ │ ├── .eslintrc.json
│ │ ├── README.md
│ │ ├── jest.config.ts
│ │ ├── package.json
│ │ ├── project.json
│ │ ├── src/
│ │ │ ├── components/
│ │ │ │ ├── arrow-mark-picker.tsx
│ │ │ │ ├── arrow-picker.tsx
│ │ │ │ ├── clean-confirm/
│ │ │ │ │ ├── clean-confirm.scss
│ │ │ │ │ └── clean-confirm.tsx
│ │ │ │ ├── color-picker.scss
│ │ │ │ ├── color-picker.tsx
│ │ │ │ ├── dialog/
│ │ │ │ │ ├── dialog.scss
│ │ │ │ │ └── dialog.tsx
│ │ │ │ ├── icons.tsx
│ │ │ │ ├── island.scss
│ │ │ │ ├── island.tsx
│ │ │ │ ├── menu/
│ │ │ │ │ ├── common.ts
│ │ │ │ │ ├── menu-group.tsx
│ │ │ │ │ ├── menu-item-content-radio.tsx
│ │ │ │ │ ├── menu-item-content.tsx
│ │ │ │ │ ├── menu-item-custom.tsx
│ │ │ │ │ ├── menu-item-link.tsx
│ │ │ │ │ ├── menu-item.tsx
│ │ │ │ │ ├── menu-separator.tsx
│ │ │ │ │ ├── menu.scss
│ │ │ │ │ └── menu.tsx
│ │ │ │ ├── popover/
│ │ │ │ │ └── popover.tsx
│ │ │ │ ├── popup/
│ │ │ │ │ └── link-popup/
│ │ │ │ │ ├── link-popup.scss
│ │ │ │ │ └── link-popup.tsx
│ │ │ │ ├── radio-group.scss
│ │ │ │ ├── radio-group.tsx
│ │ │ │ ├── select/
│ │ │ │ │ ├── select.scss
│ │ │ │ │ └── select.tsx
│ │ │ │ ├── shape-picker.tsx
│ │ │ │ ├── size-slider.scss
│ │ │ │ ├── size-slider.tsx
│ │ │ │ ├── stack.scss
│ │ │ │ ├── stack.tsx
│ │ │ │ ├── tool-button.tsx
│ │ │ │ ├── tool-icon.scss
│ │ │ │ ├── toolbar/
│ │ │ │ │ ├── app-toolbar/
│ │ │ │ │ │ ├── app-menu-items.tsx
│ │ │ │ │ │ ├── app-toolbar.tsx
│ │ │ │ │ │ └── language-switcher-menu.tsx
│ │ │ │ │ ├── creation-toolbar.tsx
│ │ │ │ │ ├── extra-tools/
│ │ │ │ │ │ ├── extra-tools-button.tsx
│ │ │ │ │ │ └── menu-items.tsx
│ │ │ │ │ ├── freehand-panel/
│ │ │ │ │ │ └── freehand-panel.tsx
│ │ │ │ │ ├── pencil-mode-toolbar.tsx
│ │ │ │ │ ├── popup-toolbar/
│ │ │ │ │ │ ├── arrow-mark-button.tsx
│ │ │ │ │ │ ├── fill-button.tsx
│ │ │ │ │ │ ├── font-color-button.tsx
│ │ │ │ │ │ ├── font-size-control.tsx
│ │ │ │ │ │ ├── link-button.tsx
│ │ │ │ │ │ ├── popup-toolbar.scss
│ │ │ │ │ │ ├── popup-toolbar.tsx
│ │ │ │ │ │ └── stroke-button.tsx
│ │ │ │ │ ├── theme-toolbar.tsx
│ │ │ │ │ └── zoom-toolbar.tsx
│ │ │ │ ├── ttd-dialog/
│ │ │ │ │ ├── markdown-to-drawnix.tsx
│ │ │ │ │ ├── mermaid-to-drawnix.scss
│ │ │ │ │ ├── mermaid-to-drawnix.tsx
│ │ │ │ │ ├── ttd-dialog-input.tsx
│ │ │ │ │ ├── ttd-dialog-output.tsx
│ │ │ │ │ ├── ttd-dialog-panel.tsx
│ │ │ │ │ ├── ttd-dialog-panels.tsx
│ │ │ │ │ ├── ttd-dialog-submit-shortcut.tsx
│ │ │ │ │ ├── ttd-dialog.scss
│ │ │ │ │ └── ttd-dialog.tsx
│ │ │ │ ├── tutorial.scss
│ │ │ │ └── tutorial.tsx
│ │ │ ├── constants/
│ │ │ │ └── color.ts
│ │ │ ├── constants.ts
│ │ │ ├── css.d.ts
│ │ │ ├── data/
│ │ │ │ ├── blob.ts
│ │ │ │ ├── filesystem.ts
│ │ │ │ ├── image.ts
│ │ │ │ ├── json.ts
│ │ │ │ └── types.ts
│ │ │ ├── drawnix.spec.tsx
│ │ │ ├── drawnix.tsx
│ │ │ ├── errors.ts
│ │ │ ├── hooks/
│ │ │ │ └── use-drawnix.tsx
│ │ │ ├── i18n/
│ │ │ │ ├── index.tsx
│ │ │ │ ├── translations/
│ │ │ │ │ ├── ar.ts
│ │ │ │ │ ├── en.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── ru.ts
│ │ │ │ │ ├── vi.ts
│ │ │ │ │ └── zh.ts
│ │ │ │ └── types.ts
│ │ │ ├── i18n.tsx
│ │ │ ├── index.ts
│ │ │ ├── keys.ts
│ │ │ ├── libs/
│ │ │ │ └── image-viewer.ts
│ │ │ ├── plugins/
│ │ │ │ ├── components/
│ │ │ │ │ ├── emoji.tsx
│ │ │ │ │ └── image.tsx
│ │ │ │ ├── freehand/
│ │ │ │ │ ├── freehand.component.ts
│ │ │ │ │ ├── freehand.generator.ts
│ │ │ │ │ ├── smoother.ts
│ │ │ │ │ ├── type.ts
│ │ │ │ │ ├── utils.ts
│ │ │ │ │ ├── with-freehand-create.ts
│ │ │ │ │ ├── with-freehand-erase.ts
│ │ │ │ │ ├── with-freehand-fragment.ts
│ │ │ │ │ └── with-freehand.ts
│ │ │ │ ├── with-common.tsx
│ │ │ │ ├── with-hotkey.ts
│ │ │ │ ├── with-image.tsx
│ │ │ │ ├── with-mind-extend.tsx
│ │ │ │ ├── with-pencil.ts
│ │ │ │ └── with-text-link.tsx
│ │ │ ├── styles/
│ │ │ │ ├── index.scss
│ │ │ │ ├── theme.scss
│ │ │ │ └── variables.module.scss
│ │ │ ├── transforms/
│ │ │ │ └── property.ts
│ │ │ ├── types.ts
│ │ │ └── utils/
│ │ │ ├── color.ts
│ │ │ ├── common.ts
│ │ │ ├── image.ts
│ │ │ ├── index.ts
│ │ │ ├── laser-pointer.ts
│ │ │ ├── property.ts
│ │ │ └── utility-types.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.lib.json
│ │ ├── tsconfig.spec.json
│ │ └── vite.config.ts
│ ├── react-board/
│ │ ├── .babelrc
│ │ ├── .eslintrc.json
│ │ ├── README.md
│ │ ├── jest.config.ts
│ │ ├── package.json
│ │ ├── project.json
│ │ ├── src/
│ │ │ ├── board.spec.tsx
│ │ │ ├── board.tsx
│ │ │ ├── hooks/
│ │ │ │ ├── use-board-event.ts
│ │ │ │ ├── use-board.tsx
│ │ │ │ └── use-plugin-event.tsx
│ │ │ ├── index.ts
│ │ │ ├── plugins/
│ │ │ │ ├── board.ts
│ │ │ │ ├── with-pinch-zoom-plugin.ts
│ │ │ │ └── with-react.tsx
│ │ │ ├── styles/
│ │ │ │ ├── index.scss
│ │ │ │ └── mixins.scss
│ │ │ └── wrapper.tsx
│ │ ├── tsconfig.json
│ │ ├── tsconfig.lib.json
│ │ ├── tsconfig.spec.json
│ │ └── vite.config.ts
│ └── react-text/
│ ├── .babelrc
│ ├── .eslintrc.json
│ ├── README.md
│ ├── jest.config.ts
│ ├── package.json
│ ├── project.json
│ ├── src/
│ │ ├── custom-types.ts
│ │ ├── index.ts
│ │ ├── plugins/
│ │ │ ├── index.ts
│ │ │ ├── with-link.tsx
│ │ │ └── with-text.ts
│ │ ├── styles/
│ │ │ └── index.scss
│ │ ├── text.spec.tsx
│ │ └── text.tsx
│ ├── tsconfig.json
│ ├── tsconfig.lib.json
│ ├── tsconfig.spec.json
│ └── vite.config.ts
├── scripts/
│ ├── publish.js
│ └── release-version.js
└── tsconfig.base.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
.nx
.dist
.node_modules
================================================
FILE: .editorconfig
================================================
# Editor configuration, see http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
max_line_length = off
trim_trailing_whitespace = false
================================================
FILE: .eslintignore
================================================
node_modules
================================================
FILE: .eslintrc.json
================================================
{
"root": true,
"ignorePatterns": ["**/*"],
"plugins": ["@nx"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"enforceBuildableLibDependency": true,
"allow": [],
"depConstraints": [
{
"sourceTag": "*",
"onlyDependOnLibsWithTags": ["*"]
}
]
}
]
}
},
{
"files": ["*.ts", "*.tsx"],
"extends": ["plugin:@nx/typescript"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"extends": ["plugin:@nx/javascript"],
"rules": {}
},
{
"files": ["*.spec.ts", "*.spec.tsx", "*.spec.js", "*.spec.jsx"],
"env": {
"jest": true
},
"rules": {}
}
]
}
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
branches:
- main
pull_request:
permissions:
actions: read
contents: read
jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# Connect your workspace on nx.app and uncomment this to enable task distribution.
# The "--stop-agents-after" is optional, but allows idle agents to shut down once the "e2e-ci" targets have been requested
# - run: npx nx-cloud start-ci-run --distribute-on="5 linux-medium-js" --stop-agents-after="e2e-ci"
# Cache node_modules
- uses: actions/setup-node@v3
with:
node-version: 20
cache: 'npm'
- run: npm ci
- uses: nrwl/nx-set-shas@v4
# 安装 Playwright 浏览器和依赖
- name: Install Playwright
run: npx playwright install --with-deps
# Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud
# - run: npx nx-cloud record -- echo Hello World
- run: npx nx affected -t lint test build --base=origin/develop --verbose
- run: npx nx affected --parallel 1 -t e2e-ci --base=origin/develop --verbose
================================================
FILE: .github/workflows/publish.yml
================================================
name: publish
on:
push:
tags:
- "*"
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set TAG
run: echo TAG=${GITHUB_REF#refs/tags/} >> $GITHUB_ENV
- name: Publish ${{ matrix.svc }}
uses: docker/build-push-action@v3
with:
file: Dockerfile
outputs: "type=registry,push=true"
platforms: linux/amd64, linux/arm64
tags: |
ghcr.io/${{ github.repository_owner }}/drawnix:latest
ghcr.io/${{ github.repository_owner }}/drawnix:${{ env.TAG }}
pubuzhixing/drawnix:${{ env.TAG }}
pubuzhixing/drawnix:latest
================================================
FILE: .gitignore
================================================
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
dist
tmp
/out-tsc
# dependencies
node_modules
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db
.nx/cache
.nx/workspace-data
================================================
FILE: .prettierignore
================================================
# Add files here to ignore them from prettier formatting
/dist
/coverage
/.nx/cache
/.nx/workspace-data
================================================
FILE: .prettierrc
================================================
{
"singleQuote": true
}
================================================
FILE: .vscode/extensions.json
================================================
{
"recommendations": [
"nrwl.angular-console",
"esbenp.prettier-vscode",
"ms-playwright.playwright",
"firsttris.vscode-jest-runner"
]
}
================================================
FILE: .vscode/settings.json
================================================
{
"cSpell.words": [
"drawnix"
]
}
================================================
FILE: CFPAGE-DEPLOY.md
================================================
##
### 1. 打开 Cloudflare Pages
访问:https://dash.cloudflare.com/pages
### 2. 创建项目
- 点击 **"Create a project"**
- 选择 **"Connect to Git"**
- 选择您的 GitHub 仓库
### 3. 配置构建设置
在可视化界面中填写:
```
Framework preset: None
Build command: npm run build:web
Build output directory: dist/apps/web
Root directory: (留空)
```
在 **Environment variables** 部分添加:
```
NODE_VERSION = 20
```
### 4. 点击 "Save and Deploy"
就这么简单!
## 项目已包含的配置文件
- `apps/web/public/_redirects` - SPA 路由支持
- `apps/web/public/_headers` - 基本缓存配置
- `package.json` 中的 `build:web` 脚本
## 部署后检查
1. 访问分配的 `.pages.dev` 域名
2. 确认网站正常运行
3. 测试页面刷新是否正常(SPA 路由)
## 自定义域名(可选)
部署成功后,在 Cloudflare Pages 项目中:
1. 点击 **"Custom domains"**
2. 添加您的域名
3. 按提示配置 DNS
就是这么简单!无需复杂的配置文件。
================================================
FILE: CHANGELOG.md
================================================
## 0.4.0-2 (2025-12-05)
This was a version bump only, there were no code changes.
## 0.4.0-1 (2025-12-05)
### 🚀 Features
- improve i18n ([4e44c7b](https://github.com/plait-board/drawnix/commit/4e44c7b))
- **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))
- **i18n:** add Vietnamese translations for UI elements ([892b627](https://github.com/plait-board/drawnix/commit/892b627))
### 🩹 Fixes
- add "babel-plugin-macros": "^3.1.0" to resolve `npm ci` error in ([2940a09](https://github.com/plait-board/drawnix/commit/2940a09))
- add tooltip, fix get wrong percentage, change cursor when not allowed ([#318](https://github.com/plait-board/drawnix/pull/318))
### ❤️ Thank You
- Phạm Viết Nghĩa @vigstudio
- pubuzhixing8 @pubuzhixing8
- Six
## 0.4.0-0 (2025-11-10)
### 🩹 Fixes
- **mind:** fix image scaling issue ([44dd360](https://github.com/plait-board/drawnix/commit/44dd360))
### ❤️ Thank You
- seepine @seepine
## 0.3.3 (2025-10-26)
### 🚀 Features
- handle text editing on touch device ([e4a42d0](https://github.com/plait-board/drawnix/commit/e4a42d0))
### 🩹 Fixes
- 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))
- 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))
- fix moving and selection error since leave out pointerMove, some plugins don't work ([193e193](https://github.com/plait-board/drawnix/commit/193e193))
- improve arrow ([1d111fb](https://github.com/plait-board/drawnix/commit/1d111fb))
- **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))
- **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))
### ❤️ Thank You
- Andy Lu (Lu, Yu-An)
- pubuzhixing8 @pubuzhixing8
## 0.3.2 (2025-10-19)
This was a version bump only, there were no code changes.
## 0.3.1 (2025-10-16)
### 🩹 Fixes
- correct @plait-board/markdown-to-drawnix version ([9ff924e](https://github.com/plait-board/drawnix/commit/9ff924e))
- **hotkey:** move preventDefault() into specific conditional branching ([#303](https://github.com/plait-board/drawnix/pull/303))
### ❤️ Thank You
- pubuzhixing8 @pubuzhixing8
## 0.3.0 (2025-09-13)
### 🚀 Features
- **arrow:** support set arrow mark ([#258](https://github.com/plait-board/drawnix/pull/258))
- **eraser:** implement eraser feature ([#221](https://github.com/plait-board/drawnix/pull/221))
- **eraser:** adding i18n for eraser ([427a730](https://github.com/plait-board/drawnix/commit/427a730))
- **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))
- **eraser:** drawing erasing trail animation effect ([#295](https://github.com/plait-board/drawnix/pull/295))
- **i18n:** added i18n tool for multi-Language support ([#232](https://github.com/plait-board/drawnix/pull/232))
- **i18n:** adding i18n for clean confirm ([7bdf543](https://github.com/plait-board/drawnix/commit/7bdf543))
- **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))
- **i18n:** add Arabic language ([#280](https://github.com/plait-board/drawnix/pull/280))
- **popup-toolbar:** add stroke select state, add stroke type text ([#272](https://github.com/plait-board/drawnix/pull/272))
### 🩹 Fixes
- fix dockerfile build logic ([#201](https://github.com/plait-board/drawnix/pull/201))
- **cursor:** set mind element css to always be inherit ([#260](https://github.com/plait-board/drawnix/pull/260))
- **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))
- **frontend:** comment addDebugLog to prevent potential XSS security issue ([#269](https://github.com/plait-board/drawnix/pull/269))
- **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))
- **hotkey:** prevent enter arrow creation mode when press a and there are some selected elements ([#205](https://github.com/plait-board/drawnix/pull/205))
- **hotkey:** Prevent Arc browser undo on Cmd+Z in Drawnix ([#254](https://github.com/plait-board/drawnix/pull/254))
- **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))
- **menu:** Adding margin for the menu components ([c9ecd09](https://github.com/plait-board/drawnix/commit/c9ecd09))
- **menu:** fix hotkey instruction for every OS ([#274](https://github.com/plait-board/drawnix/pull/274))
- **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))
- **toolbar:** fix issue mentioned in #290 ([#291](https://github.com/plait-board/drawnix/pull/291), [#290](https://github.com/plait-board/drawnix/issues/290))
- **tutorial:** fix tutorial instruction issues and update styles ([#289](https://github.com/plait-board/drawnix/pull/289))
### ❤️ Thank You
- Andy Lu (Lu, Yu-An) @NaoCoding
- coderwei @coderwei99
- MalikAli @MalikAliQassem
- Peter Chen
- pubuzhixing8 @pubuzhixing8
- Six
- vishwak @PATTASWAMY-VISHWAK-YASASHREE
## 0.2.1 (2025-08-06)
### 🩹 Fixes
- **hotkey:** assign t as hotkey to create text element ([#192](https://github.com/plait-board/drawnix/pull/192))
### ❤️ Thank You
- pubuzhixing8 @pubuzhixing8
## 0.2.0 (2025-08-06)
This was a version bump only, there were no code changes.
## 0.1.4 (2025-08-06)
This was a version bump only, there were no code changes.
## 0.1.3 (2025-08-06)
### 🩹 Fixes
- **hotkey:** prevent hotkey when type normally ([#189](https://github.com/plait-board/drawnix/pull/189))
### ❤️ Thank You
- pubuzhixing8 @pubuzhixing8
## 0.1.2 (2025-08-06)
### 🚀 Features
- **creation:** support creation mode hotkey #183 ([#185](https://github.com/plait-board/drawnix/pull/185), [#183](https://github.com/plait-board/drawnix/issues/183))
- **mind:** bump plait into 0.82.0 to improve the experience of mind ([f904594](https://github.com/plait-board/drawnix/commit/f904594))
- **viewer:** support image which in mind node view #125 ([#125](https://github.com/plait-board/drawnix/issues/125))
### ❤️ Thank You
- pubuzhixing8 @pubuzhixing8
## 0.1.1 (2025-07-10)
### 🩹 Fixes
- **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))
### ❤️ Thank You
- pubuzhixing8 @pubuzhixing8
## 0.1.0 (2025-07-02)
### 🚀 Features
- import styles ([ecfe3cd](https://github.com/plait-board/drawnix/commit/ecfe3cd))
- add script and update ci ([147c028](https://github.com/plait-board/drawnix/commit/147c028))
- bump plait into 0.62.0-next.7 ([7ab4003](https://github.com/plait-board/drawnix/commit/7ab4003))
- add main menu ([#14](https://github.com/plait-board/drawnix/pull/14))
- improve active-toolbar ([fd19725](https://github.com/plait-board/drawnix/commit/fd19725))
- rename active-toolbar to popup-toolbar and modify tool-button ([aa06c7e](https://github.com/plait-board/drawnix/commit/aa06c7e))
- support opacity for color property ([#16](https://github.com/plait-board/drawnix/pull/16))
- support local storage ([9c0e652](https://github.com/plait-board/drawnix/commit/9c0e652))
- add product_showcase bump plait into 0.69.0 ([61fe571](https://github.com/plait-board/drawnix/commit/61fe571))
- add sitemap ([3b9d9a3](https://github.com/plait-board/drawnix/commit/3b9d9a3))
- improve pinch zoom ([#77](https://github.com/plait-board/drawnix/pull/77))
- bump plait into 0.76.0 and handle break changes ([#90](https://github.com/plait-board/drawnix/pull/90))
- improve README ([9e0190d](https://github.com/plait-board/drawnix/commit/9e0190d))
- add dependencies for packages ([6d89b32](https://github.com/plait-board/drawnix/commit/6d89b32))
- init dialog and mermaid-to-dialog ([6ff70b9](https://github.com/plait-board/drawnix/commit/6ff70b9))
- support save as json from hotkey ([120dffa](https://github.com/plait-board/drawnix/commit/120dffa))
- support sub menu and export jpg ([#132](https://github.com/plait-board/drawnix/pull/132))
- improve link popup state ([#147](https://github.com/plait-board/drawnix/pull/147))
- improve seo ([#148](https://github.com/plait-board/drawnix/pull/148))
- **active-toolbar:** add active toolbar ([7e737a2](https://github.com/plait-board/drawnix/commit/7e737a2))
- **active-toolbar:** support font color property ([4b2d964](https://github.com/plait-board/drawnix/commit/4b2d964))
- **app:** use localforage to storage main board content #122 ([#122](https://github.com/plait-board/drawnix/issues/122))
- **app-toolbar:** support undo/redo operation ([50f8831](https://github.com/plait-board/drawnix/commit/50f8831))
- **app-toolbar:** add trash and duplicate in app-toolbar ([#28](https://github.com/plait-board/drawnix/pull/28))
- **clean-board:** complete clean board ([#124](https://github.com/plait-board/drawnix/pull/124))
- **clean-confirm:** autoFocus ok button ([582172a](https://github.com/plait-board/drawnix/commit/582172a))
- **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))
- **component:** improve the onXXXChange feature for drawnix component #79 ([#79](https://github.com/plait-board/drawnix/issues/79))
- **component:** add afterInit to expose board instance ([23d91dc](https://github.com/plait-board/drawnix/commit/23d91dc))
- **component:** support update value from drawnix component outside ([#103](https://github.com/plait-board/drawnix/pull/103))
- **component:** fit viewport after children updated ([#104](https://github.com/plait-board/drawnix/pull/104))
- **creation-toolbar:** support long-press triggers drag selection an… ([#78](https://github.com/plait-board/drawnix/pull/78))
- **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))
- **draw:** bump plait into 0.75.0-next.0 and support fine-grained selection ([#69](https://github.com/plait-board/drawnix/pull/69))
- **draw-toolbar:** add draw toolbar ([#9](https://github.com/plait-board/drawnix/pull/9))
- **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))
- **drawnix:** init drawnix package ([397d865](https://github.com/plait-board/drawnix/commit/397d865))
- **drawnix:** export utils ([#105](https://github.com/plait-board/drawnix/pull/105))
- **drawnix-board:** initialize drawnix board ([117e5a8](https://github.com/plait-board/drawnix/commit/117e5a8))
- **fill:** split fill color and fill opacity setting ([#53](https://github.com/plait-board/drawnix/pull/53))
- **flowchart:** add terminal shape element ([#80](https://github.com/plait-board/drawnix/pull/80))
- **freehand:** initialize freehand #2 ([#2](https://github.com/plait-board/drawnix/issues/2))
- **freehand:** apply gaussianSmooth to freehand curve ([#47](https://github.com/plait-board/drawnix/pull/47))
- **freehand:** update stroke width to 2 and optimize freehand end points ([#50](https://github.com/plait-board/drawnix/pull/50))
- **freehand:** improve freehand experience ([#51](https://github.com/plait-board/drawnix/pull/51))
- **freehand:** add FreehandSmoother to optimize freehand curve ([#62](https://github.com/plait-board/drawnix/pull/62))
- **freehand:** optimize freehand curve by stylus features ([#63](https://github.com/plait-board/drawnix/pull/63))
- **freehand:** freehand support theme ([b7c7965](https://github.com/plait-board/drawnix/commit/b7c7965))
- **freehand:** support closed freehand and add popup for freehand ([#68](https://github.com/plait-board/drawnix/pull/68))
- **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))
- **hotkey:** support export png hotkey ([#30](https://github.com/plait-board/drawnix/pull/30))
- **image:** support free image element and support insert image at m… ([#95](https://github.com/plait-board/drawnix/pull/95))
- **image:** should hide popup toolbar when selected element include image ([#96](https://github.com/plait-board/drawnix/pull/96))
- **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))
- **link:** improve link popup ([eba06e2](https://github.com/plait-board/drawnix/commit/eba06e2))
- **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))
- **menu:** support export to json file ([d0d6ca5](https://github.com/plait-board/drawnix/commit/d0d6ca5))
- **menu:** support load file action ([758aa6d](https://github.com/plait-board/drawnix/commit/758aa6d))
- **mermaid:** improve mermaid-to-drawnix ([a928ba1](https://github.com/plait-board/drawnix/commit/a928ba1))
- **mobile:** adapt mobile device ([7c0742f](https://github.com/plait-board/drawnix/commit/7c0742f))
- **pencil-mode:** add pencil mode and add drawnix context ([#76](https://github.com/plait-board/drawnix/pull/76))
- **pinch-zoom:** support pinch zoom for touch device ([#60](https://github.com/plait-board/drawnix/pull/60))
- **pinch-zoom:** improve pinch zoom functionality and support hand moving ([#75](https://github.com/plait-board/drawnix/pull/75))
- **popover:** add reusable popover and replace radix popover ([d30388a](https://github.com/plait-board/drawnix/commit/d30388a))
- **popup:** display icon when color is complete opacity ([#42](https://github.com/plait-board/drawnix/pull/42))
- **popup-toolbar:** support set branch color remove color property when select transparent #17 ([#17](https://github.com/plait-board/drawnix/issues/17))
- **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))
- **property:** support stroke style setting ([463c92a](https://github.com/plait-board/drawnix/commit/463c92a))
- **size-slider:** improve size-slider component ([780be9d](https://github.com/plait-board/drawnix/commit/780be9d))
- **text:** support soft break ([#39](https://github.com/plait-board/drawnix/pull/39))
- **text:** support update text from outside ([#58](https://github.com/plait-board/drawnix/pull/58))
- **text:** support insertSoftBreak for text #136 ([#136](https://github.com/plait-board/drawnix/issues/136))
- **theme-toolbar:** add theme selection toolbar for customizable themes ([dca0e33](https://github.com/plait-board/drawnix/commit/dca0e33))
- **toolbar:** support zoom toolbar ([76ef5d9](https://github.com/plait-board/drawnix/commit/76ef5d9))
- **web:** seo ([84cde4b](https://github.com/plait-board/drawnix/commit/84cde4b))
- **web:** add cloud.umami.is to track views ([#64](https://github.com/plait-board/drawnix/pull/64))
- **web:** modify initialize-data for adding freehand data ([#65](https://github.com/plait-board/drawnix/pull/65))
- **web:** add debug console ([#83](https://github.com/plait-board/drawnix/pull/83))
- **wrapper:** add wrapper component and context hook ([#6](https://github.com/plait-board/drawnix/pull/6))
- **zoom-toolbar:** support zoom menu ([cc6a6b8](https://github.com/plait-board/drawnix/commit/cc6a6b8))
### 🩹 Fixes
- remove theme-toolbar font-weight style ([#67](https://github.com/plait-board/drawnix/pull/67))
- revert package lock ([1aa9d42](https://github.com/plait-board/drawnix/commit/1aa9d42))
- fix pub issue ([156abcb](https://github.com/plait-board/drawnix/commit/156abcb))
- improve libs build ([9ddb6d9](https://github.com/plait-board/drawnix/commit/9ddb6d9))
- **app-toolbar:** correct app-toolbar style ([#106](https://github.com/plait-board/drawnix/pull/106))
- **arrow-line:** optimize the popup toolbar position when selected element is arrow line ([#70](https://github.com/plait-board/drawnix/pull/70))
- **board:** resolve mobile scrolling issue when resize or moving ([8fdca8e](https://github.com/plait-board/drawnix/commit/8fdca8e))
- **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))
- **board:** use updateViewBox to fix board wobbles when dragging or resizing ([#94](https://github.com/plait-board/drawnix/pull/94))
- **color-picker:** support display 0 opacity ([#48](https://github.com/plait-board/drawnix/pull/48))
- **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))
- **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))
- **creation-toolbar:** move out toolbar from board to avoid fired pointer down event when operating ([ddb6092](https://github.com/plait-board/drawnix/commit/ddb6092))
- **font-color:** fix color can not be assigned when current color is empty ([#55](https://github.com/plait-board/drawnix/pull/55))
- **freehand:** fix freehand creation issue(caused by throttleRAF) ([#40](https://github.com/plait-board/drawnix/pull/40))
- **mermaid:** bump mermaid-to-drawnix to 0.0.2 to fix text display issue ([33878d0](https://github.com/plait-board/drawnix/commit/33878d0))
- **mermaid-to-drawnix:** support group for insertToBoard ([e2f5056](https://github.com/plait-board/drawnix/commit/e2f5056))
- **mind:** remove branchColor property setting ([#46](https://github.com/plait-board/drawnix/pull/46))
- **property:** prevent set fill color opacity when color is none ([#56](https://github.com/plait-board/drawnix/pull/56))
- **react-board:** resolve text should not display in safari ([19fc20f](https://github.com/plait-board/drawnix/commit/19fc20f))
- **react-board:** support fit viewport after browser window resized ([96f4a0e](https://github.com/plait-board/drawnix/commit/96f4a0e))
- **size-slider:** correct size slider click handle can not fire ([#57](https://github.com/plait-board/drawnix/pull/57))
- **text:** fix composition input and abc input trembly issue ([#15](https://github.com/plait-board/drawnix/pull/15))
- **text:** resolve with-text build error ([#41](https://github.com/plait-board/drawnix/pull/41))
- **text:** fix text can not editing ([#52](https://github.com/plait-board/drawnix/pull/52))
- **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))
- **text:** allow scroll to show all text ([#156](https://github.com/plait-board/drawnix/pull/156))
- **text:** set whiteSpace: pre to avoid \n is ineffectual ([#165](https://github.com/plait-board/drawnix/pull/165))
- **use-board-event:** fix board event timing ([0d4a8f1](https://github.com/plait-board/drawnix/commit/0d4a8f1))
### ❤️ Thank You
- pubuzhixing8 @pubuzhixing8
## 0.0.4 (2025-04-15)
### 🚀 Features
- ci: a tiny docker image (#127) ([#122](https://github.com/plait-board/drawnix/pull/127))
- support save as json from hotkey ([120dffa](https://github.com/plait-board/drawnix/commit/120dffa))
- **app:** use localforage to storage main board content #122 ([#122](https://github.com/plait-board/drawnix/issues/122))
- **clean-board:** complete clean board ([#124](https://github.com/plait-board/drawnix/pull/124))
### 🩹 Fixes
- **react-board:** support fit viewport after browser window resized ([96f4a0e](https://github.com/plait-board/drawnix/commit/96f4a0e))
### ❤️ Thank You
- lurenyang418 @lurenyang418
- whyour @whyour
- pubuzhixing8 @pubuzhixing8
## 0.0.4-3 (2025-03-25)
### 🩹 Fixes
- improve libs build ([9ddb6d9](https://github.com/plait-board/drawnix/commit/9ddb6d9))
- **mermaid:** bump mermaid-to-drawnix to 0.0.2 to fix text display issue ([33878d0](https://github.com/plait-board/drawnix/commit/33878d0))
### ❤️ Thank You
- pubuzhixing8
## 0.0.4-2 (2025-03-19)
### 🚀 Features
- init dialog and mermaid-to-dialog ([6ff70b9](https://github.com/plait-board/drawnix/commit/6ff70b9))
- **mermaid:** improve mermaid-to-drawnix ([a928ba1](https://github.com/plait-board/drawnix/commit/a928ba1))
### ❤️ Thank You
- pubuzhixing8 @pubuzhixing8
## 0.0.4-1 (2025-03-16)
This was a version bump only, there were no code changes.
## 0.0.4-0 (2025-03-16)
### 🚀 Features
- add dependencies for packages ([6d89b32](https://github.com/plait-board/drawnix/commit/6d89b32))
- **component:** support update value from drawnix component outside ([#103](https://github.com/plait-board/drawnix/pull/103))
- **component:** fit viewport after children updated ([#104](https://github.com/plait-board/drawnix/pull/104))
- **drawnix:** export utils ([#105](https://github.com/plait-board/drawnix/pull/105))
### 🩹 Fixes
- **app-toolbar:** correct app-toolbar style ([#106](https://github.com/plait-board/drawnix/pull/106))
### ❤️ Thank You
- pubuzhixing8 @pubuzhixing8
## 0.0.3 (2025-03-14)
### 🩹 Fixes
- revert package lock ([1aa9d42](https://github.com/plait-board/drawnix/commit/1aa9d42))
- fix pub issue ([156abcb](https://github.com/plait-board/drawnix/commit/156abcb))
- **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))
### ❤️ Thank You
- pubuzhixing8 @pubuzhixing8
## 0.0.2 (2025-03-10)
### 🚀 Features
- improve README ([9e0190d](https://github.com/plait-board/drawnix/commit/9e0190d))
### ❤️ Thank You
- pubuzhixing8 @pubuzhixing8
## 0.0.1 (2025-03-10)
### 🚀 Features
- import styles ([ecfe3cd](https://github.com/plait-board/drawnix/commit/ecfe3cd))
- add script and update ci ([147c028](https://github.com/plait-board/drawnix/commit/147c028))
- bump plait into 0.62.0-next.7 ([7ab4003](https://github.com/plait-board/drawnix/commit/7ab4003))
- add main menu ([#14](https://github.com/plait-board/drawnix/pull/14))
- improve active-toolbar ([fd19725](https://github.com/plait-board/drawnix/commit/fd19725))
- rename active-toolbar to popup-toolbar and modify tool-button ([aa06c7e](https://github.com/plait-board/drawnix/commit/aa06c7e))
- support opacity for color property ([#16](https://github.com/plait-board/drawnix/pull/16))
- support local storage ([9c0e652](https://github.com/plait-board/drawnix/commit/9c0e652))
- add product_showcase bump plait into 0.69.0 ([61fe571](https://github.com/plait-board/drawnix/commit/61fe571))
- add sitemap ([3b9d9a3](https://github.com/plait-board/drawnix/commit/3b9d9a3))
- improve pinch zoom ([#77](https://github.com/plait-board/drawnix/pull/77))
- bump plait into 0.76.0 and handle break changes ([#90](https://github.com/plait-board/drawnix/pull/90))
- **active-toolbar:** add active toolbar ([7e737a2](https://github.com/plait-board/drawnix/commit/7e737a2))
- **active-toolbar:** support font color property ([4b2d964](https://github.com/plait-board/drawnix/commit/4b2d964))
- **app-toolbar:** support undo/redo operation ([50f8831](https://github.com/plait-board/drawnix/commit/50f8831))
- **app-toolbar:** add trash and duplicate in app-toolbar ([#28](https://github.com/plait-board/drawnix/pull/28))
- **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))
- **component:** improve the onXXXChange feature for drawnix component #79 ([#79](https://github.com/plait-board/drawnix/issues/79))
- **component:** add afterInit to expose board instance ([23d91dc](https://github.com/plait-board/drawnix/commit/23d91dc))
- **creation-toolbar:** support long-press triggers drag selection an… ([#78](https://github.com/plait-board/drawnix/pull/78))
- **draw:** bump plait into 0.75.0-next.0 and support fine-grained selection ([#69](https://github.com/plait-board/drawnix/pull/69))
- **draw-toolbar:** add draw toolbar ([#9](https://github.com/plait-board/drawnix/pull/9))
- **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))
- **drawnix:** init drawnix package ([397d865](https://github.com/plait-board/drawnix/commit/397d865))
- **drawnix-board:** initialize drawnix board ([117e5a8](https://github.com/plait-board/drawnix/commit/117e5a8))
- **fill:** split fill color and fill opacity setting ([#53](https://github.com/plait-board/drawnix/pull/53))
- **flowchart:** add terminal shape element ([#80](https://github.com/plait-board/drawnix/pull/80))
- **freehand:** initialize freehand #2 ([#2](https://github.com/plait-board/drawnix/issues/2))
- **freehand:** apply gaussianSmooth to freehand curve ([#47](https://github.com/plait-board/drawnix/pull/47))
- **freehand:** update stroke width to 2 and optimize freehand end points ([#50](https://github.com/plait-board/drawnix/pull/50))
- **freehand:** improve freehand experience ([#51](https://github.com/plait-board/drawnix/pull/51))
- **freehand:** add FreehandSmoother to optimize freehand curve ([#62](https://github.com/plait-board/drawnix/pull/62))
- **freehand:** optimize freehand curve by stylus features ([#63](https://github.com/plait-board/drawnix/pull/63))
- **freehand:** freehand support theme ([b7c7965](https://github.com/plait-board/drawnix/commit/b7c7965))
- **freehand:** support closed freehand and add popup for freehand ([#68](https://github.com/plait-board/drawnix/pull/68))
- **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))
- **hotkey:** support export png hotkey ([#30](https://github.com/plait-board/drawnix/pull/30))
- **image:** support free image element and support insert image at m… ([#95](https://github.com/plait-board/drawnix/pull/95))
- **image:** should hide popup toolbar when selected element include image ([#96](https://github.com/plait-board/drawnix/pull/96))
- **menu:** support export to json file ([d0d6ca5](https://github.com/plait-board/drawnix/commit/d0d6ca5))
- **menu:** support load file action ([758aa6d](https://github.com/plait-board/drawnix/commit/758aa6d))
- **mobile:** adapt mobile device ([7c0742f](https://github.com/plait-board/drawnix/commit/7c0742f))
- **pencil-mode:** add pencil mode and add drawnix context ([#76](https://github.com/plait-board/drawnix/pull/76))
- **pinch-zoom:** support pinch zoom for touch device ([#60](https://github.com/plait-board/drawnix/pull/60))
- **pinch-zoom:** improve pinch zoom functionality and support hand moving ([#75](https://github.com/plait-board/drawnix/pull/75))
- **popover:** add reusable popover and replace radix popover ([d30388a](https://github.com/plait-board/drawnix/commit/d30388a))
- **popup:** display icon when color is complete opacity ([#42](https://github.com/plait-board/drawnix/pull/42))
- **popup-toolbar:** support set branch color remove color property when select transparent #17 ([#17](https://github.com/plait-board/drawnix/issues/17))
- **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))
- **property:** support stroke style setting ([463c92a](https://github.com/plait-board/drawnix/commit/463c92a))
- **size-slider:** improve size-slider component ([780be9d](https://github.com/plait-board/drawnix/commit/780be9d))
- **text:** support soft break ([#39](https://github.com/plait-board/drawnix/pull/39))
- **text:** support update text from outside ([#58](https://github.com/plait-board/drawnix/pull/58))
- **theme-toolbar:** add theme selection toolbar for customizable themes ([dca0e33](https://github.com/plait-board/drawnix/commit/dca0e33))
- **toolbar:** support zoom toolbar ([76ef5d9](https://github.com/plait-board/drawnix/commit/76ef5d9))
- **web:** seo ([84cde4b](https://github.com/plait-board/drawnix/commit/84cde4b))
- **web:** add cloud.umami.is to track views ([#64](https://github.com/plait-board/drawnix/pull/64))
- **web:** modify initialize-data for adding freehand data ([#65](https://github.com/plait-board/drawnix/pull/65))
- **web:** add debug console ([#83](https://github.com/plait-board/drawnix/pull/83))
- **wrapper:** add wrapper component and context hook ([#6](https://github.com/plait-board/drawnix/pull/6))
- **zoom-toolbar:** support zoom menu ([cc6a6b8](https://github.com/plait-board/drawnix/commit/cc6a6b8))
### 🩹 Fixes
- remove theme-toolbar font-weight style ([#67](https://github.com/plait-board/drawnix/pull/67))
- **arrow-line:** optimize the popup toolbar position when selected element is arrow line ([#70](https://github.com/plait-board/drawnix/pull/70))
- **board:** resolve mobile scrolling issue when resize or moving ([8fdca8e](https://github.com/plait-board/drawnix/commit/8fdca8e))
- **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))
- **board:** use updateViewBox to fix board wobbles when dragging or resizing ([#94](https://github.com/plait-board/drawnix/pull/94))
- **color-picker:** support display 0 opacity ([#48](https://github.com/plait-board/drawnix/pull/48))
- **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))
- **creation-toolbar:** move out toolbar from board to avoid fired pointer down event when operating ([ddb6092](https://github.com/plait-board/drawnix/commit/ddb6092))
- **font-color:** fix color can not be assigned when current color is empty ([#55](https://github.com/plait-board/drawnix/pull/55))
- **freehand:** fix freehand creation issue(caused by throttleRAF) ([#40](https://github.com/plait-board/drawnix/pull/40))
- **mind:** remove branchColor property setting ([#46](https://github.com/plait-board/drawnix/pull/46))
- **property:** prevent set fill color opacity when color is none ([#56](https://github.com/plait-board/drawnix/pull/56))
- **react-board:** resolve text should not display in safari ([19fc20f](https://github.com/plait-board/drawnix/commit/19fc20f))
- **size-slider:** correct size slider click handle can not fire ([#57](https://github.com/plait-board/drawnix/pull/57))
- **text:** fix composition input and abc input trembly issue ([#15](https://github.com/plait-board/drawnix/pull/15))
- **text:** resolve with-text build error ([#41](https://github.com/plait-board/drawnix/pull/41))
- **text:** fix text can not editing ([#52](https://github.com/plait-board/drawnix/pull/52))
- **use-board-event:** fix board event timing ([0d4a8f1](https://github.com/plait-board/drawnix/commit/0d4a8f1))
### ❤️ Thank You
- pubuzhixing8 @pubuzhixing8
================================================
FILE: Dockerfile
================================================
FROM node:20 AS builder
WORKDIR /builder
COPY . /builder
RUN npm install \
&& npm run build
FROM lipanski/docker-static-website:2.4.0
WORKDIR /home/static
COPY --from=builder /builder/dist/apps/web/ /home/static
EXPOSE 80
CMD ["/busybox-httpd", "-f", "-v", "-p", "80", "-c", "httpd.conf"]
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2024 Drawnix
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
<p align="center">
<picture style="width: 320px">
<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" />
<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" />
<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" />
</picture>
</p>
<div align="center">
<h2>
开源白板工具(SaaS),一体化白板,包含思维导图、流程图、自由画等
<br />
</h2>
</div>
<div align="center">
<figure>
<a target="_blank" rel="noopener">
<img src="https://github.com/plait-board/drawnix/blob/develop/apps/web/public/product_showcase/case-2.png" alt="Product showcase" width="80%" />
</a>
<figcaption>
<p align="center">
All in one 白板,思维导图、流程图、自由画等
</p>
</figcaption>
</figure>
<a href="https://hellogithub.com/repository/plait-board/drawnix" target="_blank">
<picture style="width: 250">
<source media="(prefers-color-scheme: light)" srcset="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=4dcea807fab7468a962c153b07ae4e4e&claim_uid=zmFSY5k8EuZri43&theme=neutral" />
<source media="(prefers-color-scheme: dark)" srcset="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=4dcea807fab7468a962c153b07ae4e4e&claim_uid=zmFSY5k8EuZri43&theme=dark" />
<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"/>
</picture>
</a>
<br />
<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>
</div>
[*English README*](https://github.com/plait-board/drawnix/blob/develop/README_en.md)
## 特性
- 💯 免费 + 开源
- ⚒️ 思维导图、流程图
- 🖌 画笔
- 😀 插入图片
- 🚀 基于插件机制
- 🖼️ 📃 导出为 PNG, JSON(`.drawnix`)
- 💾 自动保存(浏览器缓存)
- ⚡ 编辑特性:撤销、重做、复制、粘贴等
- 🌌 无限画布:缩放、滚动
- 🎨 主题模式
- 📱 移动设备适配
- 📈 支持 mermaid 语法转流程图
- ✨ 支持 markdown 文本转思维导图(新支持 🔥🔥🔥)
## 关于名称
***Drawnix*** ,源于绘画( ***Draw*** )与凤凰( ***Phoenix*** )的灵感交织。
凤凰象征着生生不息的创造力,而 *Draw* 代表着人类最原始的表达方式。在这里,每一次创作都是一次艺术的涅槃,每一笔绘画都是灵感的重生。
创意如同凤凰,浴火方能重生,而 ***Drawnix*** 要做技术与创意之火的守护者。
*Draw Beyond, Rise Above.*
## 与 Plait 画图框架
*Drawnix* 的定位是一个开箱即用、开源、免费的工具产品,它的底层是 *Plait* 框架,*Plait* 是我司开源的一款画图框架,代表着公司在知识库产品([PingCode Wiki](https://pingcode.com/product/wiki?utm_source=drawnix))上的重要技术沉淀。
Drawnix 是插件架构,与前面说到开源工具比技术架构更复杂一些,但是插件架构也有优势,比如能够支持多种 UI 框架(*Angular、React*),能够集成不同富文本框架(当前仅支持 *Slate* 框架),在开发上可以很好的实现业务的分层,开发各种细粒度的可复用插件,可以扩展更多的画板的应用场景。
## 仓储结构
```
drawnix/
├── apps/
│ ├── web # drawnix.com
│ │ └── index.html # HTML
├── dist/ # 构建产物
├── packages/
│ └── drawnix/ # 白板应用
│ └── react-board/ # 白板 React 视图层
│ └── react-text/ # 文本渲染模块
├── package.json
├── ...
└── README.md
└── README_en.md
```
## 应用
[*https://drawnix.com*](https://drawnix.com) 是 *drawnix* 的最小化应用。
近期会高频迭代 drawnix.com,直到发布 *Dawn(破晓)* 版本。
## 开发
```
npm install
npm run start
```
## Docker
```
docker pull pubuzhixing/drawnix:latest
```
## 依赖
- [plait](https://github.com/worktile/plait) - 开源画图框架
- [slate](https://github.com/ianstormtaylor/slate) - 富文本编辑器框架
- [floating-ui](https://github.com/floating-ui/floating-ui) - 一个超级好用的创建弹出层基础库
## 贡献
欢迎任何形式的贡献:
- 提 Bug
- 贡献代码
## 感谢支持
特别感谢公司对开源项目的大力支持,也感谢为本项目贡献代码、提供建议的朋友。
<p align="left">
<a href="https://pingcode.com?utm_source=drawnix" target="_blank">
<img src="https://cdn-aliyun.pingcode.com/static/site/img/pingcode-logo.4267e7b.svg" width="120" alt="PingCode" />
</a>
</p>
## License
[MIT License](https://github.com/plait-board/drawnix/blob/master/LICENSE)
================================================
FILE: README_en.md
================================================
<p align="center">
<picture style="width: 320px">
<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" />
<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" />
<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" />
</picture>
</p>
<div align="center">
<h2>
Open-source whiteboard tool (SaaS), an all-in-one collaborative canvas that includes mind mapping, flowcharts, freehand and more.
<br />
</h2>
</div>
<div align="center">
<figure>
<a target="_blank" rel="noopener">
<img src="https://github.com/plait-board/drawnix/blob/develop/apps/web/public/product_showcase/case-2.png" alt="Product showcase" width="80%" />
</a>
<figcaption>
<p align="center">
Whiteboard with mind mapping, flowcharts, freehand drawing and more
</p>
</figcaption>
</figure>
<a href="https://hellogithub.com/repository/plait-board/drawnix" target="_blank">
<picture style="width: 250">
<source media="(prefers-color-scheme: light)" srcset="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=4dcea807fab7468a962c153b07ae4e4e&claim_uid=zmFSY5k8EuZri43&theme=neutral" />
<source media="(prefers-color-scheme: dark)" srcset="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=4dcea807fab7468a962c153b07ae4e4e&claim_uid=zmFSY5k8EuZri43&theme=dark" />
<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"/>
</picture>
</a>
<br />
<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>
</div>
[*中文*](https://github.com/plait-board/drawnix/blob/develop/README.md)
## Features
- 💯 Free and Open Source
- ⚒️ Mind Maps and Flowcharts
- 🖌 Freehand
- 😀 Image Support
- 🚀 Plugin-based Architecture - Extensible
- 🖼️ 📃 Export to PNG, JPG, JSON(.drawnix)
- 💾 Auto-save (Browser Storage)
- ⚡ Edit Features: Undo, Redo, Copy, Paste, etc.
- 🌌 Infinite Canvas: Zoom, Pan
- 🎨 Theme Support
- 📱 Mobile-friendly
- 📈 Support mermaid syntax conversion to flowchart
- ✨ Support markdown text conversion to mind map(New 🔥🔥🔥)
## About the Name
***Drawnix*** is born from the interweaving of ***Draw*** and ***Phoenix***, a fusion of artistic inspiration.
The *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.
Like a Phoenix, creativity must rise from the flames to be reborn, and ***Drawnix*** stands as the guardian of both technical and creative fire.
*Draw Beyond, Rise Above.*
## About Plait Drawing Framework
*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)).
*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.
## Repository Structure
```
drawnix/
├── apps/
│ ├── web # drawnix.com
│ │ └── index.html # HTML
├── dist/ # Build artifacts
├── packages/
│ └── drawnix/ # Whiteboard application core
│ └── react-board/ # Whiteboard react view layer
│ └── react-text/ # Text rendering module
├── package.json
├── ...
└── README.md
└── README_en.md
```
## Try It Out
*https://drawnix.com* is the minimal application of *drawnix*.
I will be iterating frequently on *drawnix.com* until the release of the *Dawn* version.
## Development
```
npm install
npm run start
```
## Docker
```
docker pull pubuzhixing/drawnix:latest
```
## Dependencies
- [plait](https://github.com/worktile/plait) - Open source drawing framework
- [slate](https://github.com/ianstormtaylor/slate) - Rich text editor framework
- [floating-ui](https://github.com/floating-ui/floating-ui) - An awesome library for creating floating UI elements
## Contributing
Any form of contribution is welcome:
- Report bugs
- Contribute code
## Thank you for supporting
Special 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.
<p align="left">
<a href="https://pingcode.com?utm_source=drawnix" target="_blank">
<img src="https://cdn-aliyun.pingcode.com/static/site/img/pingcode-logo.4267e7b.svg" width="120" alt="PingCode" />
</a>
</p>
## License
[MIT License](https://github.com/plait-board/drawnix/blob/master/LICENSE)
================================================
FILE: SECURITY.md
================================================
# Security Policy
## Reporting a Vulnerability
We have an official discord server for discussing and reporting about Drawnix.
Please contact pubuzhixing in the server if the valnerability is confidential and critical.
[Discord Server Link](https://discord.gg/5d9undgnsP)
================================================
FILE: apps/web/.eslintrc.json
================================================
{
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
================================================
FILE: apps/web/index.html
================================================
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<!-- 基本 SEO Meta 标签 (中文) -->
<title>Drawnix - 开源白板工具</title>
<meta name="description" content="Drawnix 是一款强大的开源白板工具(https://github.com/plait-board/drawnix),集成思维导图、流程图等功能。基于 Plait 框架开发,支持插件扩展,提供自动保存、无限画布等特性。Draw Beyond, Rise Above.">
<meta name="keywords" content="Drawnix,白板工具,白板,思维导图,流程图,开源白板,开源思维导图,在线绘图,在线白板,协作工具,协作白板,Plait 框架">
<!-- 基本 SEO Meta 标签 (English) -->
<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.">
<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">
<!-- 基本 SEO Meta 标签 (Русский) -->
<meta name="description" lang="ru" content="Drawnix — это виртуальная доска с открытым исходным кодом, позволяющая строить mind-карты и блок-схемы. Написанный на основе фреймворка Plait, он имеет возможности расширения плагинами, автосохранения, бесконечного холта и многое другое. Draw Beyond, Rise Above.">
<meta name="keywords" lang="ru" content="Drawnix,доска,виртуальная доска,mind-карты,интеллект-карты,карты мыслей,блоксхемы,блок-схемы,open source доска,доска с открытым кодом,open source mind-карты,mind-карты с открытым кодом,онлайн-рисование,рисовалка онлайн,совместная работа,доска для совместной работы,электронная доска для совместной работы,Plait framework,фреймворк Plait">
<!-- Open Graph Meta 标签 (中文) -->
<meta property="og:title" content="Drawnix - 开源白板工具 | 思维导图 | 流程图 | 白板 | 协作白板">
<meta property="og:description" content="一体化开源白板工具(在线白板 | 协作白板),支持思维导图、流程图,基于 Plait 框架开发">
<meta property="og:type" content="website">
<meta property="og:url" content="https://drawnix.com">
<meta property="og:site_name" content="Drawnix">
<!-- Open Graph Meta 标签 (English) -->
<meta property="og:title" lang="en" content="Drawnix - Open Source Whiteboard | Mind Mapping | Flowchart | Whiteboard | Collaboration Whiteboard">
<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">
<!-- Open Graph Meta 标签 (Русский) -->
<meta property="og:title" lang="ru" content="Drawnix - Доска с открытым кодом | Mind-карты | Блок-схемы | Электронная доска для совместной работы">
<meta property="og:description" lang="ru" content="Интегрированная доска с открытым исходным кодом (онлайн доска | доска для совеместной работы), поддерживающая создание mind-карт и блок-схем, построенная на основе фреймворка Plait">
<!-- Twitter Card Meta 标签 (English) -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Drawnix - Open Source Whiteboard | Mind Mapping | Flowchart | Whiteboard | Collaboration Whiteboard">
<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">
<!-- 其他重要 Meta 标签 -->
<meta name="robots" content="index, follow">
<meta name="author" content="Drawnix Team">
<link rel="canonical" href="https://drawnix.com">
<!-- 语言替代链接 -->
<link rel="alternate" hreflang="zh-CN" href="https://drawnix.com">
<!-- <link rel="alternate" hreflang="en" href="https://drawnix.com/en"> -->
<!-- <link rel="alternate" hreflang="ru" href="https://drawnix.com/ru"> -->
<link rel="alternate" hreflang="x-default" href="https://drawnix.com">
<!-- 结构化数据 (JSON-LD) - 中英双语 -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Drawnix",
"alternateName": ["开源白板工具", "Open Source Whiteboard Tool"],
"description": {
"zh-CN": "Drawnix 是一款强大的开源白板工具,集成思维导图、流程图等功能。基于 Plait 框架开发,支持插件扩展,提供自动保存、无限画布等特性。",
"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.",
"ru": "Drawnix — это мощная виртуальная доска с открытым исходным кодом, позволяющая строить mind-карты и блок-схемы. Написанный на основе фреймворка Plait, он имеет возможности расширения плагинами, автосохранения, бесконечного холта и многое другое."
},
"url": "https://drawnix.com",
"applicationCategory": "DesignApplication",
"operatingSystem": "Any",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"creator": {
"@type": "Organization",
"name": "Drawnix Team"
},
"keywords": ["доска", "электронная доска", "mind-карты", "блок-схемы", "диаграммы", "whiteboard", "mind mapping", "flowchart", "open source", "思维导图", "流程图", "白板"],
"inLanguage": ["zh-CN", "en", "ru"],
"license": "https://github.com/plait-board/drawnix/blob/master/LICENSE"
}
</script>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="stylesheet" href="/src/styles.scss" />
<script defer src="https://cloud.umami.is/script.js" data-website-id="7083aa92-85b1-4a67-a6d4-03d52819ba3d"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
================================================
FILE: apps/web/jest.config.ts
================================================
/* eslint-disable */
export default {
displayName: 'web',
preset: '../../jest.preset.js',
transform: {
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/apps/web',
};
================================================
FILE: apps/web/project.json
================================================
{
"name": "web",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/web/src",
"projectType": "application",
"tags": [],
"// targets": "to see all targets run: nx show project web --web",
"targets": {}
}
================================================
FILE: apps/web/public/_headers
================================================
# 基本缓存配置
/*
Cache-Control: public, max-age=31536000, immutable
/*.html
Cache-Control: public, max-age=0, must-revalidate
/
Cache-Control: public, max-age=0, must-revalidate
================================================
FILE: apps/web/public/_redirects
================================================
# SPA 路由支持
/* /index.html 200
================================================
FILE: apps/web/public/robots.txt
================================================
id: robots-txt
name: Robots.txt
type: code.txt
content: |-
User-agent: *
Allow: /
# 禁止访问管理后台
Disallow: /admin/
Disallow: /private/
# 站点地图
Sitemap: https://drawnix.com/sitemap.xml
================================================
FILE: apps/web/public/sitemap.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<url>
<loc>https://drawnix.com/</loc>
<lastmod>2024-11-15</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://drawnix.com/en</loc>
<lastmod>2024-11-15</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://drawnix.com/docs</loc>
<lastmod>2024-11-15</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://drawnix.com/docs/getting-started</loc>
<lastmod>2024-11-15</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
</urlset>
================================================
FILE: apps/web/src/app/app.module.scss
================================================
/* Your styles goes here. */
================================================
FILE: apps/web/src/app/app.spec.tsx
================================================
import { render } from '@testing-library/react';
import App from './app';
describe('App', () => {
it('should render successfully', () => {
// const { baseElement } = render(<App />);
// expect(baseElement).toBeTruthy();
});
});
================================================
FILE: apps/web/src/app/app.tsx
================================================
import { useState, useEffect } from 'react';
import { Drawnix } from '@drawnix/drawnix';
import { PlaitBoard, PlaitElement, PlaitTheme, Viewport } from '@plait/core';
import localforage from 'localforage';
type AppValue = {
children: PlaitElement[];
viewport?: Viewport;
theme?: PlaitTheme;
};
const MAIN_BOARD_CONTENT_KEY = 'main_board_content';
localforage.config({
name: 'Drawnix',
storeName: 'drawnix_store',
driver: [localforage.INDEXEDDB, localforage.LOCALSTORAGE],
});
export function App() {
const [value, setValue] = useState<AppValue>({ children: [] });
const [tutorial, setTutorial] = useState(false);
useEffect(() => {
const loadData = async () => {
const storedData = (await localforage.getItem(
MAIN_BOARD_CONTENT_KEY
)) as AppValue;
if (storedData) {
setValue(storedData);
if (storedData.children && storedData.children.length === 0) {
setTutorial(true);
}
return;
}
setTutorial(true);
};
loadData();
}, []);
return (
<Drawnix
value={value.children}
viewport={value.viewport}
theme={value.theme}
onChange={(value) => {
const newValue = value as AppValue;
localforage.setItem(MAIN_BOARD_CONTENT_KEY, newValue);
setValue(newValue);
if (newValue.children && newValue.children.length > 0) {
setTutorial(false);
}
}}
tutorial={tutorial}
afterInit={(board) => {
console.log('board initialized');
// console.log(
// `add __drawnix__web__debug_log to window, so you can call add log anywhere, like: window.__drawnix__web__console('some thing')`
// );
// (window as any)['__drawnix__web__console'] = (value: string) => {
// addDebugLog(board, value);
// };
}}
></Drawnix>
);
}
const addDebugLog = (board: PlaitBoard, value: string) => {
const container = PlaitBoard.getBoardContainer(board).closest(
'.drawnix'
) as HTMLElement;
let consoleContainer = container.querySelector('.drawnix-console');
if (!consoleContainer) {
consoleContainer = document.createElement('div');
consoleContainer.classList.add('drawnix-console');
container.append(consoleContainer);
}
const div = document.createElement('div');
div.innerHTML = value;
consoleContainer.append(div);
};
export default App;
================================================
FILE: apps/web/src/assets/.gitkeep
================================================
================================================
FILE: apps/web/src/main.tsx
================================================
import { StrictMode } from 'react';
import * as ReactDOM from 'react-dom/client';
import App from './app/app';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<StrictMode>
<App />
</StrictMode>
);
================================================
FILE: apps/web/src/styles.scss
================================================
/* You can add global styles to this file, and also import other style files */
body {
margin: 0;
padding: 0;
}
html,
body {
height: 100%;
width: 100%;
overflow: hidden;
}
#root {
height: 100%;
width: 100%;
overflow: hidden;
}
.drawnix-console {
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 0;
height: 200px;
width: 100px;
overflow: auto;
background-color: black;
color: white;
padding: 8px;
opacity: 0.5;
}
================================================
FILE: apps/web/tsconfig.app.json
================================================
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": [
"node",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts",
"vite/client"
]
},
"exclude": [
"src/**/*.spec.ts",
"src/**/*.test.ts",
"src/**/*.spec.tsx",
"src/**/*.test.tsx",
"src/**/*.spec.js",
"src/**/*.test.js",
"src/**/*.spec.jsx",
"src/**/*.test.jsx"
],
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
}
================================================
FILE: apps/web/tsconfig.json
================================================
{
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"types": ["vite/client"]
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../tsconfig.base.json"
}
================================================
FILE: apps/web/tsconfig.spec.json
================================================
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": [
"jest",
"node",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts"
]
},
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
]
}
================================================
FILE: apps/web/vite.config.ts
================================================
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
root: __dirname,
cacheDir: '../../node_modules/.vite/apps/web',
server: {
port: 7200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
plugins: [react(), nxViteTsPaths()],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
build: {
outDir: '../../dist/apps/web',
emptyOutDir: true,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
},
});
================================================
FILE: apps/web-e2e/.eslintrc.json
================================================
{
"extends": ["plugin:playwright/recommended", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
},
{
"files": ["src/**/*.{ts,js,tsx,jsx}"],
"rules": {}
}
]
}
================================================
FILE: apps/web-e2e/playwright.config.ts
================================================
import { defineConfig, devices } from '@playwright/test';
import { nxE2EPreset } from '@nx/playwright/preset';
import { workspaceRoot } from '@nx/devkit';
// For CI, you may want to set BASE_URL to the deployed application.
const baseURL = process.env['BASE_URL'] || 'http://localhost:7200';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
...nxE2EPreset(__filename, { testDir: './src' }),
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
baseURL,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Run your local dev server before starting the tests */
webServer: {
command: 'npx nx serve web',
url: 'http://localhost:7200',
reuseExistingServer: !process.env.CI,
cwd: workspaceRoot,
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// Uncomment for mobile browsers support
/* {
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
}, */
// Uncomment for branded browsers
/* {
name: 'Microsoft Edge',
use: { ...devices['Desktop Edge'], channel: 'msedge' },
},
{
name: 'Google Chrome',
use: { ...devices['Desktop Chrome'], channel: 'chrome' },
}, */
],
});
================================================
FILE: apps/web-e2e/project.json
================================================
{
"name": "web-e2e",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"sourceRoot": "apps/web-e2e/src",
"implicitDependencies": ["web"],
"// targets": "to see all targets run: nx show project web-e2e --web",
"targets": {}
}
================================================
FILE: apps/web-e2e/src/example.spec.ts
================================================
import { test, expect } from '@playwright/test';
test('has title', async ({ page }) => {
await page.goto('/');
// Expect h1 to contain a substring.
expect(await page.title()).toContain('Drawnix - 开源白板工具');
expect(page.locator('drawnix')).toBeTruthy();
});
================================================
FILE: apps/web-e2e/tsconfig.json
================================================
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"allowJs": true,
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"sourceMap": false
},
"include": [
"**/*.ts",
"**/*.js",
"playwright.config.ts",
"src/**/*.spec.ts",
"src/**/*.spec.js",
"src/**/*.test.ts",
"src/**/*.test.js",
"src/**/*.d.ts"
]
}
================================================
FILE: jest.config.ts
================================================
import { getJestProjectsAsync } from '@nx/jest';
export default async () => ({
projects: await getJestProjectsAsync(),
});
================================================
FILE: jest.preset.js
================================================
const nxPreset = require('@nx/jest/preset').default;
module.exports = { ...nxPreset };
================================================
FILE: nx.json
================================================
{
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"namedInputs": {
"default": [
"{projectRoot}/**/*",
"sharedGlobals"
],
"production": [
"default",
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
"!{projectRoot}/tsconfig.spec.json",
"!{projectRoot}/.eslintrc.json",
"!{projectRoot}/eslint.config.js",
"!{projectRoot}/jest.config.[jt]s",
"!{projectRoot}/src/test-setup.[jt]s",
"!{projectRoot}/test-setup.[jt]s"
],
"sharedGlobals": []
},
"plugins": [
{
"plugin": "@nx/vite/plugin",
"options": {
"buildTargetName": "build",
"testTargetName": "test",
"serveTargetName": "serve",
"previewTargetName": "preview",
"serveStaticTargetName": "serve-static"
}
},
{
"plugin": "@nx/eslint/plugin",
"options": {
"targetName": "lint"
}
},
{
"plugin": "@nx/playwright/plugin",
"options": {
"targetName": "e2e"
}
},
{
"plugin": "@nx/jest/plugin",
"options": {
"targetName": "test"
}
}
],
"generators": {
"@nx/react": {
"application": {
"babel": true,
"style": "scss",
"linter": "eslint",
"bundler": "vite"
},
"component": {
"style": "scss"
},
"library": {
"style": "scss",
"linter": "eslint",
"unitTestRunner": "jest"
}
}
},
"release": {
"changelog": {
"workspaceChangelog": true,
"file": "CHANGELOG.md",
"git": {
"commit": false,
"tag": false
}
},
"version": {
"git": {
"commit": false,
"tag": false
}
}
}
}
================================================
FILE: package.json
================================================
{
"name": "@drawnix/source",
"version": "0.0.2",
"license": "MIT",
"scripts": {
"start": "nx serve web --host=0.0.0.0",
"build": "nx run-many -t=build",
"lint": "nx run-many --target=lint --all --fix",
"build:web": "nx build web",
"test": "nx run-many -t=test",
"release": "node scripts/release-version.js",
"pub": "npm run build && node scripts/publish.js"
},
"private": true,
"dependencies": {
"@floating-ui/react": "^0.26.24",
"@plait-board/markdown-to-drawnix": "^0.0.8",
"@plait-board/mermaid-to-drawnix": "^0.0.7",
"@plait/common": "^0.92.1",
"@plait/core": "^0.92.1",
"@plait/draw": "^0.92.1",
"@plait/layouts": "^0.92.1",
"@plait/mind": "^0.92.1",
"@plait/text-plugins": "^0.92.1",
"@types/lodash": "^4.17.21",
"ahooks": "^3.9.6",
"browser-fs-access": "^0.35.0",
"classnames": "^2.5.1",
"is-hotkey": "^0.2.0",
"laser-pen": "^1.0.1",
"localforage": "^1.10.0",
"lodash": "^4.17.21",
"mobile-detect": "^1.4.5",
"open-color": "^1.9.1",
"react": "19.2.0",
"react-dom": "19.2.0",
"roughjs": "^4.6.6",
"slate": "^0.116.0",
"slate-dom": "^0.116.0",
"slate-history": "^0.115.0",
"slate-react": "^0.116.0",
"tslib": "^2.3.0"
},
"devDependencies": {
"@babel/core": "^7.14.5",
"@babel/preset-react": "^7.14.5",
"@nx/cypress": "19.3.0",
"@nx/devkit": "19.3.0",
"@nx/eslint": "19.3.0",
"@nx/eslint-plugin": "19.3.0",
"@nx/jest": "19.3.0",
"@nx/js": "19.3.0",
"@nx/playwright": "19.3.0",
"@nx/react": "19.3.0",
"@nx/vite": "^20.6.0",
"@nx/web": "19.3.0",
"@nx/workspace": "19.3.0",
"@playwright/test": "^1.36.0",
"@swc-node/register": "~1.9.1",
"@swc/cli": "^0.6.0",
"@swc/core": "~1.5.7",
"@swc/helpers": "~0.5.11",
"@testing-library/react": "16.3.0",
"@types/is-hotkey": "^0.1.10",
"@types/jest": "^29.4.0",
"@types/node": "18.16.9",
"@types/react": "18.3.1",
"@types/react-dom": "18.3.0",
"@typescript-eslint/eslint-plugin": "^7.3.0",
"@typescript-eslint/parser": "^7.3.0",
"@vitejs/plugin-react": "^4.2.0",
"@vitest/ui": "^3.0.8",
"babel-jest": "^29.4.1",
"babel-plugin-macros": "^3.1.0",
"eslint": "~8.57.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-jsx-a11y": "6.7.1",
"eslint-plugin-playwright": "^0.15.3",
"eslint-plugin-react": "7.32.2",
"eslint-plugin-react-hooks": "4.6.0",
"jest": "^29.4.1",
"jest-environment-jsdom": "^29.4.1",
"jsdom": "~22.1.0",
"nx": "19.3.0",
"prettier": "^2.6.2",
"sass": "^1.55.0",
"ts-jest": "^29.1.0",
"ts-node": "10.9.1",
"typescript": "~5.4.2",
"vite": "^6.2.2",
"vite-plugin-dts": "^4.5.3",
"vitest": "^3.0.8"
}
}
================================================
FILE: packages/drawnix/.babelrc
================================================
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
================================================
FILE: packages/drawnix/.eslintrc.json
================================================
{
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
================================================
FILE: packages/drawnix/README.md
================================================
# drawnix
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test drawnix` to execute the unit tests via [Vitest](https://vitest.dev/).
================================================
FILE: packages/drawnix/jest.config.ts
================================================
/* eslint-disable */
export default {
displayName: 'drawnix',
preset: '../../jest.preset.js',
transform: {
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/packages/drawnix',
};
================================================
FILE: packages/drawnix/package.json
================================================
{
"name": "@drawnix/drawnix",
"version": "0.4.0-2",
"main": "./index.js",
"types": "./index.d.ts",
"private": false,
"dependencies": {
"@floating-ui/react": "^0.26.24",
"mobile-detect": "^1.4.5",
"open-color": "^1.9.1",
"@plait-board/mermaid-to-drawnix": "^0.0.7",
"@plait-board/markdown-to-drawnix": "^0.0.8",
"laser-pen": "^1.0.1"
},
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.js",
"types": "./index.d.ts"
},
"./index.css": "./index.css"
}
}
================================================
FILE: packages/drawnix/project.json
================================================
{
"name": "drawnix",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/drawnix/src",
"projectType": "library",
"tags": [],
"// targets": "to see all targets run: nx show project drawnix --web",
"targets": {}
}
================================================
FILE: packages/drawnix/src/components/arrow-mark-picker.tsx
================================================
import classNames from 'classnames';
import { Island } from './island';
import Stack from './stack';
import { ToolButton } from './tool-button';
import { LineIcon, ArrowIcon } from './icons';
import { useBoard } from '@plait-board/react-board';
import { ATTACHED_ELEMENT_CLASS_NAME } from '@plait/core';
import React from 'react';
import { PropertyTransforms } from '@plait/common';
import { ArrowLineHandle } from '@plait/draw';
import { useI18n } from '../i18n';
export type ArrowMarkerPickerProps = {
end: 'source' | 'target';
property: ArrowLineHandle;
};
export const ArrowMarkerPicker: React.FC<ArrowMarkerPickerProps> = ({
end,
property,
}) => {
const board = useBoard();
const { marker: currentMarker } = property;
const { t } = useI18n();
const setMarker = (marker: string) => {
PropertyTransforms.setProperty(board, {
[end]: {
...property,
marker,
},
});
};
return (
<Island
padding={2}
className={classNames(
`${ATTACHED_ELEMENT_CLASS_NAME} ${
end === 'source' ? 'source-arrow-island' : ''
} `
)}
>
<Stack.Row gap={1}>
<ToolButton
className={classNames(`property-button`)}
visible={true}
icon={LineIcon}
type="button"
title={t('line.none')}
aria-label={t('line.none')}
selected={currentMarker === 'none'}
onPointerUp={() => {
setMarker('none');
}}
></ToolButton>
<ToolButton
className={classNames(`property-button`)}
visible={true}
icon={ArrowIcon}
type="button"
title={t('line.arrow')}
aria-label={t('line.arrow')}
selected={currentMarker === 'arrow'}
onPointerUp={() => {
setMarker('arrow');
}}
></ToolButton>
</Stack.Row>
</Island>
);
};
================================================
FILE: packages/drawnix/src/components/arrow-picker.tsx
================================================
import classNames from 'classnames';
import { Island } from './island';
import Stack from './stack';
import { ToolButton } from './tool-button';
import { StraightArrowIcon, ElbowArrowIcon, CurveArrowIcon } from './icons';
import { useBoard } from '@plait-board/react-board';
import { Translations, useI18n } from '../i18n';
import { BoardTransforms , PlaitBoard } from '@plait/core';
import React from 'react';
import { BoardCreationMode, setCreationMode } from '@plait/common';
import { ArrowLineShape, DrawPointerType } from '@plait/draw';
export interface ArrowProps {
icon: React.ReactNode;
title: string;
pointer: ArrowLineShape;
}
export const ARROWS: ArrowProps[] = [
{
icon: StraightArrowIcon,
title: 'toolbar.arrow.straight',
pointer: ArrowLineShape.straight,
},
{
icon: ElbowArrowIcon,
title: 'toolbar.arrow.elbow',
pointer: ArrowLineShape.elbow,
},
{
icon: CurveArrowIcon,
title: 'toolbar.arrow.curve',
pointer: ArrowLineShape.curve,
},
];
export type ArrowPickerProps = {
onPointerUp: (pointer: DrawPointerType) => void;
};
export const ArrowPicker: React.FC<ArrowPickerProps> = ({ onPointerUp }) => {
const board = useBoard();
const { t } = useI18n();
return (
<Island padding={1}>
<Stack.Row gap={1}>
{ARROWS.map((arrow, index) => {
return (
<ToolButton
key={index}
className={classNames({ fillable: false })}
type="icon"
size={'small'}
visible={true}
selected={PlaitBoard.isPointer(board, arrow.pointer)}
icon={arrow.icon}
title={t(arrow.title as keyof Translations)}
aria-label={t(arrow.title as keyof Translations)}
onPointerDown={() => {
setCreationMode(board, BoardCreationMode.drawing);
BoardTransforms.updatePointerType(board, arrow.pointer);
}}
onPointerUp={() => {
onPointerUp(arrow.pointer);
}}
/>
);
})}
</Stack.Row>
</Island>
);
};
================================================
FILE: packages/drawnix/src/components/clean-confirm/clean-confirm.scss
================================================
.clean-confirm {
background: white;
border-radius: 8px;
padding: 20px;
width: 300px;
&__title {
font-size: 18px;
font-weight: 500;
margin: 0 0 8px;
}
&__description {
color: #666;
font-size: 14px;
margin: 0 0 20px;
}
&__actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
&__button {
padding: 8px 16px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
border: none;
&--cancel {
background: #f5f5f5;
color: #000;
&:hover {
background: #e8e8e8;
}
}
&--ok {
background: white;
color: #ff4d4f;
border: 1px solid #ff4d4f;
&:hover {
color: white;
background: #ff4d4f;
}
}
}
}
================================================
FILE: packages/drawnix/src/components/clean-confirm/clean-confirm.tsx
================================================
import { Dialog, DialogContent } from '../dialog/dialog';
import { useDrawnix } from '../../hooks/use-drawnix';
import './clean-confirm.scss';
import { useBoard } from '@plait-board/react-board';
import { useI18n } from '../../i18n';
export const CleanConfirm = ({
container,
}: {
container: HTMLElement | null;
}) => {
const { appState, setAppState } = useDrawnix();
const { t } = useI18n();
const board = useBoard();
return (
<Dialog
open={appState.openCleanConfirm}
onOpenChange={(open) => {
setAppState({ ...appState, openCleanConfirm: open });
}}
>
<DialogContent className="clean-confirm" container={container}>
<h2 className="clean-confirm__title">{t('cleanConfirm.title')}</h2>
<p className="clean-confirm__description">
{t('cleanConfirm.description')}
</p>
<div className="clean-confirm__actions">
<button
className="clean-confirm__button clean-confirm__button--cancel"
onClick={() => {
setAppState({ ...appState, openCleanConfirm: false });
}}
>
{t('cleanConfirm.cancel')}
</button>
<button
className="clean-confirm__button clean-confirm__button--ok"
autoFocus
onClick={() => {
board.deleteFragment(board.children);
setAppState({ ...appState, openCleanConfirm: false });
}}
>
{t('cleanConfirm.ok')}
</button>
</div>
</DialogContent>
</Dialog>
);
};
================================================
FILE: packages/drawnix/src/components/color-picker.scss
================================================
@import "open-color/open-color.scss";
.color-select-item {
width: var(--default-button-size);
height: var(--default-button-size);
border-radius: 50%;
display: inline-flex;
justify-content: center;
align-items: center;
border: 1px solid var(--color-gray-30);
cursor: pointer;
padding: 0;
&.active {
border-color: var(--color-primary);
&.no-color {
.selected-icon {
background-color: $oc-white;
}
}
}
.selected-icon {
stroke: currentColor;
outline: none;
position: absolute;
width: var(--default-icon-size);
height: var(--default-icon-size);
}
&.no-color {
border: none;
.no-color-icon {
display: block;
width: var(-default-button-size);
height: var(-default-button-size);
color: rgba($oc-black, 0.4);
}
}
}
================================================
FILE: packages/drawnix/src/components/color-picker.tsx
================================================
import { useState } from 'react';
import { Check, NoColorIcon } from './icons';
import Stack from '../components/stack';
import './color-picker.scss';
import { splitRows } from '../utils/common';
import {
hexAlphaToOpacity,
isDefaultStroke,
isNoColor,
removeHexAlpha,
} from '../utils/color';
import React from 'react';
import { SizeSlider } from './size-slider';
import {
DEFAULT_COLOR,
isNullOrUndefined,
MERGING,
PlaitHistoryBoard,
} from '@plait/core';
import {
CLASSIC_COLORS,
NO_COLOR,
TRANSPARENT,
WHITE,
} from '../constants/color';
import { useBoard } from '@plait-board/react-board';
import { Translations, useI18n } from '../i18n';
const ROWS_CLASSIC_COLORS = splitRows(CLASSIC_COLORS, 4);
export type ColorPickerProps = {
onColorChange: (color: string) => void;
onOpacityChange: (opacity: number) => void;
currentColor?: string;
};
export const ColorPicker = React.forwardRef((props: ColorPickerProps, ref) => {
const board = useBoard();
const { t } = useI18n();
const { currentColor, onColorChange, onOpacityChange } = props;
const [selectedColor, setSelectedColor] = useState(
(currentColor && removeHexAlpha(currentColor)) ||
ROWS_CLASSIC_COLORS[0][0].value
);
const [opacity, setOpacity] = useState(() => {
const _opacity = currentColor && hexAlphaToOpacity(currentColor);
return (!isNullOrUndefined(_opacity) ? _opacity : 100) as number;
});
return (
<Stack.Col gap={3}>
<SizeSlider
title={t('popupToolbar.opacity')}
step={5}
defaultValue={opacity}
onChange={(value) => {
setOpacity(value);
onOpacityChange(value);
}}
beforeStart={() => {
MERGING.set(board, true);
PlaitHistoryBoard.setSplittingOnce(board, true);
}}
afterEnd={() => {
MERGING.set(board, false);
}}
disabled={selectedColor === CLASSIC_COLORS[0]['value']}
></SizeSlider>
<Stack.Col gap={2}>
{ROWS_CLASSIC_COLORS.map((colors, index) => (
<Stack.Row key={index} gap={2}>
{colors.map((color) => {
return (
<button
key={color.value}
className={`color-select-item ${
selectedColor === color.value ? 'active' : ''
} ${isNoColor(color.value) ? 'no-color' : ''}`}
style={{
backgroundColor: isNoColor(color.value)
? TRANSPARENT
: color.value,
color: isDefaultStroke(color.value)
? WHITE
: DEFAULT_COLOR,
}}
onClick={() => {
setSelectedColor(color.value);
if (color.value === NO_COLOR) {
setOpacity(100);
}
onColorChange(color.value);
}}
title={t((color.name || 'color.unknown') as keyof Translations)}
aria-label={t((color.name || 'color.unknown') as keyof Translations)}
>
{isNoColor(color.value) && NoColorIcon}
{selectedColor === color.value && Check}
</button>
);
})}
</Stack.Row>
))}
</Stack.Col>
</Stack.Col>
);
});
================================================
FILE: packages/drawnix/src/components/dialog/dialog.scss
================================================
.Dialog-overlay {
background: rgba(#121212, 0.2);
display: grid;
place-items: center;
}
.Dialog {
margin: 15px;
background-color: white;
padding: 15px;
border-radius: 4px;
}
================================================
FILE: packages/drawnix/src/components/dialog/dialog.tsx
================================================
import * as React from 'react';
import {
useFloating,
useClick,
useDismiss,
useRole,
useInteractions,
useMergeRefs,
FloatingPortal,
FloatingFocusManager,
FloatingOverlay,
useId,
} from '@floating-ui/react';
import './dialog.scss';
interface DialogOptions {
initialOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}
export function useDialog({
initialOpen = false,
open: controlledOpen,
onOpenChange: setControlledOpen,
}: DialogOptions = {}) {
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(initialOpen);
const [labelId, setLabelId] = React.useState<string | undefined>();
const [descriptionId, setDescriptionId] = React.useState<
string | undefined
>();
const open = controlledOpen ?? uncontrolledOpen;
const setOpen = setControlledOpen ?? setUncontrolledOpen;
const data = useFloating({
open,
onOpenChange: setOpen,
});
const context = data.context;
const click = useClick(context, {
enabled: controlledOpen == null,
});
const dismiss = useDismiss(context, { outsidePressEvent: 'mousedown' });
const role = useRole(context);
const interactions = useInteractions([click, dismiss, role]);
return React.useMemo(
() => ({
open,
setOpen,
...interactions,
...data,
labelId,
descriptionId,
setLabelId,
setDescriptionId,
}),
[open, setOpen, interactions, data, labelId, descriptionId]
);
}
type ContextType =
| (ReturnType<typeof useDialog> & {
setLabelId: React.Dispatch<React.SetStateAction<string | undefined>>;
setDescriptionId: React.Dispatch<
React.SetStateAction<string | undefined>
>;
})
| null;
const DialogContext = React.createContext<ContextType>(null);
export const useDialogContext = () => {
const context = React.useContext(DialogContext);
if (context == null) {
throw new Error('Dialog components must be wrapped in <Dialog />');
}
return context;
};
export function Dialog({
children,
...options
}: {
children: React.ReactNode;
} & DialogOptions) {
const dialog = useDialog(options);
return (
<DialogContext.Provider value={dialog}>{children}</DialogContext.Provider>
);
}
interface DialogTriggerProps {
children: React.ReactNode;
asChild?: boolean;
}
export const DialogTrigger = React.forwardRef<
HTMLElement,
React.HTMLProps<HTMLElement> & DialogTriggerProps
>(function DialogTrigger({ children, asChild = false, ...props }, propRef) {
const context = useDialogContext();
const childrenRef = (children as any).ref;
const ref = useMergeRefs([context.refs.setReference, propRef, childrenRef]);
// `asChild` allows the user to pass any element as the anchor
if (asChild && React.isValidElement(children)) {
return React.cloneElement(
children,
context.getReferenceProps({
ref,
...props,
...children.props,
'data-state': context.open ? 'open' : 'closed',
})
);
}
return (
<button
ref={ref}
// The user can style the trigger based on the state
data-state={context.open ? 'open' : 'closed'}
{...context.getReferenceProps(props)}
>
{children}
</button>
);
});
export const DialogContent = React.forwardRef<
HTMLDivElement,
React.HTMLProps<HTMLDivElement> & { container?: HTMLElement | null }
>(function DialogContent(props, propRef) {
const { context: floatingContext, ...context } = useDialogContext();
const ref = useMergeRefs([context.refs.setFloating, propRef]);
if (!floatingContext.open) return null;
return (
<FloatingPortal root={props.container}>
<FloatingOverlay className="Dialog-overlay" lockScroll>
<FloatingFocusManager context={floatingContext}>
<div
ref={ref}
aria-labelledby={context.labelId}
aria-describedby={context.descriptionId}
{...context.getFloatingProps(props)}
>
{props.children}
</div>
</FloatingFocusManager>
</FloatingOverlay>
</FloatingPortal>
);
});
export const DialogHeading = React.forwardRef<
HTMLHeadingElement,
React.HTMLProps<HTMLHeadingElement>
>(function DialogHeading({ children, ...props }, ref) {
const { setLabelId } = useDialogContext();
const id = useId();
// Only sets `aria-labelledby` on the Dialog root element
// if this component is mounted inside it.
React.useLayoutEffect(() => {
setLabelId(id);
return () => setLabelId(undefined);
}, [id, setLabelId]);
return (
<h2 {...props} ref={ref} id={id}>
{children}
</h2>
);
});
export const DialogDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLProps<HTMLParagraphElement>
>(function DialogDescription({ children, ...props }, ref) {
const { setDescriptionId } = useDialogContext();
const id = useId();
// Only sets `aria-describedby` on the Dialog root element
// if this component is mounted inside it.
React.useLayoutEffect(() => {
setDescriptionId(id);
return () => setDescriptionId(undefined);
}, [id, setDescriptionId]);
return (
<p {...props} ref={ref} id={id}>
{children}
</p>
);
});
export const DialogClose = React.forwardRef<
HTMLButtonElement,
React.ButtonHTMLAttributes<HTMLButtonElement>
>(function DialogClose(props, ref) {
const { setOpen } = useDialogContext();
return (
<button type="button" {...props} ref={ref} onClick={() => setOpen(false)} />
);
});
================================================
FILE: packages/drawnix/src/components/icons.tsx
================================================
import React from 'react';
export const createIcon = (svg: React.ReactNode) => {
return svg;
};
export const HandIcon = createIcon(
<svg viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g id="Hand" stroke="none" fill="currentColor">
<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>
</g>
</svg>
);
export const SelectionIcon = createIcon(
<svg viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g id="selection" stroke="none" fill="currentColor">
<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>
</g>
</svg>
);
export const MindIcon = createIcon(
<svg viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g id="Mind" stroke="none" fill="currentColor">
<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>
</g>
</svg>
);
export const ShapeIcon = createIcon(
<svg viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g id="geometry" stroke="none" fill="currentColor">
<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>
</g>
</svg>
);
export const TextIcon = createIcon(
<svg viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g id="font" stroke="none" fill="currentColor">
<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>
</g>
</svg>
);
export const EraseIcon = createIcon(
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" />
<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" />
<path d="M18 13.3l-6.3 -6.3" />
</svg>
);
export const StraightArrowLineIcon = createIcon(
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1">
<g id="straight-line" stroke="none" fill="currentColor">
<path
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"
id=""
transform="translate(8.500035, 7.500035) rotate(-135.000000) translate(-8.500035, -7.500035) "
/>
</g>
</svg>
);
export const RectangleIcon = createIcon(
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M3 3h18v18H3z"
stroke="currentColor"
strokeWidth="2"
fill="none"
></path>
</svg>
);
export const TerminalIcon = createIcon(
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g id="terminal" stroke="none" fill="currentColor">
<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" />
</g>
</svg>
);
export const EllipseIcon = createIcon(
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1">
<g id="ellipse" stroke="none" fill="currentColor">
<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" />
</g>
</svg>
);
export const TriangleIcon = createIcon(
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1">
<g id="triangle" stroke="none" fill="currentColor">
<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" />
</g>
</svg>
);
export const DiamondIcon = createIcon(
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1">
<g stroke="none" fill="currentColor">
<path
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"
transform="translate(7.989647, 8.003560) rotate(-315.000000) translate(-7.989647, -8.003560) "
/>
</g>
</svg>
);
export const ParallelogramIcon = createIcon(
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1">
<g stroke="none" fill="currentColor">
<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" />
</g>
</svg>
);
export const RoundRectangleIcon = createIcon(
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1">
<g stroke="none" fill="currentColor">
<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" />
</g>
</svg>
);
export const StraightArrowIcon = createIcon(
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1">
<g stroke="none" fill="currentColor">
<path
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"
transform="translate(8.500035, 7.500035) rotate(-135.000000) translate(-8.500035, -7.500035) "
/>
</g>
</svg>
);
export const ElbowArrowIcon = createIcon(
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1">
<g stroke="none" fill="currentColor">
<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" />
</g>
</svg>
);
export const CurveArrowIcon = createIcon(
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1">
<g stroke="none" fill="currentColor">
<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" />
</g>
</svg>
);
export const MenuIcon = createIcon(
<svg
xmlns="http://www.w3.org/2000/svg"
strokeLinecap="round"
strokeLinejoin="round"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<g strokeWidth="1.5">
<path stroke="none" d="M0 0h24v24H0z"></path>
<line x1="4" y1="6" x2="20" y2="6"></line>
<line x1="4" y1="12" x2="20" y2="12"></line>
<line x1="4" y1="18" x2="20" y2="18"></line>
</g>
</svg>
);
export const GithubIcon = createIcon(
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
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"
strokeWidth="1.25"
></path>
</svg>
);
export const ExportImageIcon = createIcon(
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g
strokeWidth="1.25"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
>
<path stroke="none" d="M0 0h24v24H0z"></path>
<path d="M15 8h.01"></path>
<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>
<path d="M4 15l4 -4c.928 -.893 2.072 -.893 3 0l4 4"></path>
<path d="M14 14l1 -1c.617 -.593 1.328 -.793 2.009 -.598"></path>
<path d="M19 16v6"></path>
<path d="M22 19l-3 3l-3 -3"></path>
</g>
</svg>
);
export const ZoomOutIcon = createIcon(
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<g id="zoom-out" stroke="none" fill="currentColor" strokeWidth="1">
<path
fillRule="nonzero"
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"
></path>
</g>
</svg>
);
export const ZoomInIcon = createIcon(
<svg viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g id="zoom-in" stroke="none" fill="currentColor" strokeWidth="1">
<path
fillRule="nonzero"
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"
></path>
</g>
</svg>
);
export const SaveFileIcon = createIcon(
<svg viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g id="save-file" stroke="none" fill="currentColor">
<path
fillRule="nonzero"
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"
></path>
</g>
</svg>
);
export const OpenFileIcon = createIcon(
<svg viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g id="save-file" stroke="currentColor" fill="none">
<path
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"
strokeWidth="1.25"
/>
</g>
</svg>
);
export const BackgroundColorIcon = createIcon(
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
className="background-color-icon"
>
<g transform="translate(1 1)" fillRule="evenodd" fill="#000" stroke="none">
<circle fillOpacity=".04" r="11" cy="11" cx="11"></circle>
<path
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"
fillOpacity=".12"
></path>
</g>
</svg>
);
export const NoColorIcon = createIcon(
<svg viewBox="0 0 32 32" className="no-color-icon">
<g
xmlns="http://www.w3.org/2000/svg"
fillRule="nonzero"
fill="currentColor"
stroke="none"
>
<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>
<path d="M6.354 26.354l-.708-.708 20-20 .708.708z"></path>
</g>
</svg>
);
export const Check = createIcon(
<svg
className="selected-icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
);
export const StrokeIcon = createIcon(
<svg viewBox="0 0 24 24" className="stroke-icon">
<g
xmlns="http://www.w3.org/2000/svg"
stroke="none"
fillRule="evenodd"
fill="#000"
>
<path
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"
fillRule="nonzero"
fillOpacity=".04"
></path>
<path
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"
fillOpacity=".12"
></path>
</g>
</svg>
);
export const StrokeWhiteIcon = createIcon(
<svg viewBox="0 0 24 24">
<g
xmlns="http://www.w3.org/2000/svg"
id="icon-border-white"
stroke="none"
strokeWidth="1"
fill="none"
fillRule="evenodd"
opacity="0.1"
>
<g id="Group">
<path
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"
fill="#000000"
fillRule="nonzero"
/>
<path
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"
fill="#000000"
fillRule="nonzero"
/>
</g>
</g>
</svg>
);
export const StrokeStyleNormalIcon = createIcon(
<svg viewBox="0 0 24 32" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(0 14)" fillRule="evenodd" fill="none">
<path d="M-18-19h60v40h-60z"></path>
<path d="M0 0h24v2H0z" fill="currentColor"></path>
</g>
</svg>
);
export const StrokeStyleDashedIcon = createIcon(
<svg viewBox="0 0 24 32" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(0 14)" fillRule="evenodd" fill="none">
<g fill="currentColor">
<path d="M0 0h6v2H0zM9 0h6v2H9zM18 0h6v2h-6z"></path>
</g>
</g>
</svg>
);
export const StrokeStyleDotedIcon = createIcon(
<svg viewBox="0 0 24 32" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(0 14)" fillRule="evenodd" fill="none">
<g fill="currentColor">
<rect rx="1" height="2" width="2"></rect>
<rect rx="1" x="4" height="2" width="2"></rect>
<rect rx="1" x="8" height="2" width="2"></rect>
<rect rx="1" x="12" height="2" width="2"></rect>
<rect rx="1" x="16" height="2" width="2"></rect>
<rect rx="1" x="20" height="2" width="2"></rect>
</g>
</g>
</svg>
);
export const FontColorIcon: React.FC<{ currentColor?: string }> = ({
currentColor,
}) => {
return (
<svg
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
className="font-color-icon"
>
<g
id="font-color"
strokeWidth="1"
fillRule="evenodd"
stroke="none"
fill="currentColor"
>
<path
id="secondary-color"
d="M1.999 15.011h11.998V13.81H1.999z"
fill={currentColor || '#333333'}
></path>
<path
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"
id="A"
></path>
</g>
</svg>
);
};
export const UndoIcon = createIcon(
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<g stroke="none" fill="currentColor">
<g id="undo-cion" transform="translate(1 1)">
<path
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"
id="undo-icon-path"
></path>
</g>
</g>
</svg>
);
export const RedoIcon = createIcon(
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<g stroke="none" fill="currentColor">
<g id="redo-cion" transform="matrix(-1 0 0 1 15.015 1)">
<path
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"
id="redo-icon-path"
></path>
</g>
</g>
</svg>
);
export const TrashIcon = createIcon(
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor">
<path
strokeWidth="1.25"
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"
></path>
</svg>
);
export const DuplicateIcon = createIcon(
<svg
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<g strokeWidth="1.25">
<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>
<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>
</g>
</svg>
);
export const FeltTipPenIcon = createIcon(
<svg
viewBox="0 0 1024 1024"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<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>
</svg>
);
export const ImageIcon = createIcon(
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<g id="image" stroke="none" fill="currentColor">
<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>
</g>
</svg>
);
export const ExtraToolsIcon = createIcon(
<svg
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<g strokeWidth={1.8} fill="none">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 3l-4 7h8z"></path>
<path d="M17 17m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"></path>
<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>
</g>
</svg>
);
export const MermaidLogoIcon = createIcon(
<svg
stroke="currentColor"
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke="none"
fill="currentColor"
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"
/>
</svg>
);
export const MarkdownLogoIcon = createIcon(
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1">
<g stroke="none" fill="currentColor">
<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" />
</g>
</svg>
);
export const LinkIcon = createIcon(
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g stroke="none" fill="currentColor">
<path
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"
transform="rotate(46 8.253 8.13)"
></path>
</g>
</svg>
);
export const ArrowIcon = createIcon(
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g stroke="none">
<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>
</g>
</svg>
);
export const LineIcon = createIcon(
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g stroke="none">
<rect x="1" y="7.5" width="14" height="1.2" rx=".5"></rect>
</g>
</svg>
);
export const StraightLineIcon = createIcon(
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g stroke="none">
<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>
</g>
</svg>
);
export const NoteCurlyRightIcon = createIcon(
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g stroke="none" fill="currentColor" fillRule="evenodd">
<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" />
</g>
</svg>
);
export const NoteCurlyLeftIcon = createIcon(
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g stroke="none" fill="currentColor" fillRule="evenodd">
<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" />
</g>
</svg>
);
export const ChevronDownIcon = createIcon(
<svg
width="15"
height="15"
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
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"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
></path>
</svg>
);
export const ThickCheckIcon = createIcon(
<svg
width="15"
height="15"
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
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"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
></path>
</svg>
);
export const FontSizeStepperUpIcon: React.FC<
React.SVGProps<SVGSVGElement>
> = (props) => {
return (
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M4 10L8 6L12 10"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
export const FontSizeStepperDownIcon: React.FC<
React.SVGProps<SVGSVGElement>
> = (props) => {
return (
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M4 6L8 10L12 6"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
================================================
FILE: packages/drawnix/src/components/island.scss
================================================
.drawnix {
.island {
--padding: 0;
box-sizing: border-box;
background-color: var(--island-bg-color);
box-shadow: var(--shadow-island);
border-radius: var(--border-radius-md);
padding: calc(var(--padding) * var(--space-factor));
position: relative;
transition: box-shadow 0.5s ease-in-out;
border: 1px solid var(--island-border-color);
&.zen-mode {
box-shadow: none;
}
}
}
================================================
FILE: packages/drawnix/src/components/island.tsx
================================================
// Credits to excalidraw
import classNames from 'classnames';
import './island.scss';
import React from 'react';
type IslandProps = {
children: React.ReactNode;
padding?: number;
className?: string | boolean;
style?: object;
} & React.HTMLAttributes<HTMLDivElement>;
export const Island = React.forwardRef<HTMLDivElement, IslandProps>(
({ children, padding, className, style, ...restProps }, ref) => (
<div
className={classNames('island', className)}
style={{ '--padding': padding, ...style }}
ref={ref}
{...restProps}
>
{children}
</div>
)
);
================================================
FILE: packages/drawnix/src/components/menu/common.ts
================================================
import React, { useContext } from 'react';
import { EVENT } from '../../constants';
import { composeEventHandlers } from '../../utils/common';
export const MenuContentPropsContext = React.createContext<{
onSelect?: (event: Event) => void;
}>({});
export const getMenuItemClassName = (
className = '',
active = false,
) => {
return `menu-item menu-item-base ${className} ${
active ? 'menu-item--active' : ''
}`.trim();
};
export const useHandleMenuItemClick = (
origOnClick:
| React.MouseEventHandler<HTMLAnchorElement | HTMLButtonElement>
| undefined,
onSelect: ((event: Event) => void) | undefined
) => {
const menuContentProps = useContext(MenuContentPropsContext);
return composeEventHandlers(origOnClick, (event) => {
const itemSelectEvent = new CustomEvent(EVENT.MENU_ITEM_SELECT, {
bubbles: true,
cancelable: true,
});
onSelect?.(itemSelectEvent);
if (!itemSelectEvent.defaultPrevented) {
menuContentProps.onSelect?.(itemSelectEvent);
}
});
};
================================================
FILE: packages/drawnix/src/components/menu/menu-group.tsx
================================================
import React from 'react';
const MenuGroup = ({
children,
className = '',
style,
title,
}: {
children: React.ReactNode;
className?: string;
style?: React.CSSProperties;
title?: string;
}) => {
return (
<div className={`menu-group ${className}`} style={style}>
{title && <p className="menu-group-title">{title}</p>}
{children}
</div>
);
};
export default MenuGroup;
MenuGroup.displayName = 'MenuGroup';
================================================
FILE: packages/drawnix/src/components/menu/menu-item-content-radio.tsx
================================================
import { RadioGroup } from '../radio-group';
type Props<T> = {
value: T;
shortcut?: string;
choices: {
value: T;
label: React.ReactNode;
ariaLabel?: string;
}[];
onChange: (value: T) => void;
children: React.ReactNode;
name: string;
};
const MenuItemContentRadio = <T,>({
value,
shortcut,
onChange,
choices,
children,
name,
}: Props<T>) => {
return (
<>
<div className="menu-item-base menu-item-bare">
<label className="menu-item__text" htmlFor={name}>
{children}
</label>
<RadioGroup
name={name}
value={value}
onChange={onChange}
choices={choices}
/>
</div>
{shortcut && (
<div className="menu-item__shortcut menu-item__shortcut--orphaned">
{shortcut}
</div>
)}
</>
);
};
MenuItemContentRadio.displayName = 'MenuItemContentRadio';
export default MenuItemContentRadio;
================================================
FILE: packages/drawnix/src/components/menu/menu-item-content.tsx
================================================
import React from 'react';
const MenuItemContent = ({
icon,
shortcut,
children,
}: {
icon?: React.ReactNode;
shortcut?: string;
children: React.ReactNode;
}) => {
return (
<>
{icon && <div className="menu-item__icon">{icon}</div>}
<div className="menu-item__text">{children}</div>
{shortcut && (
<div className="menu-item__shortcut">{shortcut}</div>
)}
</>
);
};
export default MenuItemContent;
================================================
FILE: packages/drawnix/src/components/menu/menu-item-custom.tsx
================================================
import React from 'react';
const MenuItemCustom = ({
children,
className = '',
selected,
...rest
}: {
children: React.ReactNode;
className?: string;
selected?: boolean;
} & React.HTMLAttributes<HTMLDivElement>) => {
return (
<div
{...rest}
className={`menu-item-base menu-item-custom ${className} ${
selected ? `menu-item--selected` : ``
}`.trim()}
>
{children}
</div>
);
};
export default MenuItemCustom;
================================================
FILE: packages/drawnix/src/components/menu/menu-item-link.tsx
================================================
import React from 'react';
import { getMenuItemClassName, useHandleMenuItemClick } from './common';
import MenuItemContent from './menu-item-content';
const MenuItemLink = ({
icon,
shortcut,
href,
children,
onSelect,
className = '',
selected,
...rest
}: {
href: string;
icon?: React.ReactNode;
children: React.ReactNode;
shortcut?: string;
className?: string;
selected?: boolean;
onSelect?: (event: Event) => void;
} & React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
const handleClick = useHandleMenuItemClick(rest.onClick, onSelect);
return (
<a
{...rest}
href={href}
target="_blank"
rel="noreferrer"
className={getMenuItemClassName(className, selected)}
title={rest.title ?? rest['aria-label']}
onClick={handleClick}
>
<MenuItemContent icon={icon} shortcut={shortcut}>
{children}
</MenuItemContent>
</a>
);
};
export default MenuItemLink;
MenuItemLink.displayName = 'MenuItemLink';
================================================
FILE: packages/drawnix/src/components/menu/menu-item.tsx
================================================
import React, { useState, useRef } from 'react';
import {
getMenuItemClassName,
useHandleMenuItemClick,
} from './common';
import MenuItemContent from './menu-item-content';
import { Popover, PopoverContent, PopoverTrigger } from '../popover/popover';
const MenuItem = ({
icon,
onSelect,
children,
shortcut,
className,
selected,
submenu,
...rest
}: {
icon?: React.ReactNode;
onSelect: (event: Event) => void;
children: React.ReactNode;
shortcut?: string;
selected?: boolean;
className?: string;
submenu?: React.ReactNode;
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onSelect'>) => {
const [isOpen, setIsOpen] = useState(false);
const closeTimeoutRef = useRef<number>();
const handleClick = useHandleMenuItemClick(rest.onClick, onSelect);
const menuItemContent = (
<MenuItemContent icon={icon} shortcut={shortcut}>
{children}
</MenuItemContent>
);
const handleMouseEnter = () => {
if (closeTimeoutRef.current) {
window.clearTimeout(closeTimeoutRef.current);
}
setIsOpen(true);
};
const handleMouseLeave = () => {
closeTimeoutRef.current = window.setTimeout(() => {
setIsOpen(false);
}, 100);
};
const handleMenuItemClick = (event: React.MouseEvent<HTMLButtonElement>) => {
if (submenu) {
setIsOpen(!isOpen);
rest.onClick?.(event as any);
} else {
handleClick(event as any);
}
};
if (submenu) {
return (
<Popover
open={isOpen}
onOpenChange={setIsOpen}
placement="right-start"
>
<PopoverTrigger asChild>
<button
{...rest}
type="button"
className={getMenuItemClassName(className, selected || isOpen)}
title={rest.title ?? rest['aria-label']}
onClick={handleMenuItemClick}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{menuItemContent}
</button>
</PopoverTrigger>
<PopoverContent onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
{submenu}
</PopoverContent>
</Popover>
);
}
return (
<button
{...rest}
onClick={handleClick}
type="button"
className={getMenuItemClassName(className, selected)}
title={rest.title ?? rest['aria-label']}
>
{menuItemContent}
</button>
);
};
MenuItem.displayName = 'MenuItem';
export const DropDownMenuItemBadge = ({
children,
}: {
children: React.ReactNode;
}) => {
return (
<div
style={{
display: 'inline-flex',
marginLeft: 'auto',
padding: '2px 4px',
background: 'var(--color-promo)',
color: 'var(--color-surface-lowest)',
borderRadius: 6,
fontSize: 9,
fontFamily: 'Cascadia, monospace',
}}
>
{children}
</div>
);
};
DropDownMenuItemBadge.displayName = 'MenuItemBadge';
MenuItem.Badge = DropDownMenuItemBadge;
export default MenuItem;
================================================
FILE: packages/drawnix/src/components/menu/menu-separator.tsx
================================================
const MenuSeparator = () => (
<div
style={{
height: '1px',
backgroundColor: 'var(--color-gray-10)',
margin: '.5rem 0',
}}
/>
);
export default MenuSeparator;
MenuSeparator.displayName = 'MenuSeparator';
================================================
FILE: packages/drawnix/src/components/menu/menu.scss
================================================
@import "../../styles/variables.module.scss";
.drawnix {
.menu {
&--mobile {
left: 0;
width: 100%;
row-gap: 0.75rem;
.menu-container {
padding: 8px 8px;
box-sizing: border-box;
box-shadow: var(--shadow-island);
border-radius: var(--border-radius-lg);
position: relative;
transition: box-shadow 0.5s ease-in-out;
&.zen-mode {
box-shadow: none;
}
}
}
.menu-container {
background-color: var(--island-bg-color);
max-height: calc(100vh - 150px);
overflow-y: auto;
--gap: 2;
}
.menu-item-base {
display: flex;
padding: 0 0.625rem;
column-gap: 0.625rem;
font-size: 0.875rem;
color: var(--color-gray-90);
width: 100%;
box-sizing: border-box;
font-weight: normal;
font-family: inherit;
}
.menu-item {
background-color: transparent;
border: 1px solid transparent;
align-items: center;
height: 2rem;
margin-top: 4px;
cursor: pointer;
border-radius: var(--border-radius-md);
@media screen and (min-width: 1921px) {
height: 2.25rem;
}
&--active {
background-color: var(--color-surface-primary-container);
text-decoration: none;
}
&__text {
display: flex;
align-items: center;
width: 100%;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
gap: 0.75rem;
}
&__shortcut {
margin-inline-start: auto;
opacity: 0.5;
&--orphaned {
text-align: right;
font-size: 0.875rem;
padding: 0 0.625rem;
}
}
&:hover {
background-color: var(--color-surface-primary-container);
text-decoration: none;
}
&:active {
background-color: var(--color-surface-primary-container);
border-color: var(--color-brand-active);
}
svg {
width: 1rem;
height: 1rem;
display: block;
}
}
.menu-item-bare {
align-items: center;
height: 2rem;
justify-content: space-between;
@media screen and (min-width: 1921px) {
height: 2.25rem;
}
svg {
width: 1rem;
height: 1rem;
display: block;
}
}
.menu-item-custom {
margin-top: 0.5rem;
}
.menu-group-title {
font-size: 14px;
text-align: left;
margin: 10px 0;
font-weight: 500;
}
}
.menu-button {
@include outlineButtonStyles;
width: var(--lg-button-size);
height: var(--lg-button-size);
@at-root .drawnix.theme--dark#{&} {
--background: var(--color-surface-high);
&:hover {
--background: #363541;
}
}
svg {
width: var(--lg-icon-size);
height: var(--lg-icon-size);
}
&--mobile {
border: none;
margin: 0;
padding: 0;
width: var(--default-button-size);
height: var(--default-button-size);
}
}
}
================================================
FILE: packages/drawnix/src/components/menu/menu.tsx
================================================
import { Island } from '../island';
import React from 'react';
import { MenuContentPropsContext } from './common';
import classNames from 'classnames';
import './menu.scss';
const Menu = ({
children,
className = '',
onSelect,
style,
}: {
children?: React.ReactNode;
className?: string;
/**
* Called when any menu item is selected (clicked on).
*/
onSelect?: (event: Event) => void;
style?: React.CSSProperties;
}) => {
const newClassName = classNames(`menu ${className}`).trim();
return (
<MenuContentPropsContext.Provider value={{ onSelect }}>
<div className={newClassName} style={style} data-testid="menu">
{
<Island className="menu-container" padding={2}>
{children}
</Island>
}
</div>
</MenuContentPropsContext.Provider>
);
};
Menu.displayName = 'Menu';
export default Menu;
================================================
FILE: packages/drawnix/src/components/popover/popover.tsx
================================================
import * as React from 'react';
import {
useFloating,
autoUpdate,
offset,
flip,
shift,
useClick,
useDismiss,
useRole,
useInteractions,
useMergeRefs,
Placement,
FloatingPortal,
FloatingFocusManager,
} from '@floating-ui/react';
interface PopoverOptions {
initialOpen?: boolean;
placement?: Placement;
modal?: boolean;
open?: boolean;
sideOffset?: number;
onOpenChange?: (open: boolean) => void;
}
export function usePopover({
initialOpen = false,
placement = 'bottom',
modal,
sideOffset,
open: controlledOpen,
onOpenChange: setControlledOpen,
}: PopoverOptions = {}) {
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(initialOpen);
const [labelId, setLabelId] = React.useState<string | undefined>();
const [descriptionId, setDescriptionId] = React.useState<
string | undefined
>();
const open = controlledOpen ?? uncontrolledOpen;
const setOpen = setControlledOpen ?? setUncontrolledOpen;
const data = useFloating({
placement,
open,
onOpenChange: setOpen,
whileElementsMounted: autoUpdate,
middleware: [
offset(sideOffset || 4),
flip({
crossAxis: placement.includes('-'),
fallbackAxisSideDirection: 'end',
padding: 5,
}),
shift({ padding: 5 }),
],
});
const context = data.context;
const click = useClick(context, {
enabled: controlledOpen == null,
});
const dismiss = useDismiss(context);
const role = useRole(context);
const interactions = useInteractions([click, dismiss, role]);
return React.useMemo(
() => ({
open,
setOpen,
...interactions,
...data,
modal,
labelId,
descriptionId,
setLabelId,
setDescriptionId,
}),
[open, setOpen, interactions, data, modal, labelId, descriptionId]
);
}
type ContextType =
| (ReturnType<typeof usePopover> & {
setLabelId: React.Dispatch<React.SetStateAction<string | undefined>>;
setDescriptionId: React.Dispatch<
React.SetStateAction<string | undefined>
>;
})
| null;
const PopoverContext = React.createContext<ContextType>(null);
export const usePopoverContext = () => {
const context = React.useContext(PopoverContext);
if (context == null) {
throw new Error('Popover components must be wrapped in <Popover />');
}
return context;
};
export function Popover({
children,
modal = false,
...restOptions
}: {
children: React.ReactNode;
} & PopoverOptions) {
// This can accept any props as options, e.g. `placement`,
// or other positioning options.
const popover = usePopover({ modal, ...restOptions });
return (
<PopoverContext.Provider value={popover}>
{children}
</PopoverContext.Provider>
);
}
interface PopoverTriggerProps {
children: React.ReactNode;
asChild?: boolean;
}
export const PopoverTrigger = React.forwardRef<
HTMLElement,
React.HTMLProps<HTMLElement> & PopoverTriggerProps
>(function PopoverTrigger({ children, asChild = false, ...props }, propRef) {
const context = usePopoverContext();
const childrenRef = (children as any).ref;
const ref = useMergeRefs([context.refs.setReference, propRef, childrenRef]);
// `asChild` allows the user to pass any element as the anchor
if (asChild && React.isValidElement(children)) {
return React.cloneElement(
children,
context.getReferenceProps({
ref,
...props,
...children.props,
'data-state': context.open ? 'open' : 'closed',
})
);
}
return (
<button
ref={ref}
type="button"
// The user can style the trigger based on the state
data-state={context.open ? 'open' : 'closed'}
{...context.getReferenceProps(props)}
>
{children}
</button>
);
});
export const PopoverContent = React.forwardRef<
HTMLDivElement,
React.HTMLProps<HTMLDivElement> & { container?: HTMLElement | null }
>(function PopoverContent({ container, style, ...props }, propRef) {
const { context: floatingContext, ...context } = usePopoverContext();
const ref = useMergeRefs([context.refs.setFloating, propRef]);
if (!floatingContext.open) return null;
return (
<FloatingPortal root={container}>
<FloatingFocusManager context={floatingContext} modal={context.modal}>
<div
ref={ref}
style={{ ...context.floatingStyles, ...style }}
aria-labelledby={context.labelId}
aria-describedby={context.descriptionId}
{...context.getFloatingProps(props)}
>
{props.children}
</div>
</FloatingFocusManager>
</FloatingPortal>
);
});
================================================
FILE: packages/drawnix/src/components/popup/link-popup/link-popup.scss
================================================
.drawnix {
.link-popup {
padding-left: 8px;
&__link {
font-size: 14px;
}
.link-popup__link {
display: inline-block;
width: 18rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__input {
padding: 10px 0px;
width: 328px;
border: none;
border-radius: 4px;
font-size: 14px;
outline: none;
}
}
}
================================================
FILE: packages/drawnix/src/components/popup/link-popup/link-popup.tsx
================================================
import { useEffect, useState, useRef } from 'react';
import { Island } from '../../island';
import Stack from '../../stack';
import { ToolButton } from '../../tool-button';
import classNames from 'classnames';
import './link-popup.scss';
import { flip, offset, useFloating } from '@floating-ui/react';
import { useDrawnix } from '../../../hooks/use-drawnix';
import { FeltTipPenIcon, TrashIcon } from '../../icons';
import { Transforms } from 'slate';
import { ReactEditor } from 'slate-react';
import { LinkEditor } from '@plait/text-plugins';
import { LinkElement } from '@plait/common';
import { useBoard } from '@plait-board/react-board';
import { useI18n } from '../../../i18n';
export const LinkPopup = () => {
const { t } = useI18n();
const [url, setUrl] = useState('');
const { appState, setAppState } = useDrawnix();
const board = useBoard();
const { refs, floatingStyles } = useFloating({
placement: 'top',
middleware: [offset(20), flip()],
});
const linkState = appState.linkState;
const target = appState.linkState?.targetDom || null;
const isEditing = appState.linkState?.isEditing || false;
const isHoveringOrigin = appState.linkState?.isHoveringOrigin || false;
const isHovering = appState.linkState?.isHovering || false;
const isOpening = isEditing || isHoveringOrigin || isHovering;
const linkStateRef = useRef(appState.linkState);
useEffect(() => {
linkStateRef.current = appState.linkState;
if (appState.linkState) {
setUrl(appState.linkState.targetElement.url);
} else {
setUrl('');
}
}, [appState.linkState]);
useEffect(() => {
if (target) {
const rect = target.getBoundingClientRect();
refs.setPositionReference({
getBoundingClientRect() {
return {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
top: rect.y,
left: rect.x,
right: rect.x + rect.width,
bottom: rect.y + rect.height,
};
},
});
}
}, [board.viewport, target]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
refs.floating.current &&
!refs.floating.current.contains(event.target as Node)
) {
if (linkStateRef.current) {
const linkElement = LinkEditor.getLinkElement(
linkStateRef.current.editor
);
if (linkElement && !(linkElement[0] as LinkElement).url.trim()) {
LinkEditor.unwrapLink(linkStateRef.current.editor);
}
}
setAppState({
...appState,
linkState: null,
});
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
const saveUrlAndExitEditing = () => {
if (url !== linkState!.targetElement.url) {
const editor = linkState!.editor;
const node = linkState!.targetElement;
const path = ReactEditor.findPath(editor, node);
Transforms.setNodes(editor, { url: url }, { at: path });
}
const linkElement = LinkEditor.getLinkElement(linkState!.editor);
setAppState({
...appState,
linkState: {
...appState.linkState!,
targetElement: linkElement[0] as LinkElement,
isEditing: false,
isHoveringOrigin: true,
},
});
};
return (
isOpening && (
<Island
ref={refs.setFloating}
style={floatingStyles}
padding={1}
className={classNames('link-popup')}
onPointerEnter={() => {
if (!isHovering) {
setAppState({
...appState,
linkState: {
...appState.linkState!,
isHovering: true,
},
});
}
}}
onPointerLeave={() => {
if (!isEditing) {
setAppState({
...appState,
linkState: {
...appState.linkState!,
isHovering: false,
},
});
}
}}
>
<Stack.Row gap={1} align="center">
{isEditing ? (
<>
<input
type="text"
value={url}
onChange={(e) => {
setUrl(e.target.value);
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
saveUrlAndExitEditing();
}
}}
className="link-popup__input"
autoFocus
/>
<ToolButton
type="icon"
visible={true}
icon={TrashIcon}
title={t('popupLink.delLink')}
aria-label={t('popupLink.delLink')}
onPointerDown={() => {
const editor = linkState!.editor;
const targetElement = linkState!.targetElement;
const path = ReactEditor.findPath(editor, targetElement);
Transforms.unwrapNodes(editor, {
at: path,
});
setAppState({
...appState,
linkState: null,
});
}}
></ToolButton>
</>
) : (
<>
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="link-popup__link"
>
{url}
</a>
<ToolButton
className="link-popup__edit"
type="icon"
visible={true}
icon={FeltTipPenIcon}
title={`Edit link`}
aria-label={`Edit link`}
onPointerDown={({ event }) => {
event.preventDefault();
setAppState({
...appState,
linkState: {
...appState.linkState!,
isEditing: true,
},
});
}}
></ToolButton>
<ToolButton
type="icon"
visible={true}
icon={TrashIcon}
title={t('popupLink.delLink')}
aria-label={t('popupLink.delLink')}
onPointerDown={() => {
const editor = linkState!.editor;
const targetElement = linkState!.targetElement;
const path = ReactEditor.findPath(editor, targetElement);
Transforms.unwrapNodes(editor, {
at: path,
});
setAppState({
...appState,
linkState: null,
});
}}
></ToolButton>
</>
)}
</Stack.Row>
</Island>
)
);
};
================================================
FILE: packages/drawnix/src/components/radio-group.scss
================================================
@import '../styles/variables.module.scss';
.drawnix {
--RadioGroup-background: var(--island-bg-color);
--RadioGroup-border: var(--color-surface-high);
--RadioGroup-choice-color-off: var(--color-primary);
--RadioGroup-choice-color-off-hover: var(--color-brand-hover);
--RadioGroup-choice-background-off: var(--island-bg-color);
--RadioGroup-choice-background-off-active: var(--color-surface-high);
--RadioGroup-choice-color-on: var(--color-surface-lowest);
--RadioGroup-choice-background-on: var(--color-primary);
--RadioGroup-choice-background-on-hover: var(--color-brand-hover);
--RadioGroup-choice-background-on-active: var(--color-brand-active);
.RadioGroup {
box-sizing: border-box;
display: flex;
flex-direction: row;
align-items: flex-start;
padding: 3px;
border-radius: 10px;
background: var(--RadioGroup-background);
border: 1px solid var(--RadioGroup-border);
&__choice {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 24px;
color: var(--RadioGroup-choice-color-off);
background: var(--RadioGroup-choice-background-off);
border-radius: 8px;
font-family: "Assistant";
font-style: normal;
font-weight: 600;
font-size: 0.75rem;
line-height: 100%;
user-select: none;
letter-spacing: 0.4px;
transition: all 75ms ease-out;
&:hover {
color: var(--RadioGroup-choice-color-off-hover);
}
&:active {
background: var(--RadioGroup-choice-background-off-active);
}
&.active {
color: var(--RadioGroup-choice-color-on);
background: var(--RadioGroup-choice-background-on);
&:hover {
background: var(--RadioGroup-choice-background-on-hover);
}
&:active {
background: var(--RadioGroup-choice-background-on-active);
}
}
& input {
z-index: 1;
position: absolute;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
border-radius: 8px;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
cursor: pointer;
}
}
}
}
================================================
FILE: packages/drawnix/src/components/radio-group.tsx
================================================
import classNames from 'classnames';
import './radio-group.scss';
export type RadioGroupChoice<T> = {
value: T;
label: React.ReactNode;
ariaLabel?: string;
};
export type RadioGroupProps<T> = {
choices: RadioGroupChoice<T>[];
value: T;
onChange: (value: T) => void;
name: string;
};
export const RadioGroup = function <T>({
onChange,
value,
choices,
name,
}: RadioGroupProps<T>) {
return (
<div className="RadioGroup">
{choices.map((choice) => (
<div
className={classNames('RadioGroup__choice', {
active: choice.value === value,
})}
key={String(choice.value)}
title={choice.ariaLabel}
>
<input
name={name}
type="radio"
checked={choice.value === value}
onChange={() => onChange(choice.value)}
aria-label={choice.ariaLabel}
/>
{choice.label}
</div>
))}
</div>
);
};
================================================
FILE: packages/drawnix/src/components/select/select.scss
================================================
.drawnix {
/* Define local variables mapped to project theme or defaults */
--dx-select-trigger-height: 2rem;
--dx-select-content-padding: 4px;
--dx-select-item-height: 2rem;
--dx-select-item-indicator-width: 20px;
--dx-select-separator-margin-right: 8px;
/* Trigger Styles */
.dx-SelectTrigger {
display: inline-flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
user-select: none;
vertical-align: top;
height: var(--dx-select-trigger-height);
box-sizing: border-box;
font-family: inherit;
font-size: 14px;
line-height: 1;
text-align: left;
cursor: default;
background-color: var(--island-bg-color);
color: var(--color-text-primary);
border: 1px solid var(--island-border-color);
border-radius: var(--border-radius-md);
padding: 0 var(--dx-space-2);
gap: var(--dx-space-2);
outline: none;
&:hover {
background-color: var(--color-gray-10);
border-color: var(--color-gray-40);
}
&[data-state='open'] {
border-color: var(--color-primary);
box-shadow: 0 0 0 1px var(--color-primary);
}
&:focus-visible {
border-color: var(--color-primary);
box-shadow: 0 0 0 1px var(--color-primary);
}
&[data-disabled] {
color: var(--color-text-disabled);
background-color: var(--color-bg-disabled);
cursor: not-allowed;
}
&.dx-variant-ghost {
background-color: transparent;
border-color: transparent;
&:hover {
background-color: var(--color-gray-10);
}
}
}
.dx-SelectTriggerInner {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.dx-SelectIcon {
flex-shrink: 0;
color: var(--color-text-secondary);
display: flex;
align-items: center;
svg {
display: block;
}
}
/* Content Styles */
.dx-SelectContent {
box-sizing: border-box;
background-color: var(--island-bg-color);
border: 1px solid var(--island-border-color);
box-shadow: var(--shadow-island);
border-radius: var(--border-radius-lg);
padding: var(--dx-select-content-padding);
min-width: var(--radix-select-trigger-width); /* Note: Floating UI might handle width */
max-height: 300px;
overflow: hidden;
z-index: 1000;
display: flex;
flex-direction: column;
&:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
}
.dx-SelectContent[data-hide-selected-indicator] {
.dx-SelectItem {
padding-left: var(--dx-space-2);
}
.dx-SelectItemIndicator {
display: none;
}
}
.dx-SelectViewport {
overflow-y: auto;
padding: 0;
flex: 1;
}
/* Item Styles */
.dx-SelectItem {
display: flex;
align-items: center;
width: 100%;
height: var(--dx-select-item-height);
padding: 0 var(--dx-space-2) 0 var(--dx-select-item-indicator-width);
position: relative;
box-sizing: border-box;
outline: none;
user-select: none;
cursor: pointer;
border: none;
background: transparent;
color: var(--color-text-primary);
font-size: 14px;
border-radius: var(--dx-radius-2);
text-align: center;
&[data-highlighted] {
background-color: var(--color-primary);
color: var(--color-icon-white);
.dx-SelectItemIndicator {
color: var(--color-icon-white);
}
}
&[data-disabled] {
color: var(--color-text-disabled);
cursor: not-allowed;
}
}
.dx-SelectItemIndicator {
position: absolute;
left: 0;
width: var(--dx-select-item-indicator-width);
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--color-primary);
}
.dx-SelectItemText {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: inherit;
}
/* Label, Group, Separator */
.dx-SelectLabel {
display: flex;
align-items: center;
height: var(--dx-select-item-height);
padding-left: var(--dx-select-item-indicator-width);
padding-right: var(--dx-space-2);
color: var(--color-text-secondary);
font-size: 12px;
font-weight: 500;
user-select: none;
cursor: default;
}
.dx-SelectSeparator {
height: 1px;
background-color: var(--island-border-color);
margin: var(--dx-space-1) 0;
}
/* Size Variants */
.dx-r-size-1 {
--dx-select-trigger-height: 24px;
--dx-select-item-height: 24px;
font-size: 12px;
&.dx-SelectItem {
font-size: 12px;
}
}
.dx-r-size-2 {
--dx-select-trigger-height: 32px;
--dx-select-item-height: 28px; /* Radix often uses slightly smaller items */
font-size: 14px;
}
.dx-r-size-3 {
--dx-select-trigger-height: 40px;
--dx-select-item-height: 32px;
font-size: 16px;
&.dx-SelectItem {
font-size: 16px;
}
}
}
================================================
FILE: packages/drawnix/src/components/select/select.tsx
================================================
import * as React from 'react';
import classNames from 'classnames';
import {
autoUpdate,
flip,
FloatingFocusManager,
FloatingList,
FloatingPortal,
offset,
Placement,
shift,
useClick,
useDismiss,
useFloating,
useInteractions,
useListItem,
useListNavigation,
useMergeRefs,
useRole,
useTypeahead,
} from '@floating-ui/react';
import { ChevronDownIcon, ThickCheckIcon } from '../icons';
import './select.scss';
type SelectValueType = string;
interface SelectContextType {
open: boolean;
setOpen: (open: boolean) => void;
value: SelectValueType | undefined;
setValue: (value: SelectValueType) => void;
activeIndex: number | null;
setActiveIndex: (index: number | null) => void;
selectedIndex: number | null;
elementsRef: React.MutableRefObject<Array<HTMLElement | null>>;
labelsRef: React.MutableRefObject<Array<string | null>>;
valuesRef: React.MutableRefObject<Array<SelectValueType | null>>;
getReferenceProps: ReturnType<typeof useInteractions>['getReferenceProps'];
getFloatingProps: ReturnType<typeof useInteractions>['getFloatingProps'];
getItemProps: ReturnType<typeof useInteractions>['getItemProps'];
refs: ReturnType<typeof useFloating>['refs'];
floatingStyles: React.CSSProperties;
floatingContext: ReturnType<typeof useFloating>['context'];
size: '1' | '2' | '3';
hideSelectedIndicator: boolean;
disableItemHoverHighlight: boolean;
}
const SelectContext = React.createContext<SelectContextType | null>(null);
const useSelectContext = () => {
const context = React.useContext(SelectContext);
if (!context) {
throw new Error('Select components must be wrapped in <Select.Root />');
}
return context;
};
interface SelectRootProps {
children: React.ReactNode;
value?: string;
defaultValue?: string;
onValueChange?: (value: string) => void;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
size?: '1' | '2' | '3';
disabled?: boolean;
placement?: Placement;
sideOffset?: number;
hideSelectedIndicator?: boolean;
disableItemHoverHighlight?: boolean;
disableInitialHighlight?: boolean;
disableTypeahead?: boolean;
}
const SelectRoot: React.FC<SelectRootProps> = ({
children,
value: controlledValue,
defaultValue,
onValueChange,
open: controlledOpen,
defaultOpen = false,
onOpenChange: setControlledOpen,
size = '2',
disabled = false,
placement = 'bottom-start',
sideOffset = 4,
hideSelectedIndicator = false,
disableItemHoverHighlight = false,
disableInitialHighlight = false,
disableTypeahead = false,
}) => {
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen);
const open = controlledOpen ?? uncontrolledOpen;
const setOpen = setControlledOpen ?? setUncontrolledOpen;
const [uncontrolledValue, setUncontrolledValue] = React.useState<
string | undefined
>(defaultValue);
const value = controlledValue ?? uncontrolledValue;
const setValue = React.useCallback(
(nextValue: string) => {
if (controlledValue === undefined) {
setUncontrolledValue(nextValue);
}
onValueChange?.(nextValue);
},
[controlledValue, onValueChange]
);
const elementsRef = React.useRef<Array<HTMLElement | null>>([]);
const labelsRef = React.useRef<Array<string | null>>([]);
const valuesRef = React.useRef<Array<SelectValueType | null>>([]);
const [activeIndex, setActiveIndex] = React.useState<number | null>(null);
const selectedIndex = React.useMemo(() => {
if (value == null) return null;
const index = valuesRef.current.findIndex((v) => v === value);
return index >= 0 ? index : null;
}, [value]);
const navigationSelectedIndex = disableInitialHighlight ? null : selectedIndex;
React.useEffect(() => {
if (!open) return;
if (disableInitialHighlight) {
setActiveIndex(null);
return;
}
setActiveIndex(selectedIndex ?? 0);
}, [open, selectedIndex, disableInitialHighlight]);
const { refs, floatingStyles, context } = useFloating({
placement,
open,
onOpenChange: setOpen,
whileElementsMounted: autoUpdate,
middleware: [
offset(sideOffset),
flip({ padding: 5 }),
shift({ padding: 5 }),
],
});
const click = useClick(context, { enabled: !disabled && controlledOpen === undefined });
const dismiss = useDismiss(context);
const role = useRole(context, { role: 'listbox' });
const listNavigation = useListNavigation(context, {
listRef: elementsRef,
activeIndex,
selectedIndex: navigationSelectedIndex,
onNavigate: setActiveIndex,
loop: true,
focusItemOnHover: !disableItemHoverHighlight,
});
const typeahead = useTypeahead(context, {
listRef: labelsRef,
activeIndex,
selectedIndex: navigationSelectedIndex,
onMatch: setActiveIndex,
});
const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([
click,
dismiss,
role,
listNavigation,
disableTypeahead ? ({} as any) : typeahead,
]);
const contextValue = React.useMemo(
() => ({
open,
setOpen,
value,
setValue,
activeIndex,
setActiveIndex,
selectedIndex,
elementsRef,
labelsRef,
valuesRef,
getReferenceProps,
getFloatingProps,
getItemProps,
refs,
floatingStyles,
floatingContext: context,
size,
hideSelectedIndicator,
disableItemHoverHighlight,
}),
[
open,
setOpen,
value,
setValue,
activeIndex,
selectedIndex,
getReferenceProps,
getFloatingProps,
getItemProps,
refs,
floatingStyles,
context,
size,
hideSelectedIndicator,
disableItemHoverHighlight,
]
);
return (
<SelectContext.Provider value={contextValue}>
{children}
</SelectContext.Provider>
);
};
SelectRoot.displayName = 'Select.Root';
interface SelectTriggerProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'classic' | 'surface' | 'soft' | 'ghost';
color?: string;
radius?: 'none' | 'small' | 'medium' | 'large' | 'full';
placeholder?: string;
asChild?: boolean;
}
const SelectTrigger = React.forwardRef<HTMLButtonElement, SelectTriggerProps>(
(
{
children,
className,
variant = 'surface',
color,
radius,
placeholder,
asChild,
...props
},
forwardedRef
) => {
const context = useSelectContext();
const mergedRef = useMergeRefs([context.refs.setReference, forwardedRef]);
if (asChild && React.isValidElement(children)) {
return React.cloneElement(children, {
ref: mergedRef,
...context.getReferenceProps(props),
'data-state': context.open ? 'open' : 'closed',
} as any);
}
// Value rendering logic
let content = children;
if (!content && context.value) {
// Find label for value
const index = context.valuesRef.current.indexOf(context.value);
if (index !== -1) {
content = context.labelsRef.current[index];
} else {
content = context.value;
}
}
const shouldShowPlaceholder = !content && placeholder;
const displayContent = shouldShowPlaceholder ? placeholder : content;
return (
<button
type="button"
ref={mergedRef}
className={classNames(
'dx-reset',
'dx-SelectTrigger',
`dx-r-size-${context.size}`,
`dx-variant-${variant}`,
className
)}
data-state={context.open ? 'open' : 'closed'}
data-placeholder={shouldShowPlaceholder ? '' : undefined}
{...context.getReferenceProps(props)}
>
<span className="dx-SelectTriggerInner">{displayContent}</span>
<span className="dx-SelectIcon">
{ChevronDownIcon}
</span>
</button>
);
}
);
SelectTrigger.displayName = 'Select.Trigger';
interface SelectContentProps extends React.HTMLAttributes<HTMLDivElement> {
variant?: 'solid' | 'soft';
color?: string;
highContrast?: boolean;
container?: HTMLElement | null;
}
const SelectContent = React.forwardRef<HTMLDivElement, SelectContentProps>(
(
{
children,
className,
variant = 'solid',
color,
highContrast,
container,
style,
...props
},
forwardedRef
) => {
const context = useSelectContext();
const mergedRef = useMergeRefs([context.refs.setFloating, forwardedRef]);
if (!context.open) return null;
return (
<FloatingPortal root={container}>
<FloatingFocusManager context={context.floatingContext} initialFocus={-1}>
<div
ref={mergedRef}
className={classNames(
'dx-SelectContent',
`dx-r-size-${context.size}`,
`dx-variant-${variant}`,
className
)}
data-hide-selected-indicator={context.hideSelectedIndicator ? '' : undefined}
style={{ ...context.floatingStyles, ...style }}
{...context.getFloatingProps(props)}
>
<FloatingList elementsRef={context.elementsRef} labelsRef={context.labelsRef}>
<div className="dx-SelectViewport">
{children}
</div>
</FloatingList>
</div>
</FloatingFocusManager>
</FloatingPortal>
);
}
);
SelectContent.displayName = 'Select.Content';
interface SelectItemProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
value: string;
textValue?: string;
}
const SelectItem = React.forwardRef<HTMLButtonElement, SelectItemProps>(
({ children, className, value, textValue, disabled, ...props }, forwardedRef) => {
const context = useSelectContext();
const { ref: itemRef, index } = useListItem({
label: textValue ?? (typeof children === 'string' ? children : value),
});
const mergedRef = useMergeRefs([itemRef, forwardedRef]);
const isActive = context.activeIndex === index;
const isSelected = context.value === value;
React.useEffect(() => {
if (index !== null) {
context.valuesRef.current[index] = value;
// Best effort to get label
context.labelsRef.current[index] = textValue ?? (typeof children === 'string' ? children : value);
}
}, [index, value, textValue, children, context.valuesRef, context.labelsRef]);
const handleSelect = () => {
context.setValue(value);
context.setOpen(false);
};
const mergedItemProps = context.getItemProps({
...props,
onClick: (e: React.MouseEvent<HTMLElement>) => {
props.onClick?.(e as React.MouseEvent<HTMLButtonElement>);
handleSelect();
},
onKeyDown: (e: React.KeyboardEvent<HTMLElement>) => {
props.onKeyDown?.(e as React.KeyboardEvent<HTMLButtonElement>);
if (e.key === 'Enter') {
e.preventDefault();
handleSelect();
}
},
});
const {
onPointerMove,
onMouseMove,
onMouseEnter,
onMouseLeave,
...restMergedItemProps
} = mergedItemProps as React.ButtonHTMLAttributes<HTMLButtonElement>;
return (
<button
ref={mergedRef}
type="button"
role="option"
aria-selected={isSelected}
data-highlighted={isActive ? '' : undefined}
data-state={isSelected ? 'checked' : 'unchecked'}
data-disabled={disabled ? '' : undefined}
tabIndex={isActive ? 0 : -1}
className={classNames('dx-SelectItem', className)}
disabled={disabled}
{...restMergedItemProps}
{...(context.disableItemHoverHighlight
? {}
: { onPointerMove, onMouseMove, onMouseEnter, onMouseLeave })}
>
{!context.hideSelectedIndicator && (
<span className="dx-SelectItemIndicator">
{isSelected && ThickCheckIcon}
</span>
)}
<span className="dx-SelectItemText">{children}</span>
</button>
);
}
);
SelectItem.displayName = 'Select.Item';
type SelectGroupProps = React.HTMLAttributes<HTMLDivElement>
const SelectGroup = React.forwardRef<HTMLDivElement, SelectGroupProps>(
({ className, ...props }, ref) => (
<div ref={ref} className={classNames('dx-SelectGroup', className)} {...props} />
)
);
SelectGroup.displayName = 'Select.Group';
type SelectLabelProps = React.HTMLAttributes<HTMLDivElement>
const SelectLabel = React.forwardRef<HTMLDivElement, SelectLabelProps>(
({ className, ...props }, ref) => (
<div ref={ref} className={classNames('dx-SelectLabel', className)} {...props} />
)
);
SelectLabel.displayName = 'Select.Label';
type SelectSeparatorProps = React.HTMLAttributes<HTMLDivElement>
const SelectSeparator = React.forwardRef<HTMLDivElement, SelectSeparatorProps>(
({ className, ...props }, ref) => (
<div ref={ref} className={classNames('dx-SelectSeparator', className)} {...props} />
)
);
SelectSeparator.displayName = 'Select.Separator';
export const Select = {
Root: SelectRoot,
Trigger: SelectTrigger,
Content: SelectContent,
Item: SelectItem,
Group: SelectGroup,
Label: SelectLabel,
Separator: SelectSeparator,
};
================================================
FILE: packages/drawnix/src/components/shape-picker.tsx
================================================
import classNames from 'classnames';
import { Island } from './island';
import Stack from './stack';
import { ToolButton } from './tool-button';
import {
RectangleIcon,
EllipseIcon,
TriangleIcon,
DiamondIcon,
ParallelogramIcon,
RoundRectangleIcon,
TerminalIcon,
NoteCurlyLeftIcon,
NoteCurlyRightIcon,
} from './icons';
import { BoardTransforms, PlaitBoard } from '@plait/core';
import React from 'react';
import { BoardCreationMode, setCreationMode } from '@plait/common';
import { BasicShapes, DrawPointerType, FlowchartSymbols } from '@plait/draw';
import { useBoard } from '@plait-board/react-board';
import { Translations, useI18n } from '../i18n';
import { splitRows } from '../utils/common';
export interface ShapeProps {
icon: React.ReactNode;
title: string;
pointer: DrawPointerType;
}
export const SHAPES: ShapeProps[] = [
{
icon: RectangleIcon,
title: 'toolbar.shape.rectangle',
pointer: BasicShapes.rectangle,
},
{
icon: EllipseIcon,
title: 'toolbar.shape.ellipse',
pointer: BasicShapes.ellipse,
},
{
icon: TriangleIcon,
title: 'toolbar.shape.triangle',
pointer: BasicShapes.triangle,
},
{
icon: TerminalIcon,
title: 'toolbar.shape.terminal',
pointer: FlowchartSymbols.terminal,
},
{
icon: NoteCurlyRightIcon,
title: 'toolbar.shape.noteCurlyRight',
pointer: FlowchartSymbols.noteCurlyRight,
},
{
icon: NoteCurlyLeftIcon,
title: 'toolbar.shape.noteCurlyLeft',
pointer: FlowchartSymbols.noteCurlyLeft,
},
{
icon: DiamondIcon,
title: 'toolbar.shape.diamond',
pointer: BasicShapes.diamond,
},
{
icon: ParallelogramIcon,
title: 'toolbar.shape.parallelogram',
pointer: BasicShapes.parallelogram,
},
{
icon: RoundRectangleIcon,
title: 'toolbar.shape.roundRectangle',
pointer: BasicShapes.roundRectangle,
},
];
const ROW_SHAPES = splitRows(SHAPES, 5);
export type ShapePickerProps = {
onPointerUp: (pointer: DrawPointerType) => void;
};
export const ShapePicker: React.FC<ShapePickerProps> = ({ onPointerUp }) => {
const board = useBoard();
const { t } = useI18n();
return (
<Island padding={1}>
<Stack.Col gap={1}>
{ROW_SHAPES.map((rowShapes, rowIndex) => {
return (
<Stack.Row gap={1} key={rowIndex}>
{rowShapes.map((shape, index) => {
return (
<ToolButton
key={index}
className={classNames({ fillable: false })}
type="icon"
size={'small'}
visible={true}
selected={PlaitBoard.isPointer(board, shape.pointer)}
icon={shape.icon}
title={t(
(shape.title || 'toolbar.shape') as keyof Translations
)}
aria-label={t(
(shape.title || 'toolbar.shape') as keyof Translations
)}
onPointerDown={() => {
setCreationMode(board, BoardCreationMode.dnd);
BoardTransforms.updatePointerType(board, shape.pointer);
}}
onPointerUp={() => {
setCreationMode(board, BoardCreationMode.drawing);
onPointerUp(shape.pointer);
}}
/>
);
})}
</Stack.Row>
);
})}
</Stack.Col>
</Island>
);
};
================================================
FILE: packages/drawnix/src/components/size-slider.scss
================================================
@import "open-color/open-color.scss";
.slider-container {
padding: 10px 0px;
&.disabled {
opacity: 50%;
cursor: not-allowed;
.slider-track,.slider-thumb {
cursor: not-allowed;
}
}
.slider-track {
position: relative;
height: 4px;
background-color: var(--color-gray-20);
border-radius: 2px;
cursor: pointer;
}
.slider-range {
position: absolute;
height: 100%;
background-color: var(--color-primary);
border-radius: 3px;
}
.slider-thumb {
position: absolute;
width: 12px;
height: 12px;
background-color: $oc-white;
border: 2px solid var(--color-primary);
border-radius: 50%;
top: 50%;
transform: translate(-50%, -50%);
cursor: grab;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1),
}
}
================================================
FILE: packages/drawnix/src/components/size-slider.tsx
================================================
import React, {
useState,
useRef,
useCallback,
useEffect,
useMemo,
} from 'react';
import { toFixed } from '@plait/core';
import './size-slider.scss';
import classNames from 'classnames';
import { throttle } from 'lodash';
interface SliderProps {
min?: number;
max?: number;
step?: number;
defaultValue?: number;
disabled?: boolean;
title?: string;
onChange?: (value: number) => void;
beforeStart?: () => void;
afterEnd?: () => void;
}
export const SizeSlider: React.FC<SliderProps> = ({
min = 0,
max = 100,
step = 1,
defaultValue = 100,
disabled = false,
onChange,
beforeStart,
afterEnd,
title,
}) => {
const [isDragging, setIsDragging] = useState(false);
const [value, setValue] = useState(defaultValue);
const sliderRef = useRef<HTMLDivElement>(null);
const thumbRef = useRef<HTMLDivElement>(null);
const percentage = ((value - min) / (max - min)) * 100;
const handleSliderChange = useCallback(
throttle(
(event: React.MouseEvent<HTMLDivElement> | MouseEvent) => {
if (sliderRef.current && thumbRef.current) {
const sliderRect = sliderRef.current.getBoundingClientRect();
const thumbRect = thumbRef.current.getBoundingClientRect();
const x = event.clientX - sliderRect.left;
const thumbPercentage = toFixed(
(thumbRect.width / 2 / sliderRect.width) * 100
);
let percentage = Math.min(Math.max(x / sliderRect.width, 0), 1);
if (percentage >= (100 - thumbPercentage) / 100) {
percentage = 1;
} else if (percentage <= thumbPercentage / 100) {
percentage = 0;
}
const newValue =
Math.round((percentage * (max - min)) / step) * step + min;
setValue(newValue);
onChange && onChange(newValue);
}
},
50,
{ leading: true, trailing: true }
),
[min, max, step, onChange]
);
const handlePointerDown = useCallback(() => {
const handleMouseMove = (e: MouseEvent) => {
setIsDragging(true);
handleSliderChange(e);
};
const handleMouseUp = () => {
document.removeEventListener('pointermove', handleMouseMove);
document.removeEventListener('pointerup', handleMouseUp);
afterEnd && afterEnd();
setTimeout(() => {
setIsDragging(false);
}, 0);
};
document.addEventListener('pointermove', handleMouseMove);
document.addEventListener('pointerup', handleMouseUp);
}, [handleSliderChange]);
useEffect(()=>{
setValue(defaultValue)
},[defaultValue])
return (
<div
data-tooltip
title={title}
className={classNames('slider-container', { disabled: disabled })}
>
<div
ref={sliderRef}
className="slider-track"
onClick={(event) => {
if (disabled || isDragging) {
return;
}
handleSliderChange(event);
}}
onPointerDown={(event) => {
event.preventDefault();
if (disabled) {
return;
}
beforeStart && beforeStart();
handlePointerDown();
}}
>
<div
className="slider-range"
style={{
width: `${percentage}%`,
}}
/>
<div
ref={thumbRef}
className="slider-thumb"
style={{
left: `${percentage}%`,
}}
/>
</div>
</div>
);
};
================================================
FILE: packages/drawnix/src/components/stack.scss
================================================
.drawnix {
.stack {
--gap: 0;
display: grid;
gap: calc(var(--space-factor) * var(--gap));
}
.stack_vertical {
grid-template-columns: auto;
grid-auto-flow: row;
grid-auto-rows: min-content;
}
.stack_horizontal {
grid-template-rows: auto;
grid-auto-flow: column;
grid-auto-columns: min-content;
}
}
================================================
FILE: packages/drawnix/src/components/stack.tsx
================================================
// Credits to excalidraw
import "./stack.scss";
import React, { forwardRef } from "react";
import clsx from "classnames";
type StackProps = {
children: React.ReactNode;
gap?: number;
align?: "start" | "center" | "end" | "baseline";
justifyContent?: "center" | "space-around" | "space-between";
className?: string | boolean;
style?: React.CSSProperties;
ref: React.RefObject<HTMLDivElement>;
};
const RowStack = forwardRef(
(
{ children, gap, align, justifyContent, className, style }: StackProps,
ref: React.ForwardedRef<HTMLDivElement>,
) => {
return (
<div
className={clsx("stack stack_horizontal", className)}
style={{
"--gap": gap,
alignItems: align,
justifyContent,
...style,
}}
ref={ref}
>
{children}
</div>
);
},
);
const ColStack = forwardRef(
(
{ children, gap, align, justifyContent, className, style }: StackProps,
ref: React.ForwardedRef<HTMLDivElement>,
) => {
return (
<div
className={clsx("stack stack_vertical", className)}
style={{
"--gap": gap,
justifyItems: align,
justifyContent,
...style,
}}
ref={ref}
>
{children}
</div>
);
},
);
export default {
Row: RowStack,
Col: ColStack,
};
================================================
FILE: packages/drawnix/src/components/tool-button.tsx
================================================
// Credits to excalidraw
import './tool-icon.scss';
import type { CSSProperties } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { AbortError } from '../errors';
import { isPromis
gitextract_iabkvbp0/ ├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github/ │ └── workflows/ │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode/ │ ├── extensions.json │ └── settings.json ├── CFPAGE-DEPLOY.md ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── README_en.md ├── SECURITY.md ├── apps/ │ ├── web/ │ │ ├── .eslintrc.json │ │ ├── index.html │ │ ├── jest.config.ts │ │ ├── project.json │ │ ├── public/ │ │ │ ├── _headers │ │ │ ├── _redirects │ │ │ ├── robots.txt │ │ │ └── sitemap.xml │ │ ├── src/ │ │ │ ├── app/ │ │ │ │ ├── app.module.scss │ │ │ │ ├── app.spec.tsx │ │ │ │ └── app.tsx │ │ │ ├── assets/ │ │ │ │ └── .gitkeep │ │ │ ├── main.tsx │ │ │ └── styles.scss │ │ ├── tsconfig.app.json │ │ ├── tsconfig.json │ │ ├── tsconfig.spec.json │ │ └── vite.config.ts │ └── web-e2e/ │ ├── .eslintrc.json │ ├── playwright.config.ts │ ├── project.json │ ├── src/ │ │ └── example.spec.ts │ └── tsconfig.json ├── jest.config.ts ├── jest.preset.js ├── nx.json ├── package.json ├── packages/ │ ├── drawnix/ │ │ ├── .babelrc │ │ ├── .eslintrc.json │ │ ├── README.md │ │ ├── jest.config.ts │ │ ├── package.json │ │ ├── project.json │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ ├── arrow-mark-picker.tsx │ │ │ │ ├── arrow-picker.tsx │ │ │ │ ├── clean-confirm/ │ │ │ │ │ ├── clean-confirm.scss │ │ │ │ │ └── clean-confirm.tsx │ │ │ │ ├── color-picker.scss │ │ │ │ ├── color-picker.tsx │ │ │ │ ├── dialog/ │ │ │ │ │ ├── dialog.scss │ │ │ │ │ └── dialog.tsx │ │ │ │ ├── icons.tsx │ │ │ │ ├── island.scss │ │ │ │ ├── island.tsx │ │ │ │ ├── menu/ │ │ │ │ │ ├── common.ts │ │ │ │ │ ├── menu-group.tsx │ │ │ │ │ ├── menu-item-content-radio.tsx │ │ │ │ │ ├── menu-item-content.tsx │ │ │ │ │ ├── menu-item-custom.tsx │ │ │ │ │ ├── menu-item-link.tsx │ │ │ │ │ ├── menu-item.tsx │ │ │ │ │ ├── menu-separator.tsx │ │ │ │ │ ├── menu.scss │ │ │ │ │ └── menu.tsx │ │ │ │ ├── popover/ │ │ │ │ │ └── popover.tsx │ │ │ │ ├── popup/ │ │ │ │ │ └── link-popup/ │ │ │ │ │ ├── link-popup.scss │ │ │ │ │ └── link-popup.tsx │ │ │ │ ├── radio-group.scss │ │ │ │ ├── radio-group.tsx │ │ │ │ ├── select/ │ │ │ │ │ ├── select.scss │ │ │ │ │ └── select.tsx │ │ │ │ ├── shape-picker.tsx │ │ │ │ ├── size-slider.scss │ │ │ │ ├── size-slider.tsx │ │ │ │ ├── stack.scss │ │ │ │ ├── stack.tsx │ │ │ │ ├── tool-button.tsx │ │ │ │ ├── tool-icon.scss │ │ │ │ ├── toolbar/ │ │ │ │ │ ├── app-toolbar/ │ │ │ │ │ │ ├── app-menu-items.tsx │ │ │ │ │ │ ├── app-toolbar.tsx │ │ │ │ │ │ └── language-switcher-menu.tsx │ │ │ │ │ ├── creation-toolbar.tsx │ │ │ │ │ ├── extra-tools/ │ │ │ │ │ │ ├── extra-tools-button.tsx │ │ │ │ │ │ └── menu-items.tsx │ │ │ │ │ ├── freehand-panel/ │ │ │ │ │ │ └── freehand-panel.tsx │ │ │ │ │ ├── pencil-mode-toolbar.tsx │ │ │ │ │ ├── popup-toolbar/ │ │ │ │ │ │ ├── arrow-mark-button.tsx │ │ │ │ │ │ ├── fill-button.tsx │ │ │ │ │ │ ├── font-color-button.tsx │ │ │ │ │ │ ├── font-size-control.tsx │ │ │ │ │ │ ├── link-button.tsx │ │ │ │ │ │ ├── popup-toolbar.scss │ │ │ │ │ │ ├── popup-toolbar.tsx │ │ │ │ │ │ └── stroke-button.tsx │ │ │ │ │ ├── theme-toolbar.tsx │ │ │ │ │ └── zoom-toolbar.tsx │ │ │ │ ├── ttd-dialog/ │ │ │ │ │ ├── markdown-to-drawnix.tsx │ │ │ │ │ ├── mermaid-to-drawnix.scss │ │ │ │ │ ├── mermaid-to-drawnix.tsx │ │ │ │ │ ├── ttd-dialog-input.tsx │ │ │ │ │ ├── ttd-dialog-output.tsx │ │ │ │ │ ├── ttd-dialog-panel.tsx │ │ │ │ │ ├── ttd-dialog-panels.tsx │ │ │ │ │ ├── ttd-dialog-submit-shortcut.tsx │ │ │ │ │ ├── ttd-dialog.scss │ │ │ │ │ └── ttd-dialog.tsx │ │ │ │ ├── tutorial.scss │ │ │ │ └── tutorial.tsx │ │ │ ├── constants/ │ │ │ │ └── color.ts │ │ │ ├── constants.ts │ │ │ ├── css.d.ts │ │ │ ├── data/ │ │ │ │ ├── blob.ts │ │ │ │ ├── filesystem.ts │ │ │ │ ├── image.ts │ │ │ │ ├── json.ts │ │ │ │ └── types.ts │ │ │ ├── drawnix.spec.tsx │ │ │ ├── drawnix.tsx │ │ │ ├── errors.ts │ │ │ ├── hooks/ │ │ │ │ └── use-drawnix.tsx │ │ │ ├── i18n/ │ │ │ │ ├── index.tsx │ │ │ │ ├── translations/ │ │ │ │ │ ├── ar.ts │ │ │ │ │ ├── en.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── ru.ts │ │ │ │ │ ├── vi.ts │ │ │ │ │ └── zh.ts │ │ │ │ └── types.ts │ │ │ ├── i18n.tsx │ │ │ ├── index.ts │ │ │ ├── keys.ts │ │ │ ├── libs/ │ │ │ │ └── image-viewer.ts │ │ │ ├── plugins/ │ │ │ │ ├── components/ │ │ │ │ │ ├── emoji.tsx │ │ │ │ │ └── image.tsx │ │ │ │ ├── freehand/ │ │ │ │ │ ├── freehand.component.ts │ │ │ │ │ ├── freehand.generator.ts │ │ │ │ │ ├── smoother.ts │ │ │ │ │ ├── type.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ ├── with-freehand-create.ts │ │ │ │ │ ├── with-freehand-erase.ts │ │ │ │ │ ├── with-freehand-fragment.ts │ │ │ │ │ └── with-freehand.ts │ │ │ │ ├── with-common.tsx │ │ │ │ ├── with-hotkey.ts │ │ │ │ ├── with-image.tsx │ │ │ │ ├── with-mind-extend.tsx │ │ │ │ ├── with-pencil.ts │ │ │ │ └── with-text-link.tsx │ │ │ ├── styles/ │ │ │ │ ├── index.scss │ │ │ │ ├── theme.scss │ │ │ │ └── variables.module.scss │ │ │ ├── transforms/ │ │ │ │ └── property.ts │ │ │ ├── types.ts │ │ │ └── utils/ │ │ │ ├── color.ts │ │ │ ├── common.ts │ │ │ ├── image.ts │ │ │ ├── index.ts │ │ │ ├── laser-pointer.ts │ │ │ ├── property.ts │ │ │ └── utility-types.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.lib.json │ │ ├── tsconfig.spec.json │ │ └── vite.config.ts │ ├── react-board/ │ │ ├── .babelrc │ │ ├── .eslintrc.json │ │ ├── README.md │ │ ├── jest.config.ts │ │ ├── package.json │ │ ├── project.json │ │ ├── src/ │ │ │ ├── board.spec.tsx │ │ │ ├── board.tsx │ │ │ ├── hooks/ │ │ │ │ ├── use-board-event.ts │ │ │ │ ├── use-board.tsx │ │ │ │ └── use-plugin-event.tsx │ │ │ ├── index.ts │ │ │ ├── plugins/ │ │ │ │ ├── board.ts │ │ │ │ ├── with-pinch-zoom-plugin.ts │ │ │ │ └── with-react.tsx │ │ │ ├── styles/ │ │ │ │ ├── index.scss │ │ │ │ └── mixins.scss │ │ │ └── wrapper.tsx │ │ ├── tsconfig.json │ │ ├── tsconfig.lib.json │ │ ├── tsconfig.spec.json │ │ └── vite.config.ts │ └── react-text/ │ ├── .babelrc │ ├── .eslintrc.json │ ├── README.md │ ├── jest.config.ts │ ├── package.json │ ├── project.json │ ├── src/ │ │ ├── custom-types.ts │ │ ├── index.ts │ │ ├── plugins/ │ │ │ ├── index.ts │ │ │ ├── with-link.tsx │ │ │ └── with-text.ts │ │ ├── styles/ │ │ │ └── index.scss │ │ ├── text.spec.tsx │ │ └── text.tsx │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── vite.config.ts ├── scripts/ │ ├── publish.js │ └── release-version.js └── tsconfig.base.json
SYMBOL INDEX (182 symbols across 58 files)
FILE: apps/web/src/app/app.tsx
type AppValue (line 6) | type AppValue = {
constant MAIN_BOARD_CONTENT_KEY (line 12) | const MAIN_BOARD_CONTENT_KEY = 'main_board_content';
function App (line 20) | function App() {
FILE: packages/drawnix/src/components/arrow-mark-picker.tsx
type ArrowMarkerPickerProps (line 13) | type ArrowMarkerPickerProps = {
FILE: packages/drawnix/src/components/arrow-picker.tsx
type ArrowProps (line 13) | interface ArrowProps {
constant ARROWS (line 19) | const ARROWS: ArrowProps[] = [
type ArrowPickerProps (line 37) | type ArrowPickerProps = {
FILE: packages/drawnix/src/components/color-picker.tsx
constant ROWS_CLASSIC_COLORS (line 29) | const ROWS_CLASSIC_COLORS = splitRows(CLASSIC_COLORS, 4);
type ColorPickerProps (line 31) | type ColorPickerProps = {
FILE: packages/drawnix/src/components/dialog/dialog.tsx
type DialogOptions (line 16) | interface DialogOptions {
function useDialog (line 22) | function useDialog({
type ContextType (line 66) | type ContextType =
function Dialog (line 87) | function Dialog({
type DialogTriggerProps (line 99) | interface DialogTriggerProps {
FILE: packages/drawnix/src/components/island.tsx
type IslandProps (line 7) | type IslandProps = {
FILE: packages/drawnix/src/components/menu/menu-item-content-radio.tsx
type Props (line 3) | type Props<T> = {
FILE: packages/drawnix/src/components/popover/popover.tsx
type PopoverOptions (line 18) | interface PopoverOptions {
function usePopover (line 27) | function usePopover({
type ContextType (line 86) | type ContextType =
function Popover (line 107) | function Popover({
type PopoverTriggerProps (line 124) | interface PopoverTriggerProps {
FILE: packages/drawnix/src/components/popup/link-popup/link-popup.tsx
method getBoundingClientRect (line 52) | getBoundingClientRect() {
FILE: packages/drawnix/src/components/radio-group.tsx
type RadioGroupChoice (line 4) | type RadioGroupChoice<T> = {
type RadioGroupProps (line 10) | type RadioGroupProps<T> = {
FILE: packages/drawnix/src/components/select/select.tsx
type SelectValueType (line 25) | type SelectValueType = string;
type SelectContextType (line 27) | interface SelectContextType {
type SelectRootProps (line 59) | interface SelectRootProps {
type SelectTriggerProps (line 223) | interface SelectTriggerProps
type SelectContentProps (line 297) | interface SelectContentProps extends React.HTMLAttributes<HTMLDivElement> {
type SelectItemProps (line 351) | interface SelectItemProps extends React.ButtonHTMLAttributes<HTMLButtonE...
type SelectGroupProps (line 432) | type SelectGroupProps = React.HTMLAttributes<HTMLDivElement>
type SelectLabelProps (line 440) | type SelectLabelProps = React.HTMLAttributes<HTMLDivElement>
type SelectSeparatorProps (line 448) | type SelectSeparatorProps = React.HTMLAttributes<HTMLDivElement>
FILE: packages/drawnix/src/components/shape-picker.tsx
type ShapeProps (line 24) | interface ShapeProps {
constant SHAPES (line 30) | const SHAPES: ShapeProps[] = [
constant ROW_SHAPES (line 78) | const ROW_SHAPES = splitRows(SHAPES, 5);
type ShapePickerProps (line 80) | type ShapePickerProps = {
FILE: packages/drawnix/src/components/size-slider.tsx
type SliderProps (line 13) | interface SliderProps {
FILE: packages/drawnix/src/components/stack.tsx
type StackProps (line 7) | type StackProps = {
FILE: packages/drawnix/src/components/tool-button.tsx
type ToolButtonSize (line 11) | type ToolButtonSize = 'small' | 'medium';
type ToolButtonBaseProps (line 13) | type ToolButtonBaseProps = {
type ToolButtonProps (line 38) | type ToolButtonProps =
FILE: packages/drawnix/src/components/toolbar/creation-toolbar.tsx
type PopupKey (line 49) | enum PopupKey {
type AppToolButtonProps (line 55) | type AppToolButtonProps = {
constant BUTTONS (line 69) | const BUTTONS: AppToolButtonProps[] = [
FILE: packages/drawnix/src/components/toolbar/freehand-panel/freehand-panel.tsx
type FreehandProps (line 20) | interface FreehandProps {
constant FREEHANDS (line 26) | const FREEHANDS: FreehandProps[] = [
constant ROW_FREEHANDS (line 39) | const ROW_FREEHANDS = splitRows(FREEHANDS, 5);
type FreehandPickerProps (line 41) | type FreehandPickerProps = {
FILE: packages/drawnix/src/components/toolbar/popup-toolbar/arrow-mark-button.tsx
type ArrowMarkButtonProps (line 12) | type ArrowMarkButtonProps = {
FILE: packages/drawnix/src/components/toolbar/popup-toolbar/fill-button.tsx
type PopupFillButtonProps (line 19) | type PopupFillButtonProps = {
FILE: packages/drawnix/src/components/toolbar/popup-toolbar/font-color-button.tsx
type PopupFontColorButtonProps (line 13) | type PopupFontColorButtonProps = {
FILE: packages/drawnix/src/components/toolbar/popup-toolbar/font-size-control.tsx
type PopupFontSizeControlProps (line 8) | type PopupFontSizeControlProps = {
constant DEFAULT_OPTIONS (line 15) | const DEFAULT_OPTIONS = [10, 12, 14, 18, 24, 36, 48];
constant MIN_FONT_SIZE (line 16) | const MIN_FONT_SIZE = 8;
constant MAX_FONT_SIZE (line 17) | const MAX_FONT_SIZE = 78;
FILE: packages/drawnix/src/components/toolbar/popup-toolbar/link-button.tsx
type PopupLinkButtonProps (line 12) | type PopupLinkButtonProps = {
FILE: packages/drawnix/src/components/toolbar/popup-toolbar/popup-toolbar.tsx
method getBoundingClientRect (line 125) | getBoundingClientRect() {
FILE: packages/drawnix/src/components/toolbar/popup-toolbar/stroke-button.tsx
type PopupStrokeButtonProps (line 30) | type PopupStrokeButtonProps = {
FILE: packages/drawnix/src/components/ttd-dialog/markdown-to-drawnix.tsx
type MarkdownToDrawnixLibProps (line 20) | interface MarkdownToDrawnixLibProps {
FILE: packages/drawnix/src/components/ttd-dialog/mermaid-to-drawnix.tsx
type MermaidToDrawnixLibProps (line 24) | interface MermaidToDrawnixLibProps {
constant MERMAID_EXAMPLE (line 34) | const MERMAID_EXAMPLE =
FILE: packages/drawnix/src/components/ttd-dialog/ttd-dialog-input.tsx
type TTDDialogInputProps (line 6) | interface TTDDialogInputProps {
FILE: packages/drawnix/src/components/ttd-dialog/ttd-dialog-output.tsx
type TTDDialogOutputProps (line 19) | interface TTDDialogOutputProps {
FILE: packages/drawnix/src/components/ttd-dialog/ttd-dialog-panel.tsx
type TTDDialogPanelProps (line 4) | interface TTDDialogPanelProps {
FILE: packages/drawnix/src/constants.ts
type EVENT (line 1) | enum EVENT {
constant IMAGE_MIME_TYPES (line 33) | const IMAGE_MIME_TYPES = {
constant MIME_TYPES (line 45) | const MIME_TYPES = {
constant VERSIONS (line 52) | const VERSIONS = {
FILE: packages/drawnix/src/constants/color.ts
constant TRANSPARENT (line 3) | const TRANSPARENT = 'TRANSPARENT';
constant NO_COLOR (line 5) | const NO_COLOR = 'NO_COLOR';
constant WHITE (line 7) | const WHITE = '#FFFFFF';
constant CLASSIC_COLORS (line 9) | const CLASSIC_COLORS = [
FILE: packages/drawnix/src/css.d.ts
type Properties (line 4) | interface Properties {
FILE: packages/drawnix/src/data/filesystem.ts
type FILE_EXTENSION (line 9) | type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, 'binary'>;
type RetType (line 17) | type RetType = M extends false | undefined ? File : File[];
FILE: packages/drawnix/src/data/types.ts
type DrawnixExportedData (line 3) | interface DrawnixExportedData {
type DrawnixExportedType (line 12) | enum DrawnixExportedType {
FILE: packages/drawnix/src/drawnix.tsx
type DrawnixProps (line 44) | type DrawnixProps = {
FILE: packages/drawnix/src/errors.ts
class AbortError (line 1) | class AbortError extends DOMException {
method constructor (line 2) | constructor(message = 'Request Aborted') {
FILE: packages/drawnix/src/hooks/use-drawnix.tsx
type DialogType (line 13) | enum DialogType {
type DrawnixPointerType (line 18) | type DrawnixPointerType =
type DrawnixBoard (line 24) | interface DrawnixBoard extends PlaitBoard {
type LinkState (line 28) | type LinkState = {
type DrawnixState (line 37) | type DrawnixState = {
FILE: packages/drawnix/src/i18n/types.ts
type Language (line 4) | type Language = 'zh' | 'en' | 'ru' | 'ar' | 'vi';
type Translations (line 7) | interface Translations {
type I18nContextType (line 166) | interface I18nContextType {
type I18nProviderProps (line 173) | interface I18nProviderProps {
FILE: packages/drawnix/src/keys.ts
constant CODES (line 3) | const CODES = {
constant KEYS (line 27) | const KEYS = {
type Key (line 84) | type Key = keyof typeof KEYS;
FILE: packages/drawnix/src/libs/image-viewer.ts
type ImageViewerOptions (line 1) | interface ImageViewerOptions {
type ImageState (line 8) | interface ImageState {
class ImageViewer (line 19) | class ImageViewer {
method constructor (line 42) | constructor(options: ImageViewerOptions = {}) {
method open (line 55) | open(src: string, alt = ''): void {
method close (line 63) | close(): void {
method createOverlay (line 95) | private createOverlay(): void {
method createCloseButton (line 124) | private createCloseButton(): void {
method createControls (line 133) | private createControls(): void {
method createImage (line 172) | private createImage(src: string, alt: string): void {
method bindDragEvents (line 206) | private bindDragEvents(): void {
method cleanupDragEvents (line 266) | private cleanupDragEvents(): void {
method bindEvents (line 276) | private bindEvents(): void {
method zoomIn (line 318) | private zoomIn(): void {
method zoomOut (line 327) | private zoomOut(): void {
method resetState (line 336) | private resetState(): void {
method updateImageTransform (line 344) | private updateImageTransform(): void {
method addStyles (line 355) | private addStyles(): void {
method removeStyles (line 404) | private removeStyles(): void {
method destroy (line 412) | destroy(): void {
FILE: packages/drawnix/src/plugins/freehand/freehand.component.ts
class FreehandComponent (line 18) | class FreehandComponent
method constructor (line 22) | constructor() {
method initializeGenerator (line 30) | initializeGenerator() {
method initialize (line 44) | initialize(): void {
method onContextChanged (line 50) | onContextChanged(
method destroy (line 77) | destroy(): void {
FILE: packages/drawnix/src/plugins/freehand/freehand.generator.ts
class FreehandGenerator (line 12) | class FreehandGenerator extends Generator<Freehand> {
method draw (line 13) | protected draw(element: Freehand): SVGGElement | undefined {
method canDraw (line 26) | canDraw(element: Freehand): boolean {
FILE: packages/drawnix/src/plugins/freehand/smoother.ts
type StrokePoint (line 3) | interface StrokePoint {
type SmootherOptions (line 11) | interface SmootherOptions {
class FreehandSmoother (line 23) | class FreehandSmoother {
method constructor (line 42) | constructor(options: SmootherOptions = {}) {
method process (line 46) | process(
method reset (line 99) | reset(): void {
method updatePoints (line 105) | private updatePoints(point: StrokePoint): void {
method checkDistance (line 112) | private checkDistance(point: Point): boolean {
method calculateDynamicParameters (line 128) | private calculateDynamicParameters(strokePoint: StrokePoint) {
method smooth (line 156) | private smooth(point: Point, params: Required<SmootherOptions>): Point {
method calculateWeights (line 174) | private calculateWeights(params: Required<SmootherOptions>): number[] {
method getDistance (line 201) | private getDistance(p1: Point, p2: Point): number {
method calculateVelocity (line 205) | private calculateVelocity(point: StrokePoint): number {
method updateMovingAverage (line 214) | private updateMovingAverage(velocity: number): void {
method getAverageVelocity (line 221) | private getAverageVelocity(): number {
method getPointVelocity (line 229) | private getPointVelocity(index: number): number {
method getPointCurvature (line 239) | private getPointCurvature(index: number): number {
FILE: packages/drawnix/src/plugins/freehand/type.ts
type FreehandShape (line 31) | enum FreehandShape {
constant FREEHAND_TYPE (line 39) | const FREEHAND_TYPE = 'freehand';
type Freehand (line 41) | type Freehand = PlaitCustomGeometry<typeof FREEHAND_TYPE, Point[], Freeh...
FILE: packages/drawnix/src/plugins/freehand/utils.ts
function getFreehandPointers (line 22) | function getFreehandPointers() {
function gaussianWeight (line 104) | function gaussianWeight(x: number, sigma: number) {
function gaussianSmooth (line 108) | function gaussianSmooth(
FILE: packages/drawnix/src/plugins/with-pencil.ts
constant IS_PENCIL_MODE (line 4) | const IS_PENCIL_MODE = new WeakMap<PlaitBoard, boolean>();
FILE: packages/drawnix/src/types.ts
type EventPointerType (line 1) | type EventPointerType = 'mouse' | 'pen' | 'touch';
type DataURL (line 3) | type DataURL = string & { _brand: 'DataURL' };
FILE: packages/drawnix/src/utils/color.ts
function transparencyToAlpha255 (line 5) | function transparencyToAlpha255(transparency: number) {
function alpha255ToTransparency (line 10) | function alpha255ToTransparency(alpha255: number) {
function applyOpacityToHex (line 14) | function applyOpacityToHex(hexColor: string, opacity: number) {
function hexAlphaToOpacity (line 20) | function hexAlphaToOpacity(hexColor: string) {
function isValidColor (line 39) | function isValidColor(color: string) {
function removeHexAlpha (line 46) | function removeHexAlpha(hexColor: string) {
function isTransparent (line 64) | function isTransparent(color?: string) {
function isWhite (line 68) | function isWhite(color?: string) {
function isFullyTransparent (line 72) | function isFullyTransparent(opacity: number) {
function isFullyOpaque (line 76) | function isFullyOpaque(opacity: number) {
function isNoColor (line 80) | function isNoColor(value: string) {
function isDefaultStroke (line 84) | function isDefaultStroke(color?: string) {
function getBackgroundColor (line 88) | function getBackgroundColor(board: PlaitBoard) {
FILE: packages/drawnix/src/utils/common.ts
function download (line 63) | function download(blob: Blob | MediaSource, filename: string) {
FILE: packages/drawnix/src/utils/laser-pointer.ts
constant LASER_POINTER_CLASS_NAME (line 14) | const LASER_POINTER_CLASS_NAME = 'laser-pointer';
class LaserPointer (line 28) | class LaserPointer {
method init (line 38) | public init(board: PlaitBoard): void {
method destroy (line 67) | public destroy(): void {
method startDraw (line 88) | private startDraw(): void {
method draw (line 95) | private draw(): void {
method setCanvasSize (line 131) | private setCanvasSize(): void {
FILE: packages/drawnix/src/utils/utility-types.ts
type ResolutionType (line 1) | type ResolutionType<T extends (...args: any) => any> = T extends (
type ValueOf (line 7) | type ValueOf<T> = T[keyof T];
FILE: packages/react-board/src/board.tsx
type PlaitBoardProps (line 30) | type PlaitBoardProps = {
FILE: packages/react-board/src/hooks/use-board.tsx
type BoardContextValue (line 9) | interface BoardContextValue {
FILE: packages/react-board/src/plugins/board.ts
type ReactBoard (line 10) | interface ReactBoard {
type BoardChangeData (line 18) | interface BoardChangeData {
FILE: packages/react-board/src/plugins/with-pinch-zoom-plugin.ts
type PointerRecord (line 12) | interface PointerRecord {
constant TOUCH_RECORDS (line 19) | const TOUCH_RECORDS = new WeakMap<PlaitBoard, PointerRecord[]>();
FILE: packages/react-board/src/wrapper.tsx
type WrapperProps (line 41) | type WrapperProps = {
FILE: packages/react-text/src/custom-types.ts
type CustomEditor (line 6) | type CustomEditor = BaseEditor &
type RenderElementPropsFor (line 12) | type RenderElementPropsFor<T> = RenderElementProps & {
type CustomTypes (line 17) | interface CustomTypes {
FILE: packages/react-text/src/text.tsx
type TextComponentProps (line 26) | type TextComponentProps = TextProps;
Condensed preview — 216 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (517K chars).
[
{
"path": ".dockerignore",
"chars": 23,
"preview": ".nx\n.dist\n.node_modules"
},
{
"path": ".editorconfig",
"chars": 245,
"preview": "# Editor configuration, see http://editorconfig.org\nroot = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = "
},
{
"path": ".eslintignore",
"chars": 13,
"preview": "node_modules\n"
},
{
"path": ".eslintrc.json",
"chars": 880,
"preview": "{\n \"root\": true,\n \"ignorePatterns\": [\"**/*\"],\n \"plugins\": [\"@nx\"],\n \"overrides\": [\n {\n \"files\": [\"*.ts\", \"*."
},
{
"path": ".github/workflows/ci.yml",
"chars": 1162,
"preview": "name: CI\n\non:\n push:\n branches:\n - main\n pull_request:\n\npermissions:\n actions: read\n contents: read\n\njobs:\n "
},
{
"path": ".github/workflows/publish.yml",
"chars": 1255,
"preview": "name: publish\n\non:\n push:\n tags:\n - \"*\"\n\njobs:\n publish:\n runs-on: ubuntu-latest\n\n steps:\n - uses: "
},
{
"path": ".gitignore",
"chars": 530,
"preview": "# See http://help.github.com/ignore-files/ for more about ignoring files.\n\n# compiled output\ndist\ntmp\n/out-tsc\n\n# depend"
},
{
"path": ".prettierignore",
"chars": 103,
"preview": "# Add files here to ignore them from prettier formatting\n/dist\n/coverage\n/.nx/cache\n/.nx/workspace-data"
},
{
"path": ".prettierrc",
"chars": 26,
"preview": "{\n \"singleQuote\": true\n}\n"
},
{
"path": ".vscode/extensions.json",
"chars": 156,
"preview": "{\n \"recommendations\": [\n \"nrwl.angular-console\",\n \"esbenp.prettier-vscode\",\n \"ms-playwright.playwright\",\n \""
},
{
"path": ".vscode/settings.json",
"chars": 49,
"preview": "{\n \"cSpell.words\": [\n \"drawnix\"\n ]\n}"
},
{
"path": "CFPAGE-DEPLOY.md",
"chars": 716,
"preview": "## \n\n### 1. 打开 Cloudflare Pages\n访问:https://dash.cloudflare.com/pages\n\n### 2. 创建项目\n- 点击 **\"Create a project\"**\n- 选择 **\"Co"
},
{
"path": "CHANGELOG.md",
"chars": 32283,
"preview": "## 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### 🚀 Feat"
},
{
"path": "Dockerfile",
"chars": 305,
"preview": "FROM node:20 AS builder \n\nWORKDIR /builder\n\nCOPY . /builder\n\nRUN npm install \\\n && npm run build \n\n\nFROM lipanski/doc"
},
{
"path": "LICENSE",
"chars": 1064,
"preview": "MIT License\n\nCopyright (c) 2024 Drawnix\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof"
},
{
"path": "README.md",
"chars": 4056,
"preview": "<p align=\"center\">\n <picture style=\"width: 320px\">\n <source media=\"(prefers-color-scheme: light)\" srcset=\"https://gi"
},
{
"path": "README_en.md",
"chars": 5653,
"preview": "<p align=\"center\">\n <picture style=\"width: 320px\">\n <source media=\"(prefers-color-scheme: light)\" srcset=\"https://gi"
},
{
"path": "SECURITY.md",
"chars": 273,
"preview": "# Security Policy\n\n## Reporting a Vulnerability\n\nWe have an official discord server for discussing and reporting about D"
},
{
"path": "apps/web/.eslintrc.json",
"chars": 326,
"preview": "{\n \"extends\": [\"plugin:@nx/react\", \"../../.eslintrc.json\"],\n \"ignorePatterns\": [\"!**/*\"],\n \"overrides\": [\n {\n "
},
{
"path": "apps/web/index.html",
"chars": 5949,
"preview": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n <head>\n <meta charset=\"utf-8\" />\n <!-- 基本 SEO Meta 标签 (中文) -->\n <title>Dr"
},
{
"path": "apps/web/jest.config.ts",
"chars": 358,
"preview": "/* eslint-disable */\nexport default {\n displayName: 'web',\n preset: '../../jest.preset.js',\n transform: {\n '^(?!.*"
},
{
"path": "apps/web/project.json",
"chars": 250,
"preview": "{\n \"name\": \"web\",\n \"$schema\": \"../../node_modules/nx/schemas/project-schema.json\",\n \"sourceRoot\": \"apps/web/src\",\n \""
},
{
"path": "apps/web/public/_headers",
"chars": 181,
"preview": "# 基本缓存配置\n/*\n Cache-Control: public, max-age=31536000, immutable\n\n/*.html\n Cache-Control: public, max-age=0, must-reval"
},
{
"path": "apps/web/public/_redirects",
"chars": 35,
"preview": "# SPA 路由支持\n/* /index.html 200\n"
},
{
"path": "apps/web/public/robots.txt",
"chars": 198,
"preview": "id: robots-txt\nname: Robots.txt\ntype: code.txt\ncontent: |-\n User-agent: *\n Allow: /\n \n # 禁止访问管理后台\n Disallow: /admin"
},
{
"path": "apps/web/public/sitemap.xml",
"chars": 1007,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"\n xmlns:xsi=\"http:/"
},
{
"path": "apps/web/src/app/app.module.scss",
"chars": 29,
"preview": "/* Your styles goes here. */\n"
},
{
"path": "apps/web/src/app/app.spec.tsx",
"chars": 242,
"preview": "import { render } from '@testing-library/react';\n\nimport App from './app';\n\ndescribe('App', () => {\n it('should render "
},
{
"path": "apps/web/src/app/app.tsx",
"chars": 2409,
"preview": "import { useState, useEffect } from 'react';\nimport { Drawnix } from '@drawnix/drawnix';\nimport { PlaitBoard, PlaitEleme"
},
{
"path": "apps/web/src/assets/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "apps/web/src/main.tsx",
"chars": 258,
"preview": "import { StrictMode } from 'react';\nimport * as ReactDOM from 'react-dom/client';\n\nimport App from './app/app';\n\nconst r"
},
{
"path": "apps/web/src/styles.scss",
"chars": 503,
"preview": "/* You can add global styles to this file, and also import other style files */\nbody {\n margin: 0;\n padding: 0;\n}\n"
},
{
"path": "apps/web/tsconfig.app.json",
"chars": 526,
"preview": "{\n \"extends\": \"./tsconfig.json\",\n \"compilerOptions\": {\n \"outDir\": \"../../dist/out-tsc\",\n \"types\": [\n \"node\""
},
{
"path": "apps/web/tsconfig.json",
"chars": 392,
"preview": "{\n \"compilerOptions\": {\n \"jsx\": \"react-jsx\",\n \"allowJs\": false,\n \"esModuleInterop\": false,\n \"allowSynthetic"
},
{
"path": "apps/web/tsconfig.spec.json",
"chars": 511,
"preview": "{\n \"extends\": \"./tsconfig.json\",\n \"compilerOptions\": {\n \"outDir\": \"../../dist/out-tsc\",\n \"module\": \"commonjs\",\n "
},
{
"path": "apps/web/vite.config.ts",
"chars": 725,
"preview": "/// <reference types='vitest' />\nimport { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport {"
},
{
"path": "apps/web-e2e/.eslintrc.json",
"chars": 415,
"preview": "{\n \"extends\": [\"plugin:playwright/recommended\", \"../../.eslintrc.json\"],\n \"ignorePatterns\": [\"!**/*\"],\n \"overrides\": "
},
{
"path": "apps/web-e2e/playwright.config.ts",
"chars": 1773,
"preview": "import { defineConfig, devices } from '@playwright/test';\nimport { nxE2EPreset } from '@nx/playwright/preset';\n\nimport {"
},
{
"path": "apps/web-e2e/project.json",
"chars": 283,
"preview": "{\n \"name\": \"web-e2e\",\n \"$schema\": \"../../node_modules/nx/schemas/project-schema.json\",\n \"projectType\": \"application\","
},
{
"path": "apps/web-e2e/src/example.spec.ts",
"chars": 266,
"preview": "import { test, expect } from '@playwright/test';\n\ntest('has title', async ({ page }) => {\n await page.goto('/');\n\n // "
},
{
"path": "apps/web-e2e/tsconfig.json",
"chars": 372,
"preview": "{\n \"extends\": \"../../tsconfig.base.json\",\n \"compilerOptions\": {\n \"allowJs\": true,\n \"outDir\": \"../../dist/out-tsc"
},
{
"path": "jest.config.ts",
"chars": 126,
"preview": "import { getJestProjectsAsync } from '@nx/jest';\n\nexport default async () => ({\n projects: await getJestProjectsAsync()"
},
{
"path": "jest.preset.js",
"chars": 88,
"preview": "const nxPreset = require('@nx/jest/preset').default;\n\nmodule.exports = { ...nxPreset };\n"
},
{
"path": "nx.json",
"chars": 1765,
"preview": "{\n \"$schema\": \"./node_modules/nx/schemas/nx-schema.json\",\n \"namedInputs\": {\n \"default\": [\n \"{projectRoot}/**/*"
},
{
"path": "package.json",
"chars": 2844,
"preview": "{\n \"name\": \"@drawnix/source\",\n \"version\": \"0.0.2\",\n \"license\": \"MIT\",\n \"scripts\": {\n \"start\": \"nx serve web --hos"
},
{
"path": "packages/drawnix/.babelrc",
"chars": 156,
"preview": "{\n \"presets\": [\n [\n \"@nx/react/babel\",\n {\n \"runtime\": \"automatic\",\n \"useBuiltIns\": \"usage\"\n "
},
{
"path": "packages/drawnix/.eslintrc.json",
"chars": 326,
"preview": "{\n \"extends\": [\"plugin:@nx/react\", \"../../.eslintrc.json\"],\n \"ignorePatterns\": [\"!**/*\"],\n \"overrides\": [\n {\n "
},
{
"path": "packages/drawnix/README.md",
"chars": 172,
"preview": "# drawnix\n\nThis library was generated with [Nx](https://nx.dev).\n\n## Running unit tests\n\nRun `nx test drawnix` to execut"
},
{
"path": "packages/drawnix/jest.config.ts",
"chars": 370,
"preview": "/* eslint-disable */\nexport default {\n displayName: 'drawnix',\n preset: '../../jest.preset.js',\n transform: {\n '^("
},
{
"path": "packages/drawnix/package.json",
"chars": 539,
"preview": "{\n \"name\": \"@drawnix/drawnix\",\n \"version\": \"0.4.0-2\",\n \"main\": \"./index.js\",\n \"types\": \"./index.d.ts\",\n \"private\": "
},
{
"path": "packages/drawnix/project.json",
"chars": 262,
"preview": "{\n \"name\": \"drawnix\",\n \"$schema\": \"../../node_modules/nx/schemas/project-schema.json\",\n \"sourceRoot\": \"packages/drawn"
},
{
"path": "packages/drawnix/src/components/arrow-mark-picker.tsx",
"chars": 1918,
"preview": "import classNames from 'classnames';\nimport { Island } from './island';\nimport Stack from './stack';\nimport { ToolButton"
},
{
"path": "packages/drawnix/src/components/arrow-picker.tsx",
"chars": 2136,
"preview": "import classNames from 'classnames';\nimport { Island } from './island';\nimport Stack from './stack';\nimport { ToolButton"
},
{
"path": "packages/drawnix/src/components/clean-confirm/clean-confirm.scss",
"chars": 780,
"preview": ".clean-confirm {\n background: white;\n border-radius: 8px;\n padding: 20px;\n width: 300px;\n\n &__title {\n font-size"
},
{
"path": "packages/drawnix/src/components/clean-confirm/clean-confirm.tsx",
"chars": 1584,
"preview": "import { Dialog, DialogContent } from '../dialog/dialog';\nimport { useDrawnix } from '../../hooks/use-drawnix';\nimport '"
},
{
"path": "packages/drawnix/src/components/color-picker.scss",
"chars": 825,
"preview": "@import \"open-color/open-color.scss\";\n\n.color-select-item {\n width: var(--default-button-size);\n height: var(--default"
},
{
"path": "packages/drawnix/src/components/color-picker.tsx",
"chars": 3518,
"preview": "import { useState } from 'react';\nimport { Check, NoColorIcon } from './icons';\nimport Stack from '../components/stack';"
},
{
"path": "packages/drawnix/src/components/dialog/dialog.scss",
"chars": 202,
"preview": ".Dialog-overlay {\n background: rgba(#121212, 0.2);\n display: grid;\n place-items: center;\n}\n\n.Dialog {\n margi"
},
{
"path": "packages/drawnix/src/components/dialog/dialog.tsx",
"chars": 5523,
"preview": "import * as React from 'react';\nimport {\n useFloating,\n useClick,\n useDismiss,\n useRole,\n useInteractions,\n useMer"
},
{
"path": "packages/drawnix/src/components/icons.tsx",
"chars": 43019,
"preview": "import React from 'react';\n\nexport const createIcon = (svg: React.ReactNode) => {\n return svg;\n};\n\nexport const HandIco"
},
{
"path": "packages/drawnix/src/components/island.scss",
"chars": 427,
"preview": ".drawnix {\n .island {\n --padding: 0;\n box-sizing: border-box;\n background-color: var(--island-bg-color);\n b"
},
{
"path": "packages/drawnix/src/components/island.tsx",
"chars": 601,
"preview": "// Credits to excalidraw\nimport classNames from 'classnames';\nimport './island.scss';\n\nimport React from 'react';\n\ntype "
},
{
"path": "packages/drawnix/src/components/menu/common.ts",
"chars": 1024,
"preview": "import React, { useContext } from 'react';\nimport { EVENT } from '../../constants';\nimport { composeEventHandlers } from"
},
{
"path": "packages/drawnix/src/components/menu/menu-group.tsx",
"chars": 444,
"preview": "import React from 'react';\n\nconst MenuGroup = ({\n children,\n className = '',\n style,\n title,\n}: {\n children: React."
},
{
"path": "packages/drawnix/src/components/menu/menu-item-content-radio.tsx",
"chars": 955,
"preview": "import { RadioGroup } from '../radio-group';\n\ntype Props<T> = {\n value: T;\n shortcut?: string;\n choices: {\n value:"
},
{
"path": "packages/drawnix/src/components/menu/menu-item-content.tsx",
"chars": 452,
"preview": "import React from 'react';\n\nconst MenuItemContent = ({\n icon,\n shortcut,\n children,\n}: {\n icon?: React.ReactNode;\n "
},
{
"path": "packages/drawnix/src/components/menu/menu-item-custom.tsx",
"chars": 470,
"preview": "import React from 'react';\n\nconst MenuItemCustom = ({\n children,\n className = '',\n selected,\n ...rest\n}: {\n childre"
},
{
"path": "packages/drawnix/src/components/menu/menu-item-link.tsx",
"chars": 1001,
"preview": "import React from 'react';\nimport { getMenuItemClassName, useHandleMenuItemClick } from './common';\nimport MenuItemConte"
},
{
"path": "packages/drawnix/src/components/menu/menu-item.tsx",
"chars": 3029,
"preview": "import React, { useState, useRef } from 'react';\nimport {\n getMenuItemClassName,\n useHandleMenuItemClick,\n} from './co"
},
{
"path": "packages/drawnix/src/components/menu/menu-separator.tsx",
"chars": 234,
"preview": "const MenuSeparator = () => (\n <div\n style={{\n height: '1px',\n backgroundColor: 'var(--color-gray-10)',\n "
},
{
"path": "packages/drawnix/src/components/menu/menu.scss",
"chars": 3093,
"preview": "@import \"../../styles/variables.module.scss\";\n\n.drawnix {\n .menu {\n &--mobile {\n left: 0;\n width: 100%;\n "
},
{
"path": "packages/drawnix/src/components/menu/menu.tsx",
"chars": 880,
"preview": "import { Island } from '../island';\nimport React from 'react';\nimport { MenuContentPropsContext } from './common';\nimpor"
},
{
"path": "packages/drawnix/src/components/popover/popover.tsx",
"chars": 4665,
"preview": "import * as React from 'react';\nimport {\n useFloating,\n autoUpdate,\n offset,\n flip,\n shift,\n useClick,\n useDismis"
},
{
"path": "packages/drawnix/src/components/popup/link-popup/link-popup.scss",
"chars": 419,
"preview": ".drawnix {\n .link-popup {\n padding-left: 8px;\n\n &__link {\n font-size: 14px;\n }\n\n .link-popup__link {\n "
},
{
"path": "packages/drawnix/src/components/popup/link-popup/link-popup.tsx",
"chars": 7094,
"preview": "import { useEffect, useState, useRef } from 'react';\nimport { Island } from '../../island';\nimport Stack from '../../sta"
},
{
"path": "packages/drawnix/src/components/radio-group.scss",
"chars": 2274,
"preview": "@import '../styles/variables.module.scss';\n\n.drawnix {\n --RadioGroup-background: var(--island-bg-color);\n --RadioGroup"
},
{
"path": "packages/drawnix/src/components/radio-group.tsx",
"chars": 979,
"preview": "import classNames from 'classnames';\nimport './radio-group.scss';\n\nexport type RadioGroupChoice<T> = {\n value: T;\n lab"
},
{
"path": "packages/drawnix/src/components/select/select.scss",
"chars": 4969,
"preview": ".drawnix {\n /* Define local variables mapped to project theme or defaults */\n --dx-select-trigger-height: 2rem;\n --dx"
},
{
"path": "packages/drawnix/src/components/select/select.tsx",
"chars": 13272,
"preview": "import * as React from 'react';\nimport classNames from 'classnames';\nimport {\n autoUpdate,\n flip,\n FloatingFocusManag"
},
{
"path": "packages/drawnix/src/components/shape-picker.tsx",
"chars": 3556,
"preview": "import classNames from 'classnames';\nimport { Island } from './island';\nimport Stack from './stack';\nimport { ToolButton"
},
{
"path": "packages/drawnix/src/components/size-slider.scss",
"chars": 914,
"preview": "@import \"open-color/open-color.scss\";\n\n.slider-container {\n padding: 10px 0px;\n &.disabled {\n opacity: 50%;"
},
{
"path": "packages/drawnix/src/components/size-slider.tsx",
"chars": 3485,
"preview": "import React, {\n useState,\n useRef,\n useCallback,\n useEffect,\n useMemo,\n} from 'react';\nimport { toFixed } from '@p"
},
{
"path": "packages/drawnix/src/components/stack.scss",
"chars": 347,
"preview": ".drawnix {\n .stack {\n --gap: 0;\n display: grid;\n gap: calc(var(--space-factor) * var(--gap));\n }\n\n .stack_ve"
},
{
"path": "packages/drawnix/src/components/stack.tsx",
"chars": 1365,
"preview": "// Credits to excalidraw\nimport \"./stack.scss\";\n\nimport React, { forwardRef } from \"react\";\nimport clsx from \"classnames"
},
{
"path": "packages/drawnix/src/components/tool-button.tsx",
"chars": 5730,
"preview": "// Credits to excalidraw\nimport './tool-icon.scss';\n\nimport type { CSSProperties } from 'react';\nimport React, { useEffe"
},
{
"path": "packages/drawnix/src/components/tool-icon.scss",
"chars": 2621,
"preview": "@import \"open-color/open-color.scss\";\n@import \"../styles/variables.module.scss\";\n\n.drawnix {\n .tool-icon {\n border-r"
},
{
"path": "packages/drawnix/src/components/toolbar/app-toolbar/app-menu-items.tsx",
"chars": 4384,
"preview": "import {\n ExportImageIcon,\n GithubIcon,\n OpenFileIcon,\n SaveFileIcon,\n TrashIcon,\n} from '../../icons';\nimport { us"
},
{
"path": "packages/drawnix/src/components/toolbar/app-toolbar/app-toolbar.tsx",
"chars": 3940,
"preview": "import { useBoard } from '@plait-board/react-board';\nimport Stack from '../../stack';\nimport { ToolButton } from '../../"
},
{
"path": "packages/drawnix/src/components/toolbar/app-toolbar/language-switcher-menu.tsx",
"chars": 2351,
"preview": "import { useContext } from 'react';\nimport { MenuIcon } from '../../icons';\nimport { useI18n } from '../../../i18n';\nimp"
},
{
"path": "packages/drawnix/src/components/toolbar/creation-toolbar.tsx",
"chars": 11432,
"preview": "import classNames from 'classnames';\nimport { Island } from '../island';\nimport Stack from '../stack';\nimport { ToolButt"
},
{
"path": "packages/drawnix/src/components/toolbar/extra-tools/extra-tools-button.tsx",
"chars": 1557,
"preview": "import { useBoard } from \"@plait-board/react-board\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../../popo"
},
{
"path": "packages/drawnix/src/components/toolbar/extra-tools/menu-items.tsx",
"chars": 1320,
"preview": "import MenuItem from '../../menu/menu-item';\nimport { MarkdownLogoIcon, MermaidLogoIcon } from '../../icons';\nimport { D"
},
{
"path": "packages/drawnix/src/components/toolbar/freehand-panel/freehand-panel.tsx",
"chars": 2567,
"preview": "import classNames from 'classnames';\nimport { Island } from '../../island';\nimport Stack from '../../stack';\nimport { To"
},
{
"path": "packages/drawnix/src/components/toolbar/pencil-mode-toolbar.tsx",
"chars": 813,
"preview": "import { ToolButton } from '../tool-button';\nimport { useBoard } from '@plait-board/react-board';\nimport { useDrawnix } "
},
{
"path": "packages/drawnix/src/components/toolbar/popup-toolbar/arrow-mark-button.tsx",
"chars": 2087,
"preview": "import React, { useState } from 'react';\nimport { ToolButton } from '../../tool-button';\nimport classNames from 'classna"
},
{
"path": "packages/drawnix/src/components/toolbar/popup-toolbar/fill-button.tsx",
"chars": 2425,
"preview": "import React, { useState } from 'react';\nimport { ToolButton } from '../../tool-button';\nimport classNames from 'classna"
},
{
"path": "packages/drawnix/src/components/toolbar/popup-toolbar/font-color-button.tsx",
"chars": 2250,
"preview": "import React, { ReactNode, useState } from 'react';\nimport { ColorPicker } from '../../color-picker';\nimport { ToolButto"
},
{
"path": "packages/drawnix/src/components/toolbar/popup-toolbar/font-size-control.tsx",
"chars": 5873,
"preview": "import React, { useEffect, useMemo, useRef, useState } from 'react';\nimport { PlaitBoard } from '@plait/core';\nimport { "
},
{
"path": "packages/drawnix/src/components/toolbar/popup-toolbar/link-button.tsx",
"chars": 1968,
"preview": "import React from 'react';\nimport { ToolButton } from '../../tool-button';\nimport classNames from 'classnames';\nimport {"
},
{
"path": "packages/drawnix/src/components/toolbar/popup-toolbar/popup-toolbar.scss",
"chars": 3182,
"preview": ".popup-toolbar {\n .popup-font-size {\n height: var(--lg-button-size);\n display: flex;\n align-item"
},
{
"path": "packages/drawnix/src/components/toolbar/popup-toolbar/popup-toolbar.tsx",
"chars": 11183,
"preview": "import Stack from '../../stack';\nimport { FontColorIcon } from '../../icons';\nimport {\n ATTACHED_ELEMENT_CLASS_NAME,\n "
},
{
"path": "packages/drawnix/src/components/toolbar/popup-toolbar/stroke-button.tsx",
"chars": 4778,
"preview": "import React, { useState } from 'react';\nimport { ToolButton } from '../../tool-button';\nimport classNames from 'classna"
},
{
"path": "packages/drawnix/src/components/toolbar/theme-toolbar.tsx",
"chars": 1136,
"preview": "import { useBoard } from '@plait-board/react-board';\nimport classNames from 'classnames';\nimport {\n ATTACHED_ELEMENT_CL"
},
{
"path": "packages/drawnix/src/components/toolbar/zoom-toolbar.tsx",
"chars": 3215,
"preview": "import { useBoard } from '@plait-board/react-board';\nimport Stack from '../stack';\nimport { ToolButton } from '../tool-b"
},
{
"path": "packages/drawnix/src/components/ttd-dialog/markdown-to-drawnix.tsx",
"chars": 4600,
"preview": "import { useState, useEffect, useDeferredValue } from 'react';\nimport './mermaid-to-drawnix.scss';\nimport './ttd-dialog."
},
{
"path": "packages/drawnix/src/components/ttd-dialog/mermaid-to-drawnix.scss",
"chars": 166,
"preview": ".drawnix {\n .dialog-mermaid {\n &-title {\n margin-block: 0.25rem;\n font-size: 1.25rem;\n font-weight: 7"
},
{
"path": "packages/drawnix/src/components/ttd-dialog/mermaid-to-drawnix.tsx",
"chars": 6797,
"preview": "import { useState, useEffect, useDeferredValue } from 'react';\nimport './mermaid-to-drawnix.scss';\nimport './ttd-dialog."
},
{
"path": "packages/drawnix/src/components/ttd-dialog/ttd-dialog-input.tsx",
"chars": 1289,
"preview": "import type { ChangeEventHandler } from \"react\";\nimport { useEffect, useRef } from \"react\";\nimport { EVENT } from \"../.."
},
{
"path": "packages/drawnix/src/components/ttd-dialog/ttd-dialog-output.tsx",
"chars": 1364,
"preview": "import { withGroup } from '@plait/common';\nimport { PlaitElement, PlaitPlugin } from '@plait/core';\nimport { withDraw } "
},
{
"path": "packages/drawnix/src/components/ttd-dialog/ttd-dialog-panel.tsx",
"chars": 1620,
"preview": "import type { ReactNode } from 'react';\nimport classNames from 'classnames';\n\ninterface TTDDialogPanelProps {\n label: s"
},
{
"path": "packages/drawnix/src/components/ttd-dialog/ttd-dialog-panels.tsx",
"chars": 182,
"preview": "import type { ReactNode } from \"react\";\n\nexport const TTDDialogPanels = ({ children }: { children: ReactNode }) => {\n r"
},
{
"path": "packages/drawnix/src/components/ttd-dialog/ttd-dialog-submit-shortcut.tsx",
"chars": 390,
"preview": "import { getShortcutKey } from \"../../utils/common\";\n\nexport const TTDDialogSubmitShortcut = () => {\n return (\n <div"
},
{
"path": "packages/drawnix/src/components/ttd-dialog/ttd-dialog.scss",
"chars": 6377,
"preview": "@import \"../../styles/variables.module.scss\";\n\n$verticalBreakpoint: 861px;\n\n.drawnix {\n .Dialog.ttd-dialog {\n paddin"
},
{
"path": "packages/drawnix/src/components/ttd-dialog/ttd-dialog.tsx",
"chars": 1276,
"preview": "import { Dialog, DialogContent } from '../dialog/dialog';\nimport MermaidToDrawnix from './mermaid-to-drawnix';\nimport { "
},
{
"path": "packages/drawnix/src/components/tutorial.scss",
"chars": 2077,
"preview": ".drawnix-tutorial {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Noto Sans', 'N"
},
{
"path": "packages/drawnix/src/components/tutorial.tsx",
"chars": 2698,
"preview": "import React, { useState, useEffect } from \"react\";\nimport { useI18n } from \"../i18n\";\nimport \"./tutorial.scss\";\n\nexport"
},
{
"path": "packages/drawnix/src/constants/color.ts",
"chars": 886,
"preview": "import { DEFAULT_COLOR } from '@plait/core';\n\nexport const TRANSPARENT = 'TRANSPARENT';\n\nexport const NO_COLOR = 'NO_COL"
},
{
"path": "packages/drawnix/src/constants.ts",
"chars": 1233,
"preview": "export enum EVENT {\n COPY = 'copy',\n PASTE = 'paste',\n CUT = 'cut',\n KEYDOWN = 'keydown',\n KEYUP = 'keyup',\n MOUSE"
},
{
"path": "packages/drawnix/src/css.d.ts",
"chars": 211,
"preview": "import \"csstype\";\n\ndeclare module \"csstype\" {\n interface Properties {\n \"--max-width\"?: number | string;\n \"--swatc"
},
{
"path": "packages/drawnix/src/data/blob.ts",
"chars": 2750,
"preview": "import { PlaitBoard } from '@plait/core';\nimport { isValidDrawnixData } from './json';\nimport { IMAGE_MIME_TYPES, MIME_T"
},
{
"path": "packages/drawnix/src/data/filesystem.ts",
"chars": 1695,
"preview": "import type { FileSystemHandle } from 'browser-fs-access';\nimport {\n fileOpen as _fileOpen,\n fileSave as _fileSave,\n "
},
{
"path": "packages/drawnix/src/data/image.ts",
"chars": 1864,
"preview": "import {\n getHitElementByPoint,\n getSelectedElements,\n PlaitBoard,\n Point,\n} from '@plait/core';\nimport { DataURL } "
},
{
"path": "packages/drawnix/src/data/json.ts",
"chars": 1691,
"preview": "import { PlaitBoard, PlaitElement } from '@plait/core';\nimport { MIME_TYPES, VERSIONS } from '../constants';\nimport { fi"
},
{
"path": "packages/drawnix/src/data/types.ts",
"chars": 313,
"preview": "import { PlaitElement, PlaitTheme, Viewport } from '@plait/core';\n\nexport interface DrawnixExportedData {\n type: Drawni"
},
{
"path": "packages/drawnix/src/drawnix.spec.tsx",
"chars": 185,
"preview": "describe('Drawnix', () => {\n it('should render successfully', () => {\n // const { baseElement } = render(<Drawnix va"
},
{
"path": "packages/drawnix/src/drawnix.tsx",
"chars": 5370,
"preview": "import { Board, BoardChangeData, Wrapper } from '@plait-board/react-board';\nimport {\n PlaitBoard,\n PlaitBoardOptions,\n"
},
{
"path": "packages/drawnix/src/errors.ts",
"chars": 132,
"preview": "export class AbortError extends DOMException {\n constructor(message = 'Request Aborted') {\n super(message, 'AbortErr"
},
{
"path": "packages/drawnix/src/hooks/use-drawnix.tsx",
"chars": 1796,
"preview": "/**\n * A React context for sharing the board object, in a way that re-renders the\n * context whenever changes occur.\n */"
},
{
"path": "packages/drawnix/src/i18n/index.tsx",
"chars": 2022,
"preview": "import React, { createContext, useContext, useState, useMemo } from 'react';\nimport { zhTranslations, enTranslations, ru"
},
{
"path": "packages/drawnix/src/i18n/translations/ar.ts",
"chars": 5914,
"preview": "import { Translations } from '../types';\n\nconst arTranslations: Translations = {\n // Toolbar items\n \"toolbar.hand\""
},
{
"path": "packages/drawnix/src/i18n/translations/en.ts",
"chars": 5447,
"preview": "import { Translations } from '../types';\n\nconst enTranslations: Translations = {\n // Toolbar items\n 'toolbar.hand': 'H"
},
{
"path": "packages/drawnix/src/i18n/translations/index.ts",
"chars": 266,
"preview": "import zhTranslations from './zh';\nimport enTranslations from './en';\nimport ruTranslations from './ru';\nimport arTransl"
},
{
"path": "packages/drawnix/src/i18n/translations/ru.ts",
"chars": 5695,
"preview": "import { Translations } from '../types';\n\nconst ruTranslations: Translations = {\n // Toolbar items\n 'toolbar.hand': 'Р"
},
{
"path": "packages/drawnix/src/i18n/translations/vi.ts",
"chars": 5878,
"preview": "import { Translations } from '../types';\n\nconst viTranslations: Translations = {\n // Toolbar items\n 'toolbar.hand'"
},
{
"path": "packages/drawnix/src/i18n/translations/zh.ts",
"chars": 4525,
"preview": "import { Translations } from '../types';\n\nconst zhTranslations: Translations = {\n // Toolbar items\n 'toolbar.hand': '手"
},
{
"path": "packages/drawnix/src/i18n/types.ts",
"chars": 4475,
"preview": "import { ReactNode } from 'react';\n\n// Define supported languages\nexport type Language = 'zh' | 'en' | 'ru' | 'ar' | 'vi"
},
{
"path": "packages/drawnix/src/i18n.tsx",
"chars": 152,
"preview": "export { I18nProvider, useI18n, i18nInsidePlaitHook } from './i18n/index';\nexport type { Language, Translations, I18nCon"
},
{
"path": "packages/drawnix/src/index.ts",
"chars": 76,
"preview": "export * from './drawnix';\nexport * from './utils';\nexport * from './i18n';\n"
},
{
"path": "packages/drawnix/src/keys.ts",
"chars": 1328,
"preview": "import { IS_APPLE, IS_IOS } from '@plait/core';\n\nexport const CODES = {\n EQUAL: 'Equal',\n MINUS: 'Minus',\n NUM_ADD: '"
},
{
"path": "packages/drawnix/src/libs/image-viewer.ts",
"chars": 11125,
"preview": "interface ImageViewerOptions {\n zoomStep?: number;\n minZoom?: number;\n maxZoom?: number;\n enableKeyboard?: boolean;\n"
},
{
"path": "packages/drawnix/src/plugins/components/emoji.tsx",
"chars": 276,
"preview": "import type { EmojiProps } from '@plait/mind';\n\nexport const Emoji: React.FC<EmojiProps> = (props: EmojiProps) => {\n re"
},
{
"path": "packages/drawnix/src/plugins/components/image.tsx",
"chars": 470,
"preview": "import type { ImageProps } from '@plait/common';\nimport classNames from 'classnames';\n\nexport const Image: React.FC<Imag"
},
{
"path": "packages/drawnix/src/plugins/freehand/freehand.component.ts",
"chars": 2116,
"preview": "import {\n PlaitBoard,\n PlaitPluginElementContext,\n OnContextChanged,\n RectangleClient,\n isSelectionMoving,\n ACTIVE"
},
{
"path": "packages/drawnix/src/plugins/freehand/freehand.generator.ts",
"chars": 966,
"preview": "import { Generator } from '@plait/common';\nimport { PlaitBoard, setStrokeLinecap } from '@plait/core';\nimport { Options "
},
{
"path": "packages/drawnix/src/plugins/freehand/smoother.ts",
"chars": 7324,
"preview": "import { distanceBetweenPointAndPoint, Point } from '@plait/core';\n\ninterface StrokePoint {\n point: Point;\n pressure?:"
},
{
"path": "packages/drawnix/src/plugins/freehand/type.ts",
"chars": 1094,
"preview": "import { DEFAULT_COLOR, Point, ThemeColorMode } from '@plait/core';\nimport { PlaitCustomGeometry } from '@plait/draw';\n\n"
},
{
"path": "packages/drawnix/src/plugins/freehand/utils.ts",
"chars": 4760,
"preview": "import {\n getSelectedElements,\n idCreator,\n isPointInPolygon,\n PlaitBoard,\n PlaitElement,\n Point,\n RectangleClien"
},
{
"path": "packages/drawnix/src/plugins/freehand/with-freehand-create.ts",
"chars": 3749,
"preview": "import {\n PlaitBoard,\n Point,\n Transforms,\n distanceBetweenPointAndPoint,\n toHostPoint,\n toViewBoxPoint,\n} from '@"
},
{
"path": "packages/drawnix/src/plugins/freehand/with-freehand-erase.ts",
"chars": 3204,
"preview": "import {\n PlaitBoard,\n PlaitElement,\n Point,\n throttleRAF,\n toHostPoint,\n toViewBoxPoint,\n} from '@plait/core';\nim"
},
{
"path": "packages/drawnix/src/plugins/freehand/with-freehand-fragment.ts",
"chars": 2056,
"preview": "import {\n ClipboardData,\n PlaitBoard,\n PlaitElement,\n Point,\n RectangleClient,\n WritableClipboardContext,\n Writab"
},
{
"path": "packages/drawnix/src/plugins/freehand/with-freehand.ts",
"chars": 2376,
"preview": "import {\n PlaitBoard,\n PlaitElement,\n PlaitOptionsBoard,\n PlaitPluginElementContext,\n RectangleClient,\n Selection,"
},
{
"path": "packages/drawnix/src/plugins/with-common.tsx",
"chars": 1621,
"preview": "import type {\n ImageProps,\n PlaitImageBoard,\n RenderComponentRef,\n} from '@plait/common';\nimport { PlaitBoard, PlaitI"
},
{
"path": "packages/drawnix/src/plugins/with-hotkey.ts",
"chars": 4946,
"preview": "import {\n BoardTransforms,\n getSelectedElements,\n PlaitBoard,\n PlaitPointerType,\n} from '@plait/core';\nimport { isHo"
},
{
"path": "packages/drawnix/src/plugins/with-image.tsx",
"chars": 2470,
"preview": "import {\n getElementOfFocusedImage,\n isResizing,\n type PlaitImageBoard,\n} from '@plait/common';\nimport {\n ClipboardD"
},
{
"path": "packages/drawnix/src/plugins/with-mind-extend.tsx",
"chars": 1351,
"preview": "import type { PlaitBoard, PlaitOptionsBoard } from '@plait/core';\nimport {\n WithMindPluginKey,\n type EmojiProps,\n typ"
},
{
"path": "packages/drawnix/src/plugins/with-pencil.ts",
"chars": 942,
"preview": "import { isPencilEvent, PlaitBoard } from '@plait/core';\nimport { DrawnixState } from '../hooks/use-drawnix';\n\nconst IS_"
},
{
"path": "packages/drawnix/src/plugins/with-text-link.tsx",
"chars": 3046,
"preview": "import {\n isMovingElements,\n PlaitBoard,\n PlaitPointerType,\n throttleRAF,\n} from '@plait/core';\nimport { DrawnixBoar"
},
{
"path": "packages/drawnix/src/styles/index.scss",
"chars": 4420,
"preview": "@import './../../../../node_modules/@plait/draw/styles/styles.scss';\n@import './../../../../node_modules/@plait/mind/sty"
},
{
"path": "packages/drawnix/src/styles/theme.scss",
"chars": 2779,
"preview": "@import \"open-color/open-color.scss\";\n@import \"./variables.module.scss\";\n\n.drawnix {\n --focus-highlight-color: #{$oc-bl"
},
{
"path": "packages/drawnix/src/styles/variables.module.scss",
"chars": 2948,
"preview": "@import \"open-color/open-color.scss\";\n\n@mixin isMobile() {\n @at-root .drawnix--mobile#{&} {\n @content;\n }\n}\n\n@mixin"
},
{
"path": "packages/drawnix/src/transforms/property.ts",
"chars": 4417,
"preview": "import { PropertyTransforms } from '@plait/common';\nimport {\n isNullOrUndefined,\n Path,\n PlaitBoard,\n PlaitElement,\n"
},
{
"path": "packages/drawnix/src/types.ts",
"chars": 113,
"preview": "export type EventPointerType = 'mouse' | 'pen' | 'touch';\n\nexport type DataURL = string & { _brand: 'DataURL' };\n"
},
{
"path": "packages/drawnix/src/utils/color.ts",
"chars": 2521,
"preview": "import { DEFAULT_COLOR, PlaitBoard } from '@plait/core';\nimport { TRANSPARENT, NO_COLOR, WHITE } from '../constants/colo"
},
{
"path": "packages/drawnix/src/utils/common.ts",
"chars": 2374,
"preview": "import { IS_APPLE, IS_MAC, PlaitBoard, toImage, ToImageOptions } from '@plait/core';\nimport type { ResolutionType } from"
},
{
"path": "packages/drawnix/src/utils/image.ts",
"chars": 1890,
"preview": "import { getSelectedElements, PlaitBoard, toSvgData } from '@plait/core';\nimport { base64ToBlob, boardToImage, download "
},
{
"path": "packages/drawnix/src/utils/index.ts",
"chars": 136,
"preview": "export * from './color';\nexport * from './common';\nexport * from './image';\nexport * from './property';\nexport * from '."
},
{
"path": "packages/drawnix/src/utils/laser-pointer.ts",
"chars": 4118,
"preview": "import { PlaitBoard } from '@plait/core';\nimport {\n drainPoints,\n drawLaserPen,\n IOriginalPointData,\n setColor,\n se"
},
{
"path": "packages/drawnix/src/utils/property.ts",
"chars": 1902,
"preview": "import { PlaitBoard, PlaitElement } from '@plait/core';\nimport {\n isClosedCustomGeometry,\n isClosedDrawElement,\n Plai"
},
{
"path": "packages/drawnix/src/utils/utility-types.ts",
"chars": 164,
"preview": "export type ResolutionType<T extends (...args: any) => any> = T extends (\n ...args: any\n) => Promise<infer R>\n ? R\n :"
},
{
"path": "packages/drawnix/tsconfig.json",
"chars": 392,
"preview": "{\n \"compilerOptions\": {\n \"jsx\": \"react-jsx\",\n \"allowJs\": false,\n \"esModuleInterop\": false,\n \"allowSynthetic"
},
{
"path": "packages/drawnix/tsconfig.lib.json",
"chars": 494,
"preview": "{\n \"extends\": \"./tsconfig.json\",\n \"compilerOptions\": {\n \"outDir\": \"../../dist/out-tsc\",\n \"types\": [\n \"node\""
},
{
"path": "packages/drawnix/tsconfig.spec.json",
"chars": 413,
"preview": "{\n \"extends\": \"./tsconfig.json\",\n \"compilerOptions\": {\n \"outDir\": \"../../dist/out-tsc\",\n \"module\": \"commonjs\",\n "
},
{
"path": "packages/drawnix/vite.config.ts",
"chars": 1963,
"preview": "/// <reference types='vitest' />\nimport { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport d"
},
{
"path": "packages/react-board/.babelrc",
"chars": 156,
"preview": "{\n \"presets\": [\n [\n \"@nx/react/babel\",\n {\n \"runtime\": \"automatic\",\n \"useBuiltIns\": \"usage\"\n "
},
{
"path": "packages/react-board/.eslintrc.json",
"chars": 326,
"preview": "{\n \"extends\": [\"plugin:@nx/react\", \"../../.eslintrc.json\"],\n \"ignorePatterns\": [\"!**/*\"],\n \"overrides\": [\n {\n "
},
{
"path": "packages/react-board/README.md",
"chars": 180,
"preview": "# react-board\n\nThis library was generated with [Nx](https://nx.dev).\n\n## Running unit tests\n\nRun `nx test react-board` t"
},
{
"path": "packages/react-board/jest.config.ts",
"chars": 378,
"preview": "/* eslint-disable */\nexport default {\n displayName: 'react-board',\n preset: '../../jest.preset.js',\n transform: {\n "
},
{
"path": "packages/react-board/package.json",
"chars": 534,
"preview": "{\n \"name\": \"@plait-board/react-board\",\n \"version\": \"0.4.0-2\",\n \"main\": \"./index.js\",\n \"types\": \"./index.d.ts\",\n \"pr"
},
{
"path": "packages/react-board/project.json",
"chars": 274,
"preview": "{\n \"name\": \"react-board\",\n \"$schema\": \"../../node_modules/nx/schemas/project-schema.json\",\n \"sourceRoot\": \"packages/r"
},
{
"path": "packages/react-board/src/board.spec.tsx",
"chars": 268,
"preview": "import { render } from '@testing-library/react';\n\nimport {Board} from './board';\n\ndescribe('ReactBoard', () => {\n it('s"
},
{
"path": "packages/react-board/src/board.tsx",
"chars": 4617,
"preview": "import rough from 'roughjs/bin/rough';\nimport {\n BOARD_TO_AFTER_CHANGE,\n BOARD_TO_CONTEXT,\n BOARD_TO_ELEMENT_HOST,\n "
},
{
"path": "packages/react-board/src/hooks/use-board-event.ts",
"chars": 2579,
"preview": "import {\n BoardTransforms,\n PlaitBoard,\n ZOOM_STEP,\n initializeViewBox,\n initializeViewportContainer,\n isFromViewp"
},
{
"path": "packages/react-board/src/hooks/use-board.tsx",
"chars": 1008,
"preview": "/**\n * A React context for sharing the board object, in a way that re-renders the\n * context whenever changes occur.\n */"
},
{
"path": "packages/react-board/src/hooks/use-plugin-event.tsx",
"chars": 4068,
"preview": "import {\n BOARD_TO_MOVING_POINT,\n BOARD_TO_MOVING_POINT_IN_BOARD,\n PlaitBoard,\n WritableClipboardOperationType,\n de"
},
{
"path": "packages/react-board/src/index.ts",
"chars": 170,
"preview": "export * from './board';\nexport * from './plugins/board';\nexport * from './wrapper';\nexport * from './hooks/use-board';\n"
},
{
"path": "packages/react-board/src/plugins/board.ts",
"chars": 526,
"preview": "import type { RenderComponentRef } from '@plait/common';\nimport {\n PlaitElement,\n PlaitOperation,\n Viewport,\n Select"
},
{
"path": "packages/react-board/src/plugins/with-pinch-zoom-plugin.ts",
"chars": 5232,
"preview": "import {\n BoardTransforms,\n distanceBetweenPointAndPoint,\n getPointBetween,\n getViewportOrigination,\n MAX_ZOOM,\n M"
},
{
"path": "packages/react-board/src/plugins/with-react.tsx",
"chars": 1988,
"preview": "import {\n type PlaitTextBoard,\n type RenderComponentRef,\n type TextProps,\n} from '@plait/common';\nimport type { Plait"
},
{
"path": "packages/react-board/src/styles/index.scss",
"chars": 2266,
"preview": "@use './mixins.scss' as mixins;\n\n.plait-board-container {\n display: block;\n width: 100%;\n height: 100%;\n pos"
},
{
"path": "packages/react-board/src/styles/mixins.scss",
"chars": 458,
"preview": "@mixin board-background-color {\n &.theme-colorful .board-host-svg,\n &.theme-default .board-host-svg {\n back"
},
{
"path": "packages/react-board/src/wrapper.tsx",
"chars": 6522,
"preview": "import {\n BOARD_TO_ON_CHANGE,\n ListRender,\n PlaitElement,\n Viewport,\n createBoard,\n withBoard,\n withHandPointer,\n"
},
{
"path": "packages/react-board/tsconfig.json",
"chars": 392,
"preview": "{\n \"compilerOptions\": {\n \"jsx\": \"react-jsx\",\n \"allowJs\": false,\n \"esModuleInterop\": false,\n \"allowSynthetic"
},
{
"path": "packages/react-board/tsconfig.lib.json",
"chars": 494,
"preview": "{\n \"extends\": \"./tsconfig.json\",\n \"compilerOptions\": {\n \"outDir\": \"../../dist/out-tsc\",\n \"types\": [\n \"node\""
},
{
"path": "packages/react-board/tsconfig.spec.json",
"chars": 413,
"preview": "{\n \"extends\": \"./tsconfig.json\",\n \"compilerOptions\": {\n \"outDir\": \"../../dist/out-tsc\",\n \"module\": \"commonjs\",\n "
},
{
"path": "packages/react-board/vite.config.ts",
"chars": 1730,
"preview": "/// <reference types='vitest' />\nimport { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport d"
},
{
"path": "packages/react-text/.babelrc",
"chars": 156,
"preview": "{\n \"presets\": [\n [\n \"@nx/react/babel\",\n {\n \"runtime\": \"automatic\",\n \"useBuiltIns\": \"usage\"\n "
},
{
"path": "packages/react-text/.eslintrc.json",
"chars": 326,
"preview": "{\n \"extends\": [\"plugin:@nx/react\", \"../../.eslintrc.json\"],\n \"ignorePatterns\": [\"!**/*\"],\n \"overrides\": [\n {\n "
},
{
"path": "packages/react-text/README.md",
"chars": 178,
"preview": "# react-text\n\nThis library was generated with [Nx](https://nx.dev).\n\n## Running unit tests\n\nRun `nx test react-text` to "
},
{
"path": "packages/react-text/jest.config.ts",
"chars": 376,
"preview": "/* eslint-disable */\nexport default {\n displayName: 'react-text',\n preset: '../../jest.preset.js',\n transform: {\n "
},
{
"path": "packages/react-text/package.json",
"chars": 503,
"preview": "{\n \"name\": \"@plait-board/react-text\",\n \"version\": \"0.4.0-2\",\n \"main\": \"./index.js\",\n \"types\": \"./index.d.ts\",\n \"pri"
}
]
// ... and 16 more files (download for full content)
About this extraction
This page contains the full source code of the plait-board/drawnix GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 216 files (470.8 KB), approximately 143.9k tokens, and a symbol index with 182 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.