mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support i18n in typescript (#2948)
This commit is contained in:
parent
3d72b6fa12
commit
0dae8cf2f9
3
frontend/appflowy_tauri/.gitignore
vendored
3
frontend/appflowy_tauri/.gitignore
vendored
@ -24,4 +24,5 @@ dist-ssr
|
||||
*.sw?
|
||||
|
||||
**/src/services/backend/models/
|
||||
**/src/services/backend/events/
|
||||
**/src/services/backend/events/
|
||||
**/src/appflowy_app/i18n/translations/
|
@ -5,14 +5,16 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"build": "pnpm sync:i18n && tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"format": "prettier --write .",
|
||||
"test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
|
||||
"test:errors": "tsc --noEmit && eslint --quiet --ext .js,.ts,.tsx .",
|
||||
"test:prettier": "yarn prettier --list-different src",
|
||||
"test:errors": "pnpm sync:i18n && tsc --noEmit && eslint --quiet --ext .js,.ts,.tsx .",
|
||||
"test:prettier": "pnpm prettier --list-different src",
|
||||
"tauri:clean": "cargo make --cwd .. tauri_clean",
|
||||
"tauri:dev": "tauri dev"
|
||||
"tauri:dev": "pnpm sync:i18n && tauri dev",
|
||||
"sync:i18n": "node scripts/i18n/index.cjs",
|
||||
"css:variables": "node style-dictionary/config.cjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emoji-mart/data": "^1.1.2",
|
||||
@ -25,13 +27,14 @@
|
||||
"@slate-yjs/core": "^1.0.0",
|
||||
"@tanstack/react-virtual": "3.0.0-beta.54",
|
||||
"@tauri-apps/api": "^1.2.0",
|
||||
"dayjs": "^1.11.7",
|
||||
"dayjs": "^1.11.9",
|
||||
"emoji-mart": "^5.5.2",
|
||||
"emoji-regex": "^10.2.1",
|
||||
"events": "^3.3.0",
|
||||
"google-protobuf": "^3.21.2",
|
||||
"i18next": "^22.4.10",
|
||||
"i18next-browser-languagedetector": "^7.0.1",
|
||||
"i18next-resources-to-backend": "^1.1.4",
|
||||
"is-hotkey": "^0.2.0",
|
||||
"jest": "^29.5.0",
|
||||
"katex": "^0.16.7",
|
||||
@ -56,7 +59,6 @@
|
||||
"slate-react": "^0.94.2",
|
||||
"ts-results": "^3.3.0",
|
||||
"utf8": "^3.0.0",
|
||||
"y-indexeddb": "^9.0.9",
|
||||
"yjs": "^13.5.51"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -83,6 +85,7 @@
|
||||
"postcss": "^8.4.21",
|
||||
"prettier": "2.8.4",
|
||||
"prettier-plugin-tailwindcss": "^0.2.2",
|
||||
"style-dictionary": "^3.8.0",
|
||||
"tailwindcss": "^3.2.7",
|
||||
"typescript": "^4.6.4",
|
||||
"uuid": "^9.0.0",
|
||||
|
@ -32,8 +32,8 @@ dependencies:
|
||||
specifier: ^1.2.0
|
||||
version: 1.3.0
|
||||
dayjs:
|
||||
specifier: ^1.11.7
|
||||
version: 1.11.7
|
||||
specifier: ^1.11.9
|
||||
version: 1.11.9
|
||||
emoji-mart:
|
||||
specifier: ^5.5.2
|
||||
version: 5.5.2
|
||||
@ -52,6 +52,9 @@ dependencies:
|
||||
i18next-browser-languagedetector:
|
||||
specifier: ^7.0.1
|
||||
version: 7.0.1
|
||||
i18next-resources-to-backend:
|
||||
specifier: ^1.1.4
|
||||
version: 1.1.4
|
||||
is-hotkey:
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0
|
||||
@ -124,9 +127,6 @@ dependencies:
|
||||
utf8:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
y-indexeddb:
|
||||
specifier: ^9.0.9
|
||||
version: 9.0.11(yjs@13.6.1)
|
||||
yjs:
|
||||
specifier: ^13.5.51
|
||||
version: 13.6.1
|
||||
@ -201,6 +201,9 @@ devDependencies:
|
||||
prettier-plugin-tailwindcss:
|
||||
specifier: ^0.2.2
|
||||
version: 0.2.8(prettier@2.8.4)
|
||||
style-dictionary:
|
||||
specifier: ^3.8.0
|
||||
version: 3.8.0
|
||||
tailwindcss:
|
||||
specifier: ^3.2.7
|
||||
version: 3.3.2
|
||||
@ -2193,6 +2196,13 @@ packages:
|
||||
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
/camel-case@4.1.2:
|
||||
resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==}
|
||||
dependencies:
|
||||
pascal-case: 3.1.2
|
||||
tslib: 2.5.0
|
||||
dev: true
|
||||
|
||||
/camelcase-css@2.0.1:
|
||||
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
|
||||
engines: {node: '>= 6'}
|
||||
@ -2211,6 +2221,14 @@ packages:
|
||||
/caniuse-lite@1.0.30001487:
|
||||
resolution: {integrity: sha512-83564Z3yWGqXsh2vaH/mhXfEM0wX+NlBCm1jYHOb97TrTWJEmPTccZgeLTPBUUb0PNVo+oomb7wkimZBIERClA==}
|
||||
|
||||
/capital-case@1.0.4:
|
||||
resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==}
|
||||
dependencies:
|
||||
no-case: 3.0.4
|
||||
tslib: 2.5.0
|
||||
upper-case-first: 2.0.2
|
||||
dev: true
|
||||
|
||||
/chalk@2.4.2:
|
||||
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
|
||||
engines: {node: '>=4'}
|
||||
@ -2226,6 +2244,23 @@ packages:
|
||||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
/change-case@4.1.2:
|
||||
resolution: {integrity: sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==}
|
||||
dependencies:
|
||||
camel-case: 4.1.2
|
||||
capital-case: 1.0.4
|
||||
constant-case: 3.0.4
|
||||
dot-case: 3.0.4
|
||||
header-case: 2.0.4
|
||||
no-case: 3.0.4
|
||||
param-case: 3.0.4
|
||||
pascal-case: 3.1.2
|
||||
path-case: 3.0.4
|
||||
sentence-case: 3.0.4
|
||||
snake-case: 3.0.4
|
||||
tslib: 2.5.0
|
||||
dev: true
|
||||
|
||||
/char-regex@1.0.2:
|
||||
resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==}
|
||||
engines: {node: '>=10'}
|
||||
@ -2308,7 +2343,6 @@ packages:
|
||||
/commander@8.3.0:
|
||||
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
|
||||
engines: {node: '>= 12'}
|
||||
dev: false
|
||||
|
||||
/compute-scroll-into-view@1.0.20:
|
||||
resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==}
|
||||
@ -2317,6 +2351,14 @@ packages:
|
||||
/concat-map@0.0.1:
|
||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||
|
||||
/constant-case@3.0.4:
|
||||
resolution: {integrity: sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==}
|
||||
dependencies:
|
||||
no-case: 3.0.4
|
||||
tslib: 2.5.0
|
||||
upper-case: 2.0.2
|
||||
dev: true
|
||||
|
||||
/convert-source-map@1.9.0:
|
||||
resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
|
||||
|
||||
@ -2358,8 +2400,8 @@ packages:
|
||||
/csstype@3.1.2:
|
||||
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
|
||||
|
||||
/dayjs@1.11.7:
|
||||
resolution: {integrity: sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==}
|
||||
/dayjs@1.11.9:
|
||||
resolution: {integrity: sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==}
|
||||
dev: false
|
||||
|
||||
/debug@4.3.4:
|
||||
@ -2455,6 +2497,13 @@ packages:
|
||||
csstype: 3.1.2
|
||||
dev: false
|
||||
|
||||
/dot-case@3.0.4:
|
||||
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
|
||||
dependencies:
|
||||
no-case: 3.0.4
|
||||
tslib: 2.5.0
|
||||
dev: true
|
||||
|
||||
/electron-to-chromium@1.4.394:
|
||||
resolution: {integrity: sha512-0IbC2cfr8w5LxTz+nmn2cJTGafsK9iauV2r5A5scfzyovqLrxuLoxOHE5OBobP3oVIggJT+0JfKnw9sm87c8Hw==}
|
||||
|
||||
@ -2884,6 +2933,15 @@ packages:
|
||||
resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==}
|
||||
dev: true
|
||||
|
||||
/fs-extra@10.1.0:
|
||||
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
jsonfile: 6.1.0
|
||||
universalify: 2.0.0
|
||||
dev: true
|
||||
|
||||
/fs.realpath@1.0.0:
|
||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||
|
||||
@ -3029,7 +3087,6 @@ packages:
|
||||
|
||||
/graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
dev: false
|
||||
|
||||
/grapheme-splitter@1.0.4:
|
||||
resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==}
|
||||
@ -3072,6 +3129,13 @@ packages:
|
||||
dependencies:
|
||||
function-bind: 1.1.1
|
||||
|
||||
/header-case@2.0.4:
|
||||
resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==}
|
||||
dependencies:
|
||||
capital-case: 1.0.4
|
||||
tslib: 2.5.0
|
||||
dev: true
|
||||
|
||||
/hoist-non-react-statics@3.3.2:
|
||||
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
|
||||
dependencies:
|
||||
@ -3099,6 +3163,12 @@ packages:
|
||||
'@babel/runtime': 7.21.5
|
||||
dev: false
|
||||
|
||||
/i18next-resources-to-backend@1.1.4:
|
||||
resolution: {integrity: sha512-hMyr9AOmIea17AOaVe1srNxK/l3mbk81P7Uf3fdcjlw3ehZy3UNTd0OP3EEi6yu4J02kf9jzhCcjokz6AFlEOg==}
|
||||
dependencies:
|
||||
'@babel/runtime': 7.21.5
|
||||
dev: false
|
||||
|
||||
/i18next@22.4.15:
|
||||
resolution: {integrity: sha512-yYudtbFrrmWKLEhl6jvKUYyYunj4bTBCe2qIUYAxbXoPusY7YmdwPvOE6fx6UIfWvmlbCWDItr7wIs8KEBZ5Zg==}
|
||||
dependencies:
|
||||
@ -3825,6 +3895,18 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
/jsonc-parser@3.2.0:
|
||||
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
|
||||
dev: true
|
||||
|
||||
/jsonfile@6.1.0:
|
||||
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
||||
dependencies:
|
||||
universalify: 2.0.0
|
||||
optionalDependencies:
|
||||
graceful-fs: 4.2.11
|
||||
dev: true
|
||||
|
||||
/jsx-ast-utils@3.3.3:
|
||||
resolution: {integrity: sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==}
|
||||
engines: {node: '>=4.0'}
|
||||
@ -3904,7 +3986,6 @@ packages:
|
||||
|
||||
/lodash@4.17.21:
|
||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||
dev: false
|
||||
|
||||
/loose-envify@1.4.0:
|
||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||
@ -3912,6 +3993,12 @@ packages:
|
||||
dependencies:
|
||||
js-tokens: 4.0.0
|
||||
|
||||
/lower-case@2.0.2:
|
||||
resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
|
||||
dependencies:
|
||||
tslib: 2.5.0
|
||||
dev: true
|
||||
|
||||
/lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
dependencies:
|
||||
@ -4003,6 +4090,13 @@ packages:
|
||||
/natural-compare@1.4.0:
|
||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||
|
||||
/no-case@3.0.4:
|
||||
resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
|
||||
dependencies:
|
||||
lower-case: 2.0.2
|
||||
tslib: 2.5.0
|
||||
dev: true
|
||||
|
||||
/node-int64@0.4.0:
|
||||
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
|
||||
dev: false
|
||||
@ -4151,6 +4245,13 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/param-case@3.0.4:
|
||||
resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
|
||||
dependencies:
|
||||
dot-case: 3.0.4
|
||||
tslib: 2.5.0
|
||||
dev: true
|
||||
|
||||
/parchment@1.1.4:
|
||||
resolution: {integrity: sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==}
|
||||
|
||||
@ -4170,6 +4271,20 @@ packages:
|
||||
lines-and-columns: 1.2.4
|
||||
dev: false
|
||||
|
||||
/pascal-case@3.1.2:
|
||||
resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==}
|
||||
dependencies:
|
||||
no-case: 3.0.4
|
||||
tslib: 2.5.0
|
||||
dev: true
|
||||
|
||||
/path-case@3.0.4:
|
||||
resolution: {integrity: sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==}
|
||||
dependencies:
|
||||
dot-case: 3.0.4
|
||||
tslib: 2.5.0
|
||||
dev: true
|
||||
|
||||
/path-exists@4.0.0:
|
||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||
engines: {node: '>=8'}
|
||||
@ -4799,6 +4914,14 @@ packages:
|
||||
dependencies:
|
||||
lru-cache: 6.0.0
|
||||
|
||||
/sentence-case@3.0.4:
|
||||
resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==}
|
||||
dependencies:
|
||||
no-case: 3.0.4
|
||||
tslib: 2.5.0
|
||||
upper-case-first: 2.0.2
|
||||
dev: true
|
||||
|
||||
/shebang-command@2.0.0:
|
||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||
engines: {node: '>=8'}
|
||||
@ -4858,6 +4981,13 @@ packages:
|
||||
tiny-warning: 1.0.3
|
||||
dev: false
|
||||
|
||||
/snake-case@3.0.4:
|
||||
resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==}
|
||||
dependencies:
|
||||
dot-case: 3.0.4
|
||||
tslib: 2.5.0
|
||||
dev: true
|
||||
|
||||
/source-map-js@1.0.2:
|
||||
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -4966,6 +5096,22 @@ packages:
|
||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
/style-dictionary@3.8.0:
|
||||
resolution: {integrity: sha512-wHlB/f5eO3mDcYv6WtOz6gvQC477jBKrwuIXe+PtHskTCBsJdAOvL8hCquczJxDui2TnwpeNE+2msK91JJomZg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
change-case: 4.1.2
|
||||
commander: 8.3.0
|
||||
fs-extra: 10.1.0
|
||||
glob: 7.2.3
|
||||
json5: 2.2.3
|
||||
jsonc-parser: 3.2.0
|
||||
lodash: 4.17.21
|
||||
tinycolor2: 1.6.0
|
||||
dev: true
|
||||
|
||||
/stylis@4.2.0:
|
||||
resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==}
|
||||
dev: false
|
||||
@ -5077,6 +5223,10 @@ packages:
|
||||
resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
|
||||
dev: false
|
||||
|
||||
/tinycolor2@1.6.0:
|
||||
resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==}
|
||||
dev: true
|
||||
|
||||
/tmpl@1.0.5:
|
||||
resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==}
|
||||
dev: false
|
||||
@ -5105,7 +5255,6 @@ packages:
|
||||
|
||||
/tslib@2.5.0:
|
||||
resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==}
|
||||
dev: false
|
||||
|
||||
/tsutils@3.21.0(typescript@4.9.5):
|
||||
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
|
||||
@ -5161,6 +5310,11 @@ packages:
|
||||
which-boxed-primitive: 1.0.2
|
||||
dev: true
|
||||
|
||||
/universalify@2.0.0:
|
||||
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
dev: true
|
||||
|
||||
/update-browserslist-db@1.0.11(browserslist@4.21.5):
|
||||
resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==}
|
||||
hasBin: true
|
||||
@ -5171,6 +5325,18 @@ packages:
|
||||
escalade: 3.1.1
|
||||
picocolors: 1.0.0
|
||||
|
||||
/upper-case-first@2.0.2:
|
||||
resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==}
|
||||
dependencies:
|
||||
tslib: 2.5.0
|
||||
dev: true
|
||||
|
||||
/upper-case@2.0.2:
|
||||
resolution: {integrity: sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==}
|
||||
dependencies:
|
||||
tslib: 2.5.0
|
||||
dev: true
|
||||
|
||||
/uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
dependencies:
|
||||
@ -5313,15 +5479,6 @@ packages:
|
||||
signal-exit: 3.0.7
|
||||
dev: false
|
||||
|
||||
/y-indexeddb@9.0.11(yjs@13.6.1):
|
||||
resolution: {integrity: sha512-HOKQ70qW1h2WJGtOKu9rE8fbX86ExVZedecndMuhwax3yM4DQsQzCTGHt/jvTrFZr/9Ahvd8neD6aZ4dMMjtdg==}
|
||||
peerDependencies:
|
||||
yjs: ^13.0.0
|
||||
dependencies:
|
||||
lib0: 0.2.74
|
||||
yjs: 13.6.1
|
||||
dev: false
|
||||
|
||||
/y-protocols@1.0.5:
|
||||
resolution: {integrity: sha512-Wil92b7cGk712lRHDqS4T90IczF6RkcvCwAD0A2OPg+adKmOe+nOiT/N2hvpQIWS3zfjmtL4CPaH5sIW1Hkm/A==}
|
||||
dependencies:
|
||||
|
53
frontend/appflowy_tauri/scripts/i18n/index.cjs
Normal file
53
frontend/appflowy_tauri/scripts/i18n/index.cjs
Normal file
@ -0,0 +1,53 @@
|
||||
const languages = [
|
||||
'ar-SA',
|
||||
'ca-ES',
|
||||
'de-DE',
|
||||
'en',
|
||||
'es-VE',
|
||||
'eu-ES',
|
||||
'fr-FR',
|
||||
'hu-HU',
|
||||
'id-ID',
|
||||
'it-IT',
|
||||
'ja-JP',
|
||||
'ko-KR',
|
||||
'pl-PL',
|
||||
'pt-BR',
|
||||
'pt-PT',
|
||||
'ru-RU',
|
||||
'sv',
|
||||
'tr-TR',
|
||||
'zh-CN',
|
||||
'zh-TW',
|
||||
];
|
||||
|
||||
const fs = require('fs');
|
||||
languages.forEach(language => {
|
||||
const json = require(`../../../resources/translations/${language}.json`);
|
||||
const outputJSON = flattenJSON(json);
|
||||
const output = JSON.stringify(outputJSON);
|
||||
const isExistDir = fs.existsSync('./src/appflowy_app/i18n/translations');
|
||||
if (!isExistDir) {
|
||||
fs.mkdirSync('./src/appflowy_app/i18n/translations');
|
||||
}
|
||||
fs.writeFile(`./src/appflowy_app/i18n/translations/${language}.json`, new Uint8Array(Buffer.from(output)), (res) => {
|
||||
if (res) {
|
||||
console.error(res);
|
||||
}
|
||||
})
|
||||
})
|
||||
function flattenJSON(obj, prefix = '') {
|
||||
let result = {};
|
||||
|
||||
for (let key in obj) {
|
||||
if (typeof obj[key] === 'object' && obj[key] !== null) {
|
||||
const nestedKeys = flattenJSON(obj[key], `${prefix}${key}.`);
|
||||
result = { ...result, ...nestedKeys };
|
||||
} else {
|
||||
result[`${prefix}${key}`] = obj[key];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
8
frontend/appflowy_tauri/src/appflowy_app/@types/i18next.d.ts
vendored
Normal file
8
frontend/appflowy_tauri/src/appflowy_app/@types/i18next.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
import resources from './resources';
|
||||
|
||||
declare module 'i18next' {
|
||||
interface CustomTypeOptions {
|
||||
defaultNS: 'translation';
|
||||
resources: typeof resources;
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
import translation from '$app/i18n/translations/en.json';
|
||||
|
||||
const resources = {
|
||||
translation,
|
||||
} as const;
|
||||
|
||||
export default resources;
|
@ -4,14 +4,12 @@ import { Provider } from 'react-redux';
|
||||
import { store } from './stores/store';
|
||||
|
||||
import { ErrorHandlerPage } from './components/error/ErrorHandlerPage';
|
||||
import initializeI18n from './stores/i18n/initializeI18n';
|
||||
import '$app/i18n/config';
|
||||
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
|
||||
import AppMain from '$app/AppMain';
|
||||
|
||||
initializeI18n();
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
|
@ -19,10 +19,20 @@ export function useUserSetting() {
|
||||
useEffect(() => {
|
||||
userSettingController?.getAppearanceSetting().then((res) => {
|
||||
if (!res) return;
|
||||
const locale = res.locale;
|
||||
let language = 'en';
|
||||
|
||||
if (locale.language_code && locale.country_code) {
|
||||
language = `${locale.language_code}-${locale.country_code}`;
|
||||
} else if (locale.language_code) {
|
||||
language = locale.language_code;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
currentUserActions.setUserSetting({
|
||||
themeMode: res.theme_mode,
|
||||
theme: res.theme as Theme,
|
||||
language: language,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@ -14,23 +14,23 @@ export const Button = ({
|
||||
useEffect(() => {
|
||||
switch (size) {
|
||||
case 'primary':
|
||||
setCls('w-[340px] h-[48px] flex items-center justify-center rounded-lg bg-content-default text-content-onfill');
|
||||
setCls('w-[340px] h-[48px] flex items-center justify-center rounded-lg bg-content-default text-content-on-fill');
|
||||
break;
|
||||
case 'medium':
|
||||
setCls('w-[170px] h-[48px] flex items-center justify-center rounded-lg bg-content-default text-content-onfill');
|
||||
setCls('w-[170px] h-[48px] flex items-center justify-center rounded-lg bg-content-default text-content-on-fill');
|
||||
break;
|
||||
case 'small':
|
||||
setCls(
|
||||
'w-[68px] h-[32px] flex items-center justify-center rounded-lg bg-content-default text-content-onfill text-xs hover:bg-content-hover'
|
||||
'w-[68px] h-[32px] flex items-center justify-center rounded-lg bg-content-default text-content-on-fill text-xs hover:bg-content-hover'
|
||||
);
|
||||
break;
|
||||
case 'medium-transparent':
|
||||
setCls(
|
||||
'w-[170px] h-[48px] flex items-center justify-center rounded-lg border border-content-default text-content-default transition-colors duration-300 hover:bg-content-hover hover:text-content-onfill'
|
||||
'w-[170px] h-[48px] flex items-center justify-center rounded-lg border border-content-default text-content-default transition-colors duration-300 hover:bg-content-blue-50 hover:text-content-on-fill'
|
||||
);
|
||||
break;
|
||||
case 'box-small-transparent':
|
||||
setCls('text-icon-default w-[24px] h-[24px] rounded hover:bg-fill-hover');
|
||||
setCls('text-icon-default w-[24px] h-[24px] rounded hover:bg-fill-list-hover');
|
||||
break;
|
||||
}
|
||||
}, [size]);
|
||||
|
@ -32,7 +32,7 @@ export const ChangeFieldTypePopup = ({
|
||||
<button
|
||||
onClick={() => onClick(t)}
|
||||
key={i}
|
||||
className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-fill-hover'}
|
||||
className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-fill-list-hover'}
|
||||
>
|
||||
<i className={'h-5 w-5'}>
|
||||
<FieldTypeIcon fieldType={t}></FieldTypeIcon>
|
||||
|
@ -37,7 +37,7 @@ export const CheckListOption = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={'flex cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-fill-hover'}
|
||||
className={'flex cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-fill-list-hover'}
|
||||
onClick={() =>
|
||||
onToggleOptionClick(
|
||||
new SelectOptionPB({
|
||||
|
@ -69,7 +69,7 @@ export const EditCheckListPopup = ({
|
||||
top={top}
|
||||
>
|
||||
<div onKeyDown={onKeyDownWrapper} className={'flex flex-col gap-2 p-2'}>
|
||||
<div className={'flex flex-1 items-center gap-2 rounded border border-line-border bg-fill-hover px-2 '}>
|
||||
<div className={'flex flex-1 items-center gap-2 rounded border border-line-divider bg-fill-list-hover px-2 '}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
className={'py-2'}
|
||||
@ -78,11 +78,13 @@ export const EditCheckListPopup = ({
|
||||
onKeyDown={onKeyDown}
|
||||
onBlur={() => onBlur()}
|
||||
/>
|
||||
<div className={'font-mono text-shade-3'}>{value.length}/30</div>
|
||||
<div className={'text-shade-3 font-mono'}>{value.length}/30</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onDeleteOptionClick()}
|
||||
className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 text-fill-default hover:bg-fill-hover'}
|
||||
className={
|
||||
'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 text-fill-default hover:bg-fill-list-hover'
|
||||
}
|
||||
>
|
||||
<i className={'h-5 w-5'}>
|
||||
<TrashSvg></TrashSvg>
|
||||
|
@ -80,7 +80,9 @@ function PopupItem({
|
||||
return (
|
||||
<button
|
||||
onClick={() => changeFormat(format)}
|
||||
className={'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-fill-hover'}
|
||||
className={
|
||||
'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-fill-list-hover'
|
||||
}
|
||||
>
|
||||
{text}
|
||||
|
||||
|
@ -87,20 +87,24 @@ export const DateTypeOptions = ({
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col'}>
|
||||
<hr className={'-mx-2 my-2 border-shade-6'} />
|
||||
<hr className={'border-shade-6 -mx-2 my-2'} />
|
||||
<button
|
||||
onClick={_onDateFormatClick}
|
||||
className={'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-2 hover:bg-fill-hover'}
|
||||
className={
|
||||
'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-2 hover:bg-fill-list-hover'
|
||||
}
|
||||
>
|
||||
<span>{t('grid.field.dateFormat')}</span>
|
||||
<i className={'h-5 w-5'}>
|
||||
<MoreSvg></MoreSvg>
|
||||
</i>
|
||||
</button>
|
||||
<hr className={'-mx-2 my-2 border-line-border'} />
|
||||
<hr className={'-mx-2 my-2 border-line-divider'} />
|
||||
<button
|
||||
onClick={() => toggleIncludeTime()}
|
||||
className={'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-2 hover:bg-fill-hover'}
|
||||
className={
|
||||
'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-2 hover:bg-fill-list-hover'
|
||||
}
|
||||
>
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<span>{t('grid.field.includeTime')}</span>
|
||||
@ -112,7 +116,9 @@ export const DateTypeOptions = ({
|
||||
|
||||
<button
|
||||
onClick={_onTimeFormatClick}
|
||||
className={'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-2 hover:bg-fill-hover'}
|
||||
className={
|
||||
'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-2 hover:bg-fill-list-hover'
|
||||
}
|
||||
>
|
||||
<span>{t('grid.field.timeFormat')}</span>
|
||||
<i className={'h-5 w-5'}>
|
||||
|
@ -93,7 +93,9 @@ const FormatButton = ({ title, checked, onClick }: { title: string; checked: boo
|
||||
return (
|
||||
<button
|
||||
onClick={() => onClick()}
|
||||
className={'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-fill-hover'}
|
||||
className={
|
||||
'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-fill-list-hover'
|
||||
}
|
||||
>
|
||||
<span className={'block pr-8'}>{title}</span>
|
||||
{checked && (
|
||||
|
@ -41,7 +41,9 @@ export const TimeFormatPopup = ({
|
||||
<PopupWindow className={'p-2 text-xs'} onOutsideClick={onOutsideClick} left={left} top={top}>
|
||||
<button
|
||||
onClick={() => changeFormat(TimeFormatPB.TwelveHour)}
|
||||
className={'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-fill-hover'}
|
||||
className={
|
||||
'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-fill-list-hover'
|
||||
}
|
||||
>
|
||||
{t('grid.field.timeFormatTwelveHour')}
|
||||
|
||||
@ -53,7 +55,9 @@ export const TimeFormatPopup = ({
|
||||
</button>
|
||||
<button
|
||||
onClick={() => changeFormat(TimeFormatPB.TwentyFourHour)}
|
||||
className={'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-fill-hover'}
|
||||
className={
|
||||
'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-fill-list-hover'
|
||||
}
|
||||
>
|
||||
{t('grid.field.timeFormatTwentyFourHour')}
|
||||
|
||||
|
@ -59,7 +59,7 @@ export const EditCellWrapper = ({
|
||||
<div
|
||||
ref={el}
|
||||
onClick={() => onClick()}
|
||||
className={'flex h-5 w-5 rounded text-icon-default hover:bg-fill-hover'}
|
||||
className={'text-icon-default flex h-5 w-5 rounded hover:bg-fill-list-hover'}
|
||||
>
|
||||
<DragElementSvg></DragElementSvg>
|
||||
</div>
|
||||
@ -72,7 +72,7 @@ export const EditCellWrapper = ({
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={'w-full cursor-pointer rounded-lg pl-3 text-sm hover:bg-fill-selector'}>
|
||||
<div className={'w-full cursor-pointer rounded-lg pl-3 text-sm hover:bg-content-blue-50'}>
|
||||
{(cellIdentifier.fieldType === FieldType.SingleSelect ||
|
||||
cellIdentifier.fieldType === FieldType.MultiSelect) &&
|
||||
cellController && (
|
||||
|
@ -101,7 +101,7 @@ export const EditFieldPopup = ({
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onBlur={() => save()}
|
||||
className={
|
||||
'flex-1 rounded border border-line-border px-2 py-2 hover:border-fill-default focus:border-fill-default'
|
||||
'flex-1 rounded border border-line-divider px-2 py-2 hover:border-fill-default focus:border-fill-default'
|
||||
}
|
||||
/>
|
||||
|
||||
@ -109,7 +109,7 @@ export const EditFieldPopup = ({
|
||||
ref={changeTypeButtonRef}
|
||||
onClick={() => onChangeFieldTypeClick()}
|
||||
className={
|
||||
'relative flex cursor-pointer items-center justify-between rounded-lg py-2 text-text-title hover:bg-fill-hover'
|
||||
'relative flex cursor-pointer items-center justify-between rounded-lg py-2 text-text-title hover:bg-fill-list-hover'
|
||||
}
|
||||
>
|
||||
<button className={'flex cursor-pointer items-center gap-2 rounded-lg pl-2'}>
|
||||
@ -129,10 +129,12 @@ export const EditFieldPopup = ({
|
||||
|
||||
{cellIdentifier.fieldType === FieldType.Number && (
|
||||
<>
|
||||
<hr className={'-mx-2 border-line-border'} />
|
||||
<hr className={'-mx-2 border-line-divider'} />
|
||||
<button
|
||||
onClick={onNumberFormatClick}
|
||||
className={'flex w-full cursor-pointer items-center justify-between rounded-lg py-2 hover:bg-fill-hover'}
|
||||
className={
|
||||
'flex w-full cursor-pointer items-center justify-between rounded-lg py-2 hover:bg-fill-list-hover'
|
||||
}
|
||||
>
|
||||
<span className={'pl-2'}>{t('grid.field.numberFormat')}</span>
|
||||
<span className={'pr-2'}>
|
||||
|
@ -206,13 +206,13 @@ export const EditRow = ({
|
||||
className={`relative flex h-[90%] w-[70%] flex-col gap-8 rounded-xl bg-bg-body `}
|
||||
>
|
||||
<div onClick={() => onCloseClick()} className={'absolute right-1 top-1'}>
|
||||
<button className={'block h-8 w-8 rounded-lg text-text-title hover:bg-fill-hover'}>
|
||||
<button className={'block h-8 w-8 rounded-lg text-text-title hover:bg-fill-list-hover'}>
|
||||
<CloseSvg></CloseSvg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={'flex h-full'}>
|
||||
<div className={'flex h-full flex-1 flex-col border-r border-line-border pb-4 pt-6'}>
|
||||
<div className={'flex h-full flex-1 flex-col border-r border-line-divider pb-4 pt-6'}>
|
||||
<div className={'pb-4 pl-12'}>
|
||||
<button className={'flex items-center gap-2 p-4'}>
|
||||
<i className={'h-5 w-5'}>
|
||||
@ -254,10 +254,10 @@ export const EditRow = ({
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
|
||||
<div className={'border-t border-line-border px-8 pt-2'}>
|
||||
<div className={'border-t border-line-divider px-8 pt-2'}>
|
||||
<button
|
||||
onClick={() => onNewColumnClick()}
|
||||
className={'flex w-full items-center gap-2 rounded-lg px-4 py-2 hover:bg-fill-hover'}
|
||||
className={'flex w-full items-center gap-2 rounded-lg px-4 py-2 hover:bg-fill-list-hover'}
|
||||
>
|
||||
<i className={'h-5 w-5'}>
|
||||
<AddSvg></AddSvg>
|
||||
|
@ -53,9 +53,9 @@ export const CellOption = ({
|
||||
return (
|
||||
<div
|
||||
onClick={onToggleOptionClick}
|
||||
className={'flex cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-fill-hover'}
|
||||
className={'flex cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-fill-list-hover'}
|
||||
>
|
||||
<div className={`${getBgColor(option.color)} rounded px-2 py-0.5 text-content-onfill`}>{option.title}</div>
|
||||
<div className={`${getBgColor(option.color)} rounded px-2 py-0.5 text-text-title`}>{option.title}</div>
|
||||
<div className={'flex items-center'}>
|
||||
{checked && (
|
||||
<button className={'h-5 w-5 p-1'}>
|
||||
|
@ -19,13 +19,9 @@ export const CellOptions = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
onClick={onClick}
|
||||
className={'flex w-full flex-wrap items-center gap-2 px-4 py-1 text-xs text-content-onfill'}
|
||||
>
|
||||
<div ref={ref} onClick={onClick} className={'flex w-full flex-wrap items-center gap-2 px-4 py-1 text-xs'}>
|
||||
{data?.select_options?.map((option, index) => (
|
||||
<div className={`${getBgColor(option.color)} rounded px-2 py-0.5`} key={index}>
|
||||
<div className={`${getBgColor(option.color)} rounded px-2 py-0.5 text-text-title`} key={index}>
|
||||
{option?.name ?? ''}
|
||||
</div>
|
||||
))}
|
||||
|
@ -59,7 +59,7 @@ export const CellOptionsPopup = ({
|
||||
<div onKeyDown={onKeyDownWrapper} className={'flex flex-col gap-2 p-2'}>
|
||||
<div
|
||||
className={
|
||||
'flex flex-1 items-center gap-2 rounded border border-line-border px-2 hover:border-fill-default focus:border-fill-default'
|
||||
'flex flex-1 items-center gap-2 rounded border border-line-divider px-2 hover:border-fill-default focus:border-fill-default'
|
||||
}
|
||||
>
|
||||
<div className={'flex flex-wrap items-center gap-2 text-text-title'}>
|
||||
|
@ -86,7 +86,7 @@ export const EditCellOptionPopup = ({
|
||||
<div onKeyDown={onKeyDownWrapper} className={'flex flex-col gap-2 p-2'}>
|
||||
<div
|
||||
className={
|
||||
'flex flex-1 items-center gap-2 rounded border border-line-border px-2 hover:border-fill-hover focus:border-fill-hover'
|
||||
'flex flex-1 items-center gap-2 rounded border border-line-divider px-2 hover:border-fill-hover focus:border-fill-hover'
|
||||
}
|
||||
>
|
||||
<input
|
||||
@ -101,7 +101,9 @@ export const EditCellOptionPopup = ({
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onDeleteOptionClick()}
|
||||
className={'text-main-alert flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 hover:bg-fill-hover'}
|
||||
className={
|
||||
'text-main-alert flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 hover:bg-fill-list-hover'
|
||||
}
|
||||
>
|
||||
<i className={'h-5 w-5'}>
|
||||
<TrashSvg></TrashSvg>
|
||||
@ -184,7 +186,7 @@ const ColorItem = ({
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={'flex cursor-pointer items-center justify-between rounded-lg p-2 hover:bg-fill-hover'}
|
||||
className={'flex cursor-pointer items-center justify-between rounded-lg p-2 hover:bg-fill-list-hover'}
|
||||
onClick={() => onClick()}
|
||||
>
|
||||
<div className={'flex items-center gap-2'}>
|
||||
|
@ -20,7 +20,7 @@ export const SelectedOption = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${getBgColor(option.color)} flex items-center gap-0.5 rounded px-1 py-0.5 text-content-onfill`}>
|
||||
<div className={`${getBgColor(option.color)} flex items-center gap-0.5 rounded px-1 py-0.5 text-content-on-fill`}>
|
||||
<span>{option?.name ?? ''}</span>
|
||||
<button onClick={onUnselectOptionClick} className={'h-5 w-5 cursor-pointer'}>
|
||||
<CloseSvg></CloseSvg>
|
||||
|
@ -103,7 +103,7 @@ export const PropertiesPanel = ({
|
||||
<div
|
||||
onClick={() => setShowAddedProperties(!showAddedProperties)}
|
||||
className={
|
||||
'flex cursor-pointer items-center justify-between gap-8 rounded-lg px-2 py-2 text-text-title hover:bg-bg-base'
|
||||
'flex cursor-pointer items-center justify-between gap-8 rounded-lg px-2 py-2 text-text-title hover:bg-fill-list-active'
|
||||
}
|
||||
>
|
||||
<div className={'text-sm'}>Added Properties</div>
|
||||
@ -118,7 +118,7 @@ export const PropertiesPanel = ({
|
||||
key={cellIndex}
|
||||
onMouseEnter={() => setHoveredPropertyIndex(cellIndex)}
|
||||
className={
|
||||
'flex cursor-pointer items-center justify-between gap-4 rounded-lg px-2 py-1 hover:bg-fill-hover'
|
||||
'flex cursor-pointer items-center justify-between gap-4 rounded-lg px-2 py-1 hover:bg-fill-list-hover'
|
||||
}
|
||||
>
|
||||
<div className={'flex items-center gap-2 text-text-title '}>
|
||||
@ -148,7 +148,9 @@ export const PropertiesPanel = ({
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setShowBasicProperties(!showBasicProperties)}
|
||||
className={'flex cursor-pointer items-center justify-between gap-8 rounded-lg px-2 py-2 hover:bg-fill-hover'}
|
||||
className={
|
||||
'flex cursor-pointer items-center justify-between gap-8 rounded-lg px-2 py-2 hover:bg-fill-list-active'
|
||||
}
|
||||
>
|
||||
<div className={'text-sm'}>Basic Properties</div>
|
||||
<i className={`h-5 w-5 transition-transform duration-500 ${showBasicProperties && 'rotate-180'}`}>
|
||||
@ -162,7 +164,7 @@ export const PropertiesPanel = ({
|
||||
<button
|
||||
onClick={() => addSelectedFieldType(type)}
|
||||
key={i}
|
||||
className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-fill-hover'}
|
||||
className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-fill-list-hover'}
|
||||
>
|
||||
<i className={'h-5 w-5'}>
|
||||
<FieldTypeIcon fieldType={type}></FieldTypeIcon>
|
||||
@ -177,7 +179,9 @@ export const PropertiesPanel = ({
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setShowAdvancedProperties(!showAdvancedProperties)}
|
||||
className={'flex cursor-pointer items-center justify-between gap-8 rounded-lg px-2 py-2 hover:bg-fill-hover'}
|
||||
className={
|
||||
'flex cursor-pointer items-center justify-between gap-8 rounded-lg px-2 py-2 hover:bg-fill-list-active'
|
||||
}
|
||||
>
|
||||
<div className={'text-sm'}>Advanced Properties</div>
|
||||
<i className={`h-5 w-5 transition-transform duration-500 ${showAdvancedProperties && 'rotate-180'}`}>
|
||||
@ -187,19 +191,25 @@ export const PropertiesPanel = ({
|
||||
<div className={'flex flex-col gap-2 text-xs'}>
|
||||
{showAdvancedProperties && (
|
||||
<div className={'flex flex-col'}>
|
||||
<button className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-fill-hover'}>
|
||||
<button
|
||||
className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-fill-list-hover'}
|
||||
>
|
||||
<i className={'h-5 w-5'}>
|
||||
<MultiSelectTypeSvg></MultiSelectTypeSvg>
|
||||
</i>
|
||||
<span>Last edited time</span>
|
||||
</button>
|
||||
<button className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-fill-hover'}>
|
||||
<button
|
||||
className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-fill-list-hover'}
|
||||
>
|
||||
<i className={'h-5 w-5'}>
|
||||
<DocumentSvg></DocumentSvg>
|
||||
</i>
|
||||
<span>Document</span>
|
||||
</button>
|
||||
<button className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-fill-hover'}>
|
||||
<button
|
||||
className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-fill-list-hover'}
|
||||
>
|
||||
<i className={'h-5 w-5'}>
|
||||
<SingleSelectTypeSvg></SingleSelectTypeSvg>
|
||||
</i>
|
||||
|
@ -39,7 +39,7 @@ export const PopupSelect = ({
|
||||
{items.map((item, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className={'flex w-full cursor-pointer items-center gap-2 rounded-lg px-2 py-2 hover:bg-fill-hover'}
|
||||
className={'flex w-full cursor-pointer items-center gap-2 rounded-lg px-2 py-2 hover:bg-fill-list-hover'}
|
||||
onClick={(e) => handleClick(e, item)}
|
||||
>
|
||||
<>
|
||||
|
@ -42,7 +42,7 @@ export const PopupWindow = ({
|
||||
<div
|
||||
ref={ref}
|
||||
className={
|
||||
'fixed z-10 rounded-lg bg-bg-base shadow-md transition-opacity duration-300 ' +
|
||||
'fixed z-10 rounded-lg bg-bg-body shadow-md transition-opacity duration-300 ' +
|
||||
(adjustedTop === -100 && adjustedLeft === -100 ? 'opacity-0 ' : 'opacity-100 ') +
|
||||
(className ?? '')
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ export const SearchInput = () => {
|
||||
const [active, setActive] = useState(false);
|
||||
|
||||
return (
|
||||
<div className={`flex items-center rounded-lg border p-2 ${active ? 'border-fill-default' : 'border-line-border'}`}>
|
||||
<div className={`flex items-center rounded-lg border p-2 ${active ? 'border-fill-default' : 'border-line-divider'}`}>
|
||||
<i className='mr-2 h-5 w-5'>
|
||||
<SearchSvg />
|
||||
</i>
|
||||
|
@ -3,23 +3,23 @@ import { SelectOptionColorPB } from '../../../services/backend';
|
||||
export const getBgColor = (color: SelectOptionColorPB | undefined): string => {
|
||||
switch (color) {
|
||||
case SelectOptionColorPB.Purple:
|
||||
return 'bg-tint-1';
|
||||
return 'bg-tint-purple';
|
||||
case SelectOptionColorPB.Pink:
|
||||
return 'bg-tint-2';
|
||||
return 'bg-tint-pink';
|
||||
case SelectOptionColorPB.LightPink:
|
||||
return 'bg-tint-3';
|
||||
return 'bg-tint-red';
|
||||
case SelectOptionColorPB.Orange:
|
||||
return 'bg-tint-4';
|
||||
return 'bg-tint-orange';
|
||||
case SelectOptionColorPB.Yellow:
|
||||
return 'bg-tint-5';
|
||||
return 'bg-tint-yellow';
|
||||
case SelectOptionColorPB.Lime:
|
||||
return 'bg-tint-6';
|
||||
return 'bg-tint-lime';
|
||||
case SelectOptionColorPB.Green:
|
||||
return 'bg-tint-7';
|
||||
return 'bg-tint-green';
|
||||
case SelectOptionColorPB.Aqua:
|
||||
return 'bg-tint-8';
|
||||
return 'bg-tint-aqua';
|
||||
case SelectOptionColorPB.Blue:
|
||||
return 'bg-tint-9';
|
||||
return 'bg-tint-blue';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
export const EditorCheckSvg = () => {
|
||||
return (
|
||||
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<rect x='2' y='2' width='12' height='12' rx='4' fill={'var(--color-fill-default)'} />
|
||||
<rect x='2' y='2' width='12' height='12' rx='4' fill={'var(--fill-default)'} />
|
||||
<path
|
||||
d='M6 8L7.61538 9.5L10.5 6.5'
|
||||
stroke={'var(--color-content-onfill)'}
|
||||
stroke={'var(--content-on-fill)'}
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
|
@ -1,7 +1,7 @@
|
||||
export const EditorUncheckSvg = () => {
|
||||
return (
|
||||
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<rect x='2.5' y='2.5' width='11' height='11' rx='3.5' stroke={'var(--color-icon-secondary)'} />
|
||||
<rect x='2.5' y='2.5' width='11' height='11' rx='3.5' stroke={'var(--line-border)'} />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
@ -1,10 +1,10 @@
|
||||
export const FullView = () => {
|
||||
return (
|
||||
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path d='M6 13H3V10' stroke='var(--color-text-title)' strokeLinecap='round' strokeLinejoin='round' />
|
||||
<path d='M10 3H13V6' stroke='var(--color-text-title)' strokeLinecap='round' strokeLinejoin='round' />
|
||||
<path d='M3 13L7 9' stroke='var(--color-text-title)' strokeLinecap='round' strokeLinejoin='round' />
|
||||
<path d='M13 3L9 7' stroke='var(--color-text-title)' strokeLinecap='round' strokeLinejoin='round' />
|
||||
<path d='M6 13H3V10' stroke='var(--text-title)' strokeLinecap='round' strokeLinejoin='round' />
|
||||
<path d='M10 3H13V6' stroke='var(--text-title)' strokeLinecap='round' strokeLinejoin='round' />
|
||||
<path d='M3 13L7 9' stroke='var(--text-title)' strokeLinecap='round' strokeLinejoin='round' />
|
||||
<path d='M13 3L9 7' stroke='var(--text-title)' strokeLinecap='round' strokeLinejoin='round' />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
@ -3,29 +3,29 @@ export const GroupBySvg = () => {
|
||||
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='M10 2H13C13.5523 2 14 2.44772 14 3V6'
|
||||
stroke='var(--color-text-title)'
|
||||
stroke='var(--text-title)'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<path
|
||||
d='M6 2H3C2.44772 2 2 2.44772 2 3V6'
|
||||
stroke='var(--color-text-title)'
|
||||
stroke='var(--text-title)'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<path
|
||||
d='M6 14H3C2.44772 14 2 13.5523 2 13V10'
|
||||
stroke='var(--color-text-title)'
|
||||
stroke='var(--text-title)'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<path
|
||||
d='M10 14H13C13.5523 14 14 13.5523 14 13V10'
|
||||
stroke='var(--color-text-title)'
|
||||
stroke='var(--text-title)'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<rect x='6' y='6' width='4' height='4' rx='1' stroke='var(--color-text-title)' />
|
||||
<rect x='6' y='6' width='4' height='4' rx='1' stroke='var(--text-title)' />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
@ -64,9 +64,12 @@ export const BoardCard = ({
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
onClick={() => onOpenRow(rowInfo)}
|
||||
className={`relative cursor-pointer select-none rounded-lg border border-line-border bg-bg-body px-3 py-2 transition-transform duration-100 hover:bg-fill-selector `}
|
||||
className={`relative cursor-pointer select-none rounded-lg bg-bg-body px-3 py-2 transition-transform duration-100 hover:bg-content-blue-50 `}
|
||||
>
|
||||
<button onClick={onDetailClick} className={'absolute right-4 top-2.5 h-5 w-5 rounded hover:bg-fill-hover'}>
|
||||
<button
|
||||
onClick={onDetailClick}
|
||||
className={'absolute right-4 top-2.5 h-5 w-5 rounded hover:bg-fill-list-hover'}
|
||||
>
|
||||
<Details2Svg></Details2Svg>
|
||||
</button>
|
||||
<div className={'flex flex-col gap-3'}>
|
||||
@ -95,7 +98,7 @@ export const BoardCard = ({
|
||||
>
|
||||
<button
|
||||
key={index}
|
||||
className={'flex w-full cursor-pointer items-center gap-2 rounded-lg px-2 py-2 hover:bg-fill-hover'}
|
||||
className={'flex w-full cursor-pointer items-center gap-2 rounded-lg px-2 py-2 hover:bg-fill-list-hover'}
|
||||
onClick={() => onDeleteRowClick()}
|
||||
>
|
||||
<i className={'h-5 w-5'}>
|
||||
|
@ -15,7 +15,7 @@ export const BoardFieldsPopup = ({ hidePopup }: { hidePopup: () => void }) => {
|
||||
<div ref={ref} className={'absolute left-full top-full z-10 rounded-lg bg-white px-2 py-2 text-xs shadow-md'}>
|
||||
{columns.map((column, index) => (
|
||||
<div
|
||||
className={'flex cursor-pointer items-center justify-between rounded-lg px-2 py-2 hover:bg-fill-hover'}
|
||||
className={'flex cursor-pointer items-center justify-between rounded-lg px-2 py-2 hover:bg-fill-list-hover'}
|
||||
key={index}
|
||||
>
|
||||
<div className={'flex items-center gap-2 '}>
|
||||
|
@ -52,10 +52,10 @@ export const BoardGroup = ({
|
||||
<span className={'text-shade-4'}>({group.rows.length})</span>
|
||||
</div>
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<button className={'h-5 w-5 rounded hover:bg-fill-hover'}>
|
||||
<button className={'h-5 w-5 rounded hover:bg-fill-list-hover'}>
|
||||
<Details2Svg></Details2Svg>
|
||||
</button>
|
||||
<button className={'h-5 w-5 rounded hover:bg-fill-hover'}>
|
||||
<button className={'h-5 w-5 rounded hover:bg-fill-list-hover'}>
|
||||
<AddSvg></AddSvg>
|
||||
</button>
|
||||
</div>
|
||||
@ -86,7 +86,7 @@ export const BoardGroup = ({
|
||||
<div className={'p-2'}>
|
||||
<button
|
||||
onClick={onNewRowClick}
|
||||
className={'flex w-full items-center gap-2 rounded-lg px-2 py-2 hover:bg-fill-hover'}
|
||||
className={'flex w-full items-center gap-2 rounded-lg px-2 py-2 hover:bg-fill-list-hover'}
|
||||
>
|
||||
<span className={'h-5 w-5'}>
|
||||
<AddSvg></AddSvg>
|
||||
|
@ -15,7 +15,7 @@ export const BoardGroupFieldsPopup = ({ hidePopup }: { hidePopup: () => void })
|
||||
<div ref={ref} className={'absolute left-full top-full z-10 rounded-lg bg-white px-2 py-2 text-xs shadow-md'}>
|
||||
{columns.map((column, index) => (
|
||||
<div
|
||||
className={'flex cursor-pointer items-center justify-between rounded-lg px-2 py-2 hover:bg-fill-hover'}
|
||||
className={'flex cursor-pointer items-center justify-between rounded-lg px-2 py-2 hover:bg-fill-list-hover'}
|
||||
key={index}
|
||||
>
|
||||
<div className={'flex items-center gap-2 '}>
|
||||
|
@ -8,6 +8,7 @@ import { Keyboard } from '$app/constants/document/keyboard';
|
||||
import { selectOptionByUpDown } from '$app/utils/document/menu';
|
||||
import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks';
|
||||
import { BlockType } from '$app/interfaces/document';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
enum BlockMenuOption {
|
||||
Duplicate = 'Duplicate',
|
||||
@ -27,6 +28,7 @@ function BlockMenu({ id, onClose }: { id: string; onClose: () => void }) {
|
||||
const { node } = useSubscribeNode(id);
|
||||
const [subMenuOpened, setSubMenuOpened] = useState(false);
|
||||
const [hovered, setHovered] = useState<BlockMenuOption | null>(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (hovered !== BlockMenuOption.TurnInto) {
|
||||
@ -53,7 +55,7 @@ function BlockMenu({ id, onClose }: { id: string; onClose: () => void }) {
|
||||
operate: () => {
|
||||
return handleClick({ operate: handleDelete });
|
||||
},
|
||||
title: 'Delete',
|
||||
title: t('document.plugins.optionAction.delete'),
|
||||
icon: <Delete />,
|
||||
key: BlockMenuOption.Delete,
|
||||
},
|
||||
@ -61,7 +63,7 @@ function BlockMenu({ id, onClose }: { id: string; onClose: () => void }) {
|
||||
operate: () => {
|
||||
return handleClick({ operate: handleDuplicate });
|
||||
},
|
||||
title: 'Duplicate',
|
||||
title: t('document.plugins.optionAction.duplicate'),
|
||||
icon: <ContentCopy />,
|
||||
key: BlockMenuOption.Duplicate,
|
||||
},
|
||||
@ -69,9 +71,10 @@ function BlockMenu({ id, onClose }: { id: string; onClose: () => void }) {
|
||||
? null
|
||||
: {
|
||||
key: BlockMenuOption.TurnInto,
|
||||
title: t('document.plugins.optionAction.turnInto'),
|
||||
},
|
||||
].filter((item) => item !== null) as Option[],
|
||||
[excludeTurnIntoBlock, handleClick, handleDelete, handleDuplicate]
|
||||
[excludeTurnIntoBlock, handleClick, handleDelete, handleDuplicate, t]
|
||||
);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
@ -128,13 +131,19 @@ function BlockMenu({ id, onClose }: { id: string; onClose: () => void }) {
|
||||
}}
|
||||
>
|
||||
<div className={'p-2'}>
|
||||
<TextField autoFocus label='Search' placeholder='Search actions...' variant='standard' />
|
||||
<TextField
|
||||
autoFocus
|
||||
label={t('search.label')}
|
||||
placeholder={t('search.placeholder.actions')}
|
||||
variant='standard'
|
||||
/>
|
||||
</div>
|
||||
{options.map((option) => {
|
||||
if (option.key === BlockMenuOption.TurnInto) {
|
||||
return (
|
||||
<BlockMenuTurnInto
|
||||
key={option.key}
|
||||
lable={option.title}
|
||||
onHovered={() => {
|
||||
setHovered(BlockMenuOption.TurnInto);
|
||||
setSubMenuOpened(true);
|
||||
|
@ -9,12 +9,14 @@ function BlockMenuTurnInto({
|
||||
onHovered,
|
||||
isHovered,
|
||||
menuOpened,
|
||||
lable,
|
||||
}: {
|
||||
id: string;
|
||||
onClose: () => void;
|
||||
onHovered: (e: MouseEvent) => void;
|
||||
isHovered: boolean;
|
||||
menuOpened: boolean;
|
||||
lable?: string;
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const [anchorPosition, setAnchorPosition] = React.useState<{ top: number; left: number }>();
|
||||
@ -37,7 +39,7 @@ function BlockMenuTurnInto({
|
||||
<>
|
||||
<MenuItem
|
||||
ref={ref}
|
||||
title='Turn into'
|
||||
title={lable}
|
||||
isHovered={isHovered}
|
||||
icon={<Transform />}
|
||||
extra={<ArrowRight />}
|
||||
|
@ -11,10 +11,12 @@ import { addBlockBelowClickThunk } from '$app_reducers/document/async-actions/me
|
||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||
import { RANGE_NAME, RECT_RANGE_NAME } from '$app/constants/document/name';
|
||||
import { setRectSelectionThunk } from '$app_reducers/document/async-actions/rect_selection';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function BlockSideToolbar({ container }: { container: HTMLDivElement }) {
|
||||
const dispatch = useAppDispatch();
|
||||
const { docId, controller } = useSubscribeDocument();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { nodeId, style, ref } = useBlockSideToolbar({ container });
|
||||
const isDragging = useAppSelector(
|
||||
@ -42,7 +44,7 @@ export default function BlockSideToolbar({ container }: { container: HTMLDivElem
|
||||
>
|
||||
{/** Add Block below */}
|
||||
<ToolbarButton
|
||||
tooltip={'Add a new block below'}
|
||||
tooltip={t('tooltip.addBlockBelow')}
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (!nodeId || !controller) return;
|
||||
dispatch(
|
||||
@ -58,7 +60,7 @@ export default function BlockSideToolbar({ container }: { container: HTMLDivElem
|
||||
|
||||
{/** Open menu or drag */}
|
||||
<ToolbarButton
|
||||
tooltip={'Click to open Menu'}
|
||||
tooltip={t('tooltip.openMenu')}
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (!nodeId) return;
|
||||
dispatch(
|
||||
|
@ -27,7 +27,6 @@ import { useSubscribeDocument } from '$app/components/document/_shared/Subscribe
|
||||
import { slashCommandActions } from '$app_reducers/document/slice';
|
||||
import { Keyboard } from '$app/constants/document/keyboard';
|
||||
import { selectOptionByUpDown } from '$app/utils/document/menu';
|
||||
import { blockEditActions } from '$app_reducers/document/block_edit_slice';
|
||||
|
||||
function BlockSlashMenu({
|
||||
id,
|
||||
@ -60,7 +59,7 @@ function BlockSlashMenu({
|
||||
);
|
||||
onClose?.();
|
||||
},
|
||||
[controller, dispatch, docId, id, onClose]
|
||||
[controller, dispatch, id, onClose]
|
||||
);
|
||||
|
||||
const options: (SlashCommandOption & {
|
||||
@ -293,7 +292,7 @@ function BlockSlashMenu({
|
||||
<div ref={ref} className={'min-h-0 flex-1 overflow-y-auto overflow-x-hidden'}>
|
||||
{Object.entries(optionsByGroup).map(([group, options]) => (
|
||||
<div key={group}>
|
||||
<div className={'px-2 py-2 text-sm text-shade-3'}>{group}</div>
|
||||
<div className={'text-shade-3 px-2 py-2 text-sm'}>{group}</div>
|
||||
<div>
|
||||
{options.map((option) => {
|
||||
return (
|
||||
|
@ -1,8 +1,7 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import Popover from '@mui/material/Popover';
|
||||
import BlockSlashMenu from '$app/components/document/BlockSlash/BlockSlashMenu';
|
||||
import { useBlockSlash } from '$app/components/document/BlockSlash/index.hooks';
|
||||
import { Keyboard } from '$app/constants/document/keyboard';
|
||||
|
||||
function BlockSlash({ container }: { container: HTMLDivElement }) {
|
||||
const { blockId, open, onClose, anchorPosition, searchText, hoverOption } = useBlockSlash();
|
||||
|
@ -17,7 +17,7 @@ export default function CalloutBlock({
|
||||
const { openEmojiSelect, open, closeEmojiSelect, id, anchorEl, onEmojiSelect } = useCalloutBlock(node.id);
|
||||
|
||||
return (
|
||||
<div className={'my-1 flex rounded border border-solid border-line-border bg-fill-selector p-4'}>
|
||||
<div className={'my-1 flex rounded border border-solid border-line-divider bg-content-blue-50 p-4'}>
|
||||
<div className={'w-[1.5em]'} onMouseDown={(e) => e.stopPropagation()}>
|
||||
<div className={'flex h-[calc(1.5em_+_2px)] w-[24px] select-none items-center justify-start'}>
|
||||
<IconButton
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import Select, { SelectChangeEvent } from '@mui/material/Select';
|
||||
@ -6,15 +6,17 @@ import { updateNodeDataThunk } from '$app_reducers/document/async-actions';
|
||||
import { useAppDispatch } from '$app/stores/store';
|
||||
import { supportLanguage } from '$app/constants/document/code';
|
||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function SelectLanguage({ id, language }: { id: string; language: string }) {
|
||||
const dispatch = useAppDispatch();
|
||||
const { controller } = useSubscribeDocument();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const onLanguageSelect = useCallback(
|
||||
(event: SelectChangeEvent) => {
|
||||
if (!controller) return;
|
||||
const language = event.target.value;
|
||||
|
||||
dispatch(
|
||||
updateNodeDataThunk({
|
||||
id,
|
||||
@ -34,7 +36,8 @@ function SelectLanguage({ id, language }: { id: string; language: string }) {
|
||||
className={'h-[28px] w-[150px]'}
|
||||
value={language || 'javascript'}
|
||||
onChange={onLanguageSelect}
|
||||
label='Language'
|
||||
placeholder={t('document.codeBlock.language.placeholder')}
|
||||
label={t('document.codeBlock.language.label')}
|
||||
>
|
||||
{supportLanguage.map((item) => (
|
||||
<MenuItem key={item.id} value={item.id}>
|
||||
|
@ -22,7 +22,10 @@ export default function CodeBlock({
|
||||
const isDark = useAppSelector((state) => state.currentUser.userSetting.themeMode === ThemeMode.Dark);
|
||||
|
||||
return (
|
||||
<div {...props} className={`my-1 rounded border border-solid border-line-border bg-fill-selector p-6 ${className}`}>
|
||||
<div
|
||||
{...props}
|
||||
className={`my-1 rounded border border-solid border-line-divider bg-content-blue-50 p-6 ${className}`}
|
||||
>
|
||||
<div className={'mb-2 w-[100%]'}>
|
||||
<SelectLanguage id={id} language={language} />
|
||||
</div>
|
||||
|
@ -1,13 +1,16 @@
|
||||
import React from 'react';
|
||||
import { useDocumentTitle } from './DocumentTitle.hooks';
|
||||
import TextBlock from '../TextBlock';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function DocumentTitle({ id }: { id: string }) {
|
||||
const { node } = useDocumentTitle(id);
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!node) return null;
|
||||
return (
|
||||
<div data-block-id={node.id} className='doc-title relative mb-2 pt-[50px] text-4xl font-bold'>
|
||||
<TextBlock placeholder='Untitled' childIds={[]} node={node} />
|
||||
<TextBlock placeholder={t('document.title.placeholder')} childIds={[]} node={node} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import { useBlockPopover } from '$app/components/document/_shared/BlockPopover/B
|
||||
import { updateNodeDataThunk } from '$app_reducers/document/async-actions';
|
||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||
import { useAppDispatch } from '$app/stores/store';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function EquationBlock({ node }: { node: NestedBlock<BlockType.EquationBlock> }) {
|
||||
const formula = node.data.formula;
|
||||
@ -60,19 +61,21 @@ function EquationBlock({ node }: { node: NestedBlock<BlockType.EquationBlock> })
|
||||
});
|
||||
const displayFormula = open ? value : formula;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={anchorElRef}
|
||||
onClick={openPopover}
|
||||
className={'my-1 flex min-h-[59px] cursor-pointer flex-col overflow-hidden rounded hover:bg-fill-selector'}
|
||||
className={'my-1 flex min-h-[59px] cursor-pointer flex-col overflow-hidden rounded hover:bg-content-blue-50'}
|
||||
>
|
||||
{displayFormula ? (
|
||||
<KatexMath latex={displayFormula} />
|
||||
) : (
|
||||
<div className={'flex h-[100%] w-[100%] flex-1 items-center bg-fill-selector px-1 text-text-title'}>
|
||||
<div className={'flex h-[100%] w-[100%] flex-1 items-center bg-content-blue-50 px-1 text-text-caption'}>
|
||||
<Functions />
|
||||
<span>Add a TeX equation</span>
|
||||
<span>{t('document.plugins.mathEquation.addMathEquation')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -4,7 +4,7 @@ import { useAppDispatch } from '$app/stores/store';
|
||||
import { updateNodeDataThunk } from '$app_reducers/document/async-actions';
|
||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||
import UploadImage from '$app/components/document/_shared/UploadImage';
|
||||
import { isTauri } from '$app/utils/env';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
enum TAB_KEYS {
|
||||
UPLOAD = 'upload',
|
||||
@ -13,6 +13,7 @@ enum TAB_KEYS {
|
||||
|
||||
function EditImage({ id, url, onClose }: { id: string; url: string; onClose: () => void }) {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const { controller } = useSubscribeDocument();
|
||||
const [linkVal, setLinkVal] = useState<string>(url);
|
||||
const [tabKey, setTabKey] = useState<TAB_KEYS>(TAB_KEYS.UPLOAD);
|
||||
@ -41,31 +42,29 @@ function EditImage({ id, url, onClose }: { id: string; url: string; onClose: ()
|
||||
<div className={'w-[540px]'}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Tabs value={tabKey} onChange={handleChange}>
|
||||
{isTauri() && <Tab label={'Upload Image'} value={TAB_KEYS.UPLOAD} />}
|
||||
<Tab label={t('document.imageBlock.upload.label')} value={TAB_KEYS.UPLOAD} />
|
||||
|
||||
<Tab label='URL Image' value={TAB_KEYS.LINK} />
|
||||
<Tab label={t('document.imageBlock.url.label')} value={TAB_KEYS.LINK} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
{isTauri() && (
|
||||
<TabPanel value={tabKey} index={TAB_KEYS.UPLOAD}>
|
||||
<UploadImage onChange={handleConfirmUrl} />
|
||||
</TabPanel>
|
||||
)}
|
||||
<TabPanel value={tabKey} index={TAB_KEYS.UPLOAD}>
|
||||
<UploadImage onChange={handleConfirmUrl} />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel className={'flex flex-col p-3'} value={tabKey} index={TAB_KEYS.LINK}>
|
||||
<TextField
|
||||
value={linkVal}
|
||||
onChange={(e) => setLinkVal(e.target.value)}
|
||||
variant='outlined'
|
||||
label={'URL'}
|
||||
label={t('document.imageBlock.url.label')}
|
||||
autoFocus={true}
|
||||
style={{
|
||||
marginBottom: '10px',
|
||||
}}
|
||||
placeholder={'Please enter the URL of the image'}
|
||||
placeholder={t('document.imageBlock.url.placeholder')}
|
||||
/>
|
||||
<Button onClick={() => handleConfirmUrl(linkVal)} variant='contained'>
|
||||
Upload
|
||||
{t('button.upload')}
|
||||
</Button>
|
||||
</TabPanel>
|
||||
</div>
|
||||
|
@ -4,8 +4,9 @@ import { useSubscribeDocument } from '$app/components/document/_shared/Subscribe
|
||||
import { Align } from '$app/interfaces/document';
|
||||
import { FormatAlignCenter, FormatAlignLeft, FormatAlignRight } from '@mui/icons-material';
|
||||
import { updateNodeDataThunk } from '$app_reducers/document/async-actions';
|
||||
import MenuTooltip from '$app/components/document/TextActionMenu/menu/MenuTooltip';
|
||||
import ToolbarTooltip from '$app/components/document/_shared/ToolbarTooltip';
|
||||
import Popover from '@mui/material/Popover';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function ImageAlign({
|
||||
id,
|
||||
@ -21,6 +22,7 @@ function ImageAlign({
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLDivElement>();
|
||||
const popoverOpen = Boolean(anchorEl);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (popoverOpen) {
|
||||
@ -61,7 +63,7 @@ function ImageAlign({
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuTooltip title='Align'>
|
||||
<ToolbarTooltip title={t('document.plugins.optionAction.align')}>
|
||||
<div
|
||||
ref={ref}
|
||||
className='flex items-center justify-center p-1'
|
||||
@ -71,7 +73,7 @@ function ImageAlign({
|
||||
>
|
||||
{renderAlign(align)}
|
||||
</div>
|
||||
</MenuTooltip>
|
||||
</ToolbarTooltip>
|
||||
<Popover
|
||||
open={popoverOpen}
|
||||
anchorOrigin={{
|
||||
@ -87,7 +89,7 @@ function ImageAlign({
|
||||
onClose={() => setAnchorEl(undefined)}
|
||||
PaperProps={{
|
||||
style: {
|
||||
backgroundColor: 'var(--color-bg-body)',
|
||||
backgroundColor: 'var(--bg-body)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Alert, CircularProgress } from '@mui/material';
|
||||
import { ImageSvg } from '$app/components/_shared/svg/ImageSvg';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function ImagePlaceholder({
|
||||
error,
|
||||
@ -20,6 +21,7 @@ function ImagePlaceholder({
|
||||
openPopover: () => void;
|
||||
}) {
|
||||
const visible = loading || error || isEmpty;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -40,12 +42,12 @@ function ImagePlaceholder({
|
||||
{isEmpty && (
|
||||
<div
|
||||
onClick={openPopover}
|
||||
className={'flex h-[100%] w-[100%] flex-1 items-center rounded bg-fill-selector px-1 text-text-title'}
|
||||
className={'flex h-[100%] w-[100%] flex-1 items-center rounded bg-content-blue-50 px-1 text-text-caption'}
|
||||
>
|
||||
<i className={'mx-2 h-5 w-5'}>
|
||||
<ImageSvg />
|
||||
</i>
|
||||
<span>Add an image</span>
|
||||
<span>{t('document.imageBlock.placeholder')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -29,7 +29,7 @@ function ImageRender({
|
||||
} top-0 flex h-[100%] w-[15px] cursor-col-resize items-center justify-center`}
|
||||
>
|
||||
<div
|
||||
className={`h-[48px] max-h-[50%] w-2 rounded-[20px] border border-solid border-line-border bg-line-border ${
|
||||
className={`h-[48px] max-h-[50%] w-2 rounded-[20px] border border-solid border-line-divider bg-line-border ${
|
||||
toolbarOpen ? 'opacity-1' : 'opacity-0'
|
||||
} transition-opacity duration-300 `}
|
||||
/>
|
||||
|
@ -1,11 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Align } from '$app/interfaces/document';
|
||||
import ImageAlign from '$app/components/document/ImageBlock/ImageAlign';
|
||||
import MenuTooltip from '$app/components/document/TextActionMenu/menu/MenuTooltip';
|
||||
import ToolbarTooltip from '$app/components/document/_shared/ToolbarTooltip';
|
||||
import { DeleteOutline } from '@mui/icons-material';
|
||||
import { useAppDispatch } from '$app/stores/store';
|
||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||
import { deleteNodeThunk } from '$app_reducers/document/async-actions';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function ImageToolbar({ id, open, align }: { id: string; open: boolean; align: Align }) {
|
||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||
@ -13,6 +14,8 @@ function ImageToolbar({ id, open, align }: { id: string; open: boolean; align: A
|
||||
const dispatch = useAppDispatch();
|
||||
const { controller } = useSubscribeDocument();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@ -21,7 +24,7 @@ function ImageToolbar({ id, open, align }: { id: string; open: boolean; align: A
|
||||
} absolute right-2 top-2 z-[1px] flex h-[26px] max-w-[calc(100%-16px)] cursor-pointer items-center justify-center whitespace-nowrap rounded bg-bg-body text-sm text-text-title transition-opacity`}
|
||||
>
|
||||
<ImageAlign id={id} align={align} onOpen={() => setPopoverOpen(true)} onClose={() => setPopoverOpen(false)} />
|
||||
<MenuTooltip title={'Delete'}>
|
||||
<ToolbarTooltip title={t('button.delete')}>
|
||||
<div
|
||||
onClick={() => {
|
||||
dispatch(deleteNodeThunk({ id, controller }));
|
||||
@ -30,7 +33,7 @@ function ImageToolbar({ id, open, align }: { id: string; open: boolean; align: A
|
||||
>
|
||||
<DeleteOutline />
|
||||
</div>
|
||||
</MenuTooltip>
|
||||
</ToolbarTooltip>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -19,6 +19,7 @@ import CodeBlock from '$app/components/document/CodeBlock';
|
||||
import { NodeIdContext } from '$app/components/document/_shared/SubscribeNode.hooks';
|
||||
import EquationBlock from '$app/components/document/EquationBlock';
|
||||
import ImageBlock from '$app/components/document/ImageBlock';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<HTMLDivElement>) {
|
||||
const { node, childIds, isSelected, ref } = useNode(id);
|
||||
@ -82,7 +83,7 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<H
|
||||
{renderBlock()}
|
||||
<BlockOverlay id={id} />
|
||||
{isSelected ? (
|
||||
<div className='pointer-events-none absolute inset-0 z-[-1] my-[1px] rounded-[4px] bg-fill-hover' />
|
||||
<div className='pointer-events-none absolute inset-0 z-[-1] my-[1px] rounded-[4px] bg-content-blue-100' />
|
||||
) : null}
|
||||
</div>
|
||||
</NodeIdContext.Provider>
|
||||
@ -94,9 +95,11 @@ const NodeWithErrorBoundary = withErrorBoundary(NodeComponent, {
|
||||
});
|
||||
|
||||
const UnSupportedBlock = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Alert severity='info' className='mb-2'>
|
||||
<p>The current version does not support this Block.</p>
|
||||
<p>{t('unSupportBlock')}</p>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
@ -18,7 +18,7 @@ const TextActionComponent = ({ container }: { container: HTMLDivElement }) => {
|
||||
style={{
|
||||
opacity: 0,
|
||||
}}
|
||||
className='absolute mt-[-6px] inline-flex h-[32px] min-w-[100px] items-stretch overflow-hidden rounded-[8px] bg-bg-base leading-tight text-text-title shadow-lg transition-opacity duration-100'
|
||||
className='absolute mt-[-6px] inline-flex h-[32px] min-w-[100px] items-stretch overflow-hidden rounded-[8px] bg-fill-toolbar leading-tight text-content-on-fill shadow-md transition-opacity duration-100'
|
||||
onMouseDown={(e) => {
|
||||
// prevent toolbar from taking focus away from editor
|
||||
e.preventDefault();
|
||||
|
@ -1,7 +1,6 @@
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { TemporaryType, TextAction } from '$app/interfaces/document';
|
||||
import MenuTooltip from '$app/components/document/TextActionMenu/menu/MenuTooltip';
|
||||
import ToolbarTooltip from '$app/components/document/_shared/ToolbarTooltip';
|
||||
import { getFormatActiveThunk, toggleFormatThunk } from '$app_reducers/document/async-actions/format';
|
||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||
import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks';
|
||||
@ -18,18 +17,19 @@ import {
|
||||
StrikethroughSOutlined,
|
||||
} from '@mui/icons-material';
|
||||
import LinkIcon from '@mui/icons-material/AddLink';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const iconSize = { width: 18, height: 18 };
|
||||
|
||||
const FormatButton = ({ format, icon }: { format: TextAction; icon: string }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { docId, controller } = useSubscribeDocument();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const focusId = useAppSelector((state) => state[RANGE_NAME][docId]?.focus?.id || '');
|
||||
const { node: focusNode } = useSubscribeNode(focusId);
|
||||
|
||||
const [isActive, setIsActive] = React.useState(false);
|
||||
const color = useMemo(() => (isActive ? 'text-content-hover' : 'text-text-title'), [isActive]);
|
||||
const color = useMemo(() => (isActive ? 'text-content-on-fill-hover' : ''), [isActive]);
|
||||
|
||||
const isFormatActive = useCallback(async () => {
|
||||
if (!focusNode) return false;
|
||||
@ -82,15 +82,15 @@ const FormatButton = ({ format, icon }: { format: TextAction; icon: string }) =>
|
||||
|
||||
const formatTooltips: Record<string, string> = useMemo(
|
||||
() => ({
|
||||
[TextAction.Bold]: 'Bold',
|
||||
[TextAction.Italic]: 'Italic',
|
||||
[TextAction.Underline]: 'Underline',
|
||||
[TextAction.Strikethrough]: 'Strike through',
|
||||
[TextAction.Code]: 'Mark as Code',
|
||||
[TextAction.Link]: 'Add Link',
|
||||
[TextAction.Equation]: 'Create equation',
|
||||
[TextAction.Bold]: t('toolbar.bold'),
|
||||
[TextAction.Italic]: t('toolbar.italic'),
|
||||
[TextAction.Underline]: t('toolbar.underline'),
|
||||
[TextAction.Strikethrough]: t('toolbar.strike'),
|
||||
[TextAction.Code]: t('toolbar.inlineCode'),
|
||||
[TextAction.Link]: t('toolbar.addLink'),
|
||||
[TextAction.Equation]: t('document.plugins.mathEquation.addMathEquation'),
|
||||
}),
|
||||
[]
|
||||
[t]
|
||||
);
|
||||
|
||||
const formatClick = useCallback(
|
||||
@ -132,7 +132,7 @@ const FormatButton = ({ format, icon }: { format: TextAction; icon: string }) =>
|
||||
marginRight: '0.25rem',
|
||||
}}
|
||||
/>
|
||||
<div className={'underline'}>Link</div>
|
||||
<div className={'underline'}>{t('toolbar.link')}</div>
|
||||
</div>
|
||||
);
|
||||
case TextAction.Equation:
|
||||
@ -140,14 +140,14 @@ const FormatButton = ({ format, icon }: { format: TextAction; icon: string }) =>
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}, [icon]);
|
||||
}, [icon, t]);
|
||||
|
||||
return (
|
||||
<MenuTooltip title={formatTooltips[format]}>
|
||||
<ToolbarTooltip title={formatTooltips[format]}>
|
||||
<div className={`${color} cursor-pointer px-1 hover:text-fill-default`} onClick={() => formatClick(format)}>
|
||||
{formatIcon}
|
||||
</div>
|
||||
</MenuTooltip>
|
||||
</ToolbarTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,16 +0,0 @@
|
||||
import React from 'react';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
|
||||
function MenuTooltip({ title, children }: { children: JSX.Element; title?: string }) {
|
||||
return (
|
||||
<Tooltip
|
||||
slotProps={{ tooltip: { style: { background: '#E0F8FF', borderRadius: 8 } } }}
|
||||
title={title}
|
||||
placement='top-start'
|
||||
>
|
||||
<div>{children}</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export default MenuTooltip;
|
@ -1,9 +1,9 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import TurnIntoPopover from '$app/components/document/_shared/TurnInto';
|
||||
import Button from '@mui/material/Button';
|
||||
import ArrowDropDown from '@mui/icons-material/ArrowDropDown';
|
||||
import MenuTooltip from './MenuTooltip';
|
||||
import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ToolbarTooltip from '../../_shared/ToolbarTooltip';
|
||||
|
||||
function TurnIntoSelect({ id }: { id: string }) {
|
||||
const [anchorPosition, setAnchorPosition] = React.useState<{
|
||||
@ -26,15 +26,16 @@ function TurnIntoSelect({ id }: { id: string }) {
|
||||
}, []);
|
||||
|
||||
const open = Boolean(anchorPosition);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuTooltip title='Turn into'>
|
||||
<ToolbarTooltip title={t('document.plugins.optionAction.turnInto')}>
|
||||
<div onClick={handleClick} className='flex cursor-pointer items-center px-2 text-sm text-fill-default'>
|
||||
<span>{node.type}</span>
|
||||
<ArrowDropDown />
|
||||
</div>
|
||||
</MenuTooltip>
|
||||
</ToolbarTooltip>
|
||||
<TurnIntoPopover
|
||||
id={id}
|
||||
open={open}
|
||||
|
@ -31,7 +31,7 @@ function TextActionMenuList() {
|
||||
{groupItems.map(
|
||||
(group, i: number) =>
|
||||
group.length > 0 && (
|
||||
<div className={'flex border-r border-solid border-line-border px-1 last:border-r-0'} key={i}>
|
||||
<div className={'flex border-r border-solid border-line-on-toolbar px-1 last:border-r-0'} key={i}>
|
||||
{group.map((item) => (
|
||||
<div key={item} className={'flex items-center'}>
|
||||
{renderNode(item)}
|
||||
|
@ -5,6 +5,7 @@ import { useChange } from '$app/components/document/_shared/EditorHooks/useChang
|
||||
import NodeChildren from '$app/components/document/Node/NodeChildren';
|
||||
import { useKeyDown } from '$app/components/document/TextBlock/useKeyDown';
|
||||
import { useSelection } from '$app/components/document/_shared/EditorHooks/useSelection';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface Props {
|
||||
node: NestedBlock;
|
||||
@ -15,10 +16,17 @@ function TextBlock({ node, childIds, placeholder }: Props) {
|
||||
const { value, onChange } = useChange(node);
|
||||
const selectionProps = useSelection(node.id);
|
||||
const { onKeyDown } = useKeyDown(node.id);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Editor value={value} onChange={onChange} {...selectionProps} onKeyDown={onKeyDown} placeholder={placeholder} />
|
||||
<Editor
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
{...selectionProps}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder={placeholder || t('document.textBlock.placeholder')}
|
||||
/>
|
||||
<NodeChildren className='pl-[1.5em]' childIds={childIds} />
|
||||
</>
|
||||
);
|
||||
|
@ -17,15 +17,18 @@ export function useBlockPopover({
|
||||
onAfterOpen?: () => void;
|
||||
renderContent: ({ onClose }: { onClose: () => void }) => React.ReactNode;
|
||||
}) {
|
||||
const anchorElRef = useRef<HTMLDivElement>(null);
|
||||
const anchorElRef = useRef<HTMLDivElement | null>(null);
|
||||
const { docId } = useSubscribeDocument();
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLDivElement | null>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const [anchorPosition, setAnchorPosition] = useState<{
|
||||
top: number;
|
||||
left: number;
|
||||
}>();
|
||||
const open = Boolean(anchorPosition);
|
||||
const editing = useEditingState(id);
|
||||
const dispatch = useAppDispatch();
|
||||
const closePopover = useCallback(() => {
|
||||
setAnchorEl(null);
|
||||
setAnchorPosition(undefined);
|
||||
dispatch(
|
||||
blockEditActions.setBlockEditState({
|
||||
id: docId,
|
||||
@ -48,7 +51,14 @@ export function useBlockPopover({
|
||||
}, [dispatch, docId, id]);
|
||||
|
||||
const openPopover = useCallback(() => {
|
||||
setAnchorEl(anchorElRef.current);
|
||||
if (!anchorElRef.current) return;
|
||||
|
||||
const rect = anchorElRef.current.getBoundingClientRect();
|
||||
|
||||
setAnchorPosition({
|
||||
top: rect.top + rect.height,
|
||||
left: rect.left + rect.width / 2,
|
||||
});
|
||||
selectBlock();
|
||||
onAfterOpen?.();
|
||||
}, [onAfterOpen, selectBlock]);
|
||||
@ -68,21 +78,18 @@ export function useBlockPopover({
|
||||
vertical: 'top',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onClose={closePopover}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
anchorReference={'anchorPosition'}
|
||||
anchorPosition={anchorPosition}
|
||||
>
|
||||
{renderContent({
|
||||
onClose: closePopover,
|
||||
})}
|
||||
</Popover>
|
||||
);
|
||||
}, [anchorEl, closePopover, open, renderContent]);
|
||||
}, [anchorPosition, closePopover, open, renderContent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!anchorElRef.current) {
|
||||
|
@ -132,6 +132,7 @@ function InlineContainer({
|
||||
style={{
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
className={'inline-block-content'}
|
||||
>
|
||||
{renderNode()}
|
||||
</span>
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { Portal, Snackbar } from '@mui/material';
|
||||
import { TransitionProps } from '@mui/material/transitions';
|
||||
import { Alert, Portal, Snackbar } from '@mui/material';
|
||||
import Slide, { SlideProps } from '@mui/material/Slide';
|
||||
|
||||
function SlideTransition(props: SlideProps) {
|
||||
@ -11,6 +10,7 @@ interface MessageProps {
|
||||
message?: string;
|
||||
key?: string;
|
||||
duration?: number;
|
||||
type?: 'success' | 'error';
|
||||
}
|
||||
export function useMessage() {
|
||||
const [state, setState] = useState<MessageProps>();
|
||||
@ -23,6 +23,7 @@ export function useMessage() {
|
||||
|
||||
const contentHolder = useMemo(() => {
|
||||
const open = !!state;
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Snackbar
|
||||
@ -31,10 +32,19 @@ export function useMessage() {
|
||||
open={open}
|
||||
onClose={hide}
|
||||
TransitionProps={{ onExited: hide }}
|
||||
message={state?.message}
|
||||
key={state?.key}
|
||||
TransitionComponent={SlideTransition}
|
||||
/>
|
||||
>
|
||||
<>
|
||||
{state?.type ? (
|
||||
<Alert severity={state.type} sx={{ width: '100%' }}>
|
||||
{state.message}
|
||||
</Alert>
|
||||
) : (
|
||||
<span>{state?.message}</span>
|
||||
)}
|
||||
</>
|
||||
</Snackbar>
|
||||
</Portal>
|
||||
);
|
||||
}, [hide, state]);
|
||||
|
@ -37,10 +37,10 @@ const TextLeaf = (props: TextLeafProps) => {
|
||||
};
|
||||
let newChildren = children;
|
||||
|
||||
if (leaf.code) {
|
||||
if (leaf.code && !leaf.temporary) {
|
||||
newChildren = (
|
||||
<span
|
||||
className={`bg-fill-selector text-text-title`}
|
||||
className={`bg-content-blue-50 text-text-title`}
|
||||
style={{
|
||||
fontSize: '85%',
|
||||
lineHeight: 'normal',
|
||||
@ -97,9 +97,9 @@ const TextLeaf = (props: TextLeafProps) => {
|
||||
isCodeBlock && 'token',
|
||||
leaf.prism_token && leaf.prism_token,
|
||||
leaf.strikethrough && 'line-through',
|
||||
leaf.selection_high_lighted && 'bg-fill-selector',
|
||||
leaf.link_selection_lighted && 'text-text-link-selector bg-fill-selector',
|
||||
leaf.code && 'inline-code',
|
||||
leaf.selection_high_lighted && 'bg-content-blue-100',
|
||||
leaf.link_selection_lighted && 'text-text-link-selector bg-content-blue-100',
|
||||
leaf.code && !leaf.temporary && 'inline-code',
|
||||
leaf.bold && 'font-bold',
|
||||
leaf.italic && 'italic',
|
||||
leaf.underline && 'underline',
|
||||
@ -114,7 +114,11 @@ const TextLeaf = (props: TextLeafProps) => {
|
||||
}
|
||||
|
||||
if (leaf.temporary) {
|
||||
newChildren = <TemporaryInput leaf={leaf}>{newChildren}</TemporaryInput>;
|
||||
newChildren = (
|
||||
<TemporaryInput getSelection={getSelection} leaf={leaf}>
|
||||
{newChildren}
|
||||
</TemporaryInput>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import { CheckOutlined, FunctionsOutlined } from '@mui/icons-material';
|
||||
import { Divider, IconButton, InputAdornment } from '@mui/material';
|
||||
import { IconButton, InputAdornment } from '@mui/material';
|
||||
|
||||
function EquationEditContent({
|
||||
value,
|
||||
|
@ -4,7 +4,7 @@ import KatexMath from '$app/components/document/_shared/KatexMath';
|
||||
|
||||
function TemporaryEquation({ latex }: { latex: string }) {
|
||||
return (
|
||||
<span className={'rounded bg-fill-selector px-1 py-0.5'} contentEditable={false}>
|
||||
<span className={'rounded bg-content-blue-50 px-1 py-0.5'} contentEditable={false}>
|
||||
{latex ? (
|
||||
<KatexMath latex={latex} isInline />
|
||||
) : (
|
||||
|
@ -1,33 +1,36 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { TemporaryType } from '$app/interfaces/document';
|
||||
import { RangeStaticNoId, TemporaryType } from '$app/interfaces/document';
|
||||
import TemporaryEquation from '$app/components/document/_shared/TemporaryInput/TemporaryEquation';
|
||||
import { useSubscribeTemporary } from '$app/components/document/_shared/SubscribeTemporary.hooks';
|
||||
import { isOverlappingPrefix } from '$app/utils/document/temporary';
|
||||
import { PopoverPosition } from '@mui/material';
|
||||
import { useAppDispatch } from '$app/stores/store';
|
||||
import { temporaryActions } from '$app_reducers/document/temporary_slice';
|
||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||
|
||||
function TemporaryInput({ leaf, children }: { leaf: { text: string }; children: React.ReactNode }) {
|
||||
function TemporaryInput({
|
||||
leaf,
|
||||
children,
|
||||
getSelection,
|
||||
}: {
|
||||
leaf: { text: string };
|
||||
children: React.ReactNode;
|
||||
getSelection: (node: Element) => RangeStaticNoId | null;
|
||||
}) {
|
||||
const temporaryState = useSubscribeTemporary();
|
||||
const id = temporaryState?.id;
|
||||
const dispatch = useAppDispatch();
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
const { docId } = useSubscribeDocument();
|
||||
const match = useMemo(() => {
|
||||
if (!ref.current) return false;
|
||||
if (!leaf.text) return false;
|
||||
if (!temporaryState) return false;
|
||||
const { selectedText, type } = temporaryState;
|
||||
const { selectedText } = temporaryState;
|
||||
const selection = getSelection(ref.current);
|
||||
|
||||
switch (type) {
|
||||
case TemporaryType.Equation:
|
||||
// when the leaf is split, the placeholder is not the same as the leaf text,
|
||||
// so we can only check for overlapping prefix and hidden other leafs
|
||||
return leaf.text === selectedText || isOverlappingPrefix(leaf.text, selectedText);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}, [temporaryState, leaf.text]);
|
||||
if (!selection) return false;
|
||||
return leaf.text === selectedText || selection.index <= temporaryState.selection.index;
|
||||
}, [leaf.text, temporaryState, getSelection]);
|
||||
|
||||
const renderPlaceholder = useCallback(() => {
|
||||
if (!temporaryState) return null;
|
||||
@ -69,7 +72,7 @@ function TemporaryInput({ leaf, children }: { leaf: { text: string }; children:
|
||||
return (
|
||||
<span ref={ref}>
|
||||
{match ? renderPlaceholder() : null}
|
||||
<span className={'absolute opacity-0'}>{children}</span>
|
||||
<span className={`absolute opacity-0 ${match ? 'w-0' : ''}`}>{children}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import LanguageIcon from '@mui/icons-material/Language';
|
||||
import CopyIcon from '@mui/icons-material/CopyAll';
|
||||
import { copyText } from '$app/utils/document/copy_paste';
|
||||
import { useMessage } from '$app/components/document/_shared/Message';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const iconSize = {
|
||||
width: '1rem',
|
||||
@ -28,6 +29,7 @@ function EditLinkToolbar({
|
||||
editing: boolean;
|
||||
onEdit: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { show, contentHolder } = useMessage();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -70,9 +72,9 @@ function EditLinkToolbar({
|
||||
onClick={async () => {
|
||||
try {
|
||||
await copyText(href);
|
||||
show({ message: 'Copied!', duration: 6000 });
|
||||
show({ message: t('message.copy.success'), duration: 6000 });
|
||||
} catch {
|
||||
show({ message: 'Copy failed!', duration: 6000 });
|
||||
show({ message: t('message.copy.fail'), duration: 6000 });
|
||||
}
|
||||
}}
|
||||
className={'mr-2 cursor-pointer'}
|
||||
@ -80,7 +82,7 @@ function EditLinkToolbar({
|
||||
<CopyIcon sx={iconSize} />
|
||||
</div>
|
||||
<div onClick={onEdit} className={'cursor-pointer'}>
|
||||
Edit
|
||||
{t('button.edit')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -8,11 +8,12 @@ import { formatLinkThunk } from '$app_reducers/document/async-actions/link';
|
||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||
import { useSubscribeLinkPopover } from '$app/components/document/_shared/SubscribeLinkPopover.hooks';
|
||||
import Button from '@mui/material/Button';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function LinkEditPopover() {
|
||||
const dispatch = useAppDispatch();
|
||||
const { docId, controller } = useSubscribeDocument();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const popoverState = useSubscribeLinkPopover();
|
||||
const { anchorPosition, id, selection, title = '', href = '', open = false } = popoverState;
|
||||
|
||||
@ -101,7 +102,7 @@ function LinkEditPopover() {
|
||||
>
|
||||
<div className='flex flex-col p-3'>
|
||||
<EditLink
|
||||
text={'URL'}
|
||||
text={t('document.inlineLink.url.label')}
|
||||
value={href}
|
||||
onChange={(link) => {
|
||||
onChange({
|
||||
@ -111,7 +112,7 @@ function LinkEditPopover() {
|
||||
}}
|
||||
/>
|
||||
<EditLink
|
||||
text={'Link title'}
|
||||
text={t('document.inlineLink.title.label')}
|
||||
value={title}
|
||||
onChange={(text) =>
|
||||
onChange({
|
||||
@ -123,7 +124,7 @@ function LinkEditPopover() {
|
||||
<div className={'flex items-center justify-end'}>
|
||||
<Button onClick={onDone}>
|
||||
<Done />
|
||||
Done
|
||||
{t('button.done')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
|
||||
function ToolbarTooltip({ title, children }: { children: JSX.Element; title?: string }) {
|
||||
return (
|
||||
<Tooltip
|
||||
slotProps={{ tooltip: { style: { background: 'var(--bg-tips)', borderRadius: 8 } } }}
|
||||
title={title}
|
||||
placement='top-start'
|
||||
>
|
||||
<div>{children}</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export default ToolbarTooltip;
|
@ -1,30 +1,53 @@
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { ImageSvg } from '$app/components/_shared/svg/ImageSvg';
|
||||
import { CircularProgress } from '@mui/material';
|
||||
import { writeImage } from '$app/utils/document/image';
|
||||
import { isTauri } from '$app/utils/env';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMessage } from '$app/components/document/_shared/Message';
|
||||
|
||||
export interface UploadImageProps {
|
||||
onChange: (filePath: string) => void;
|
||||
}
|
||||
|
||||
function UploadImage({ onChange }: UploadImageProps) {
|
||||
const { t } = useTranslation();
|
||||
const message = useMessage();
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string>('');
|
||||
const beforeUpload = useCallback((file: File) => {
|
||||
// check file size and type
|
||||
const sizeMatched = file.size / 1024 / 1024 < 5; // 5MB
|
||||
const typeMatched = /image\/(png|jpg|jpeg|gif)/.test(file.type); // png, jpg, jpeg, gif
|
||||
const beforeUpload = useCallback(
|
||||
(file: File) => {
|
||||
// check file size and type
|
||||
const sizeMatched = file.size / 1024 / 1024 < 5; // 5MB
|
||||
const typeMatched = /image\/(png|jpg|jpeg|gif)/.test(file.type); // png, jpg, jpeg, gif
|
||||
|
||||
return sizeMatched && typeMatched;
|
||||
}, []);
|
||||
if (!sizeMatched) {
|
||||
setError(t('document.imageBlock.error.invalidImageSize'));
|
||||
}
|
||||
|
||||
if (!typeMatched) {
|
||||
setError(t('document.imageBlock.error.invalidImageFormat'));
|
||||
}
|
||||
|
||||
return sizeMatched && typeMatched;
|
||||
},
|
||||
[t]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!error) return;
|
||||
message.show({
|
||||
message: error,
|
||||
duration: 3000,
|
||||
type: 'error',
|
||||
});
|
||||
}, [error]);
|
||||
|
||||
const handleUpload = useCallback(
|
||||
async (file: File) => {
|
||||
if (!file) return;
|
||||
if (!beforeUpload(file)) {
|
||||
setError('Image should be less than 5MB and in png, jpg, jpeg, gif format');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -38,10 +61,10 @@ function UploadImage({ onChange }: UploadImageProps) {
|
||||
onChange(filePath);
|
||||
} catch {
|
||||
setLoading(false);
|
||||
setError('Upload failed');
|
||||
setError(t('document.imageBlock.error.invalidImage'));
|
||||
}
|
||||
},
|
||||
[beforeUpload, onChange]
|
||||
[beforeUpload, onChange, t]
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
@ -88,7 +111,7 @@ function UploadImage({ onChange }: UploadImageProps) {
|
||||
<input onChange={handleChange} ref={inputRef} type='file' className={'hidden'} accept={'image/*'} />
|
||||
<div
|
||||
className={
|
||||
'flex flex-col items-center justify-center rounded-md border border-dashed border-content-hover py-10 text-content-hover'
|
||||
'flex flex-col items-center justify-center rounded-md border border-dashed border-content-blue-300 bg-content-blue-50 py-10 text-content-blue-300'
|
||||
}
|
||||
style={{
|
||||
borderColor: errorColor,
|
||||
@ -101,7 +124,7 @@ function UploadImage({ onChange }: UploadImageProps) {
|
||||
<div className={'h-8 w-8'}>
|
||||
<ImageSvg />
|
||||
</div>
|
||||
<div className={'my-2 p-2'}>{isTauri() ? 'Click space to chose image' : 'Chose image or drag to space'}</div>
|
||||
<div className={'my-2 p-2'}>{t('document.imageBlock.upload.placeholder')}</div>
|
||||
</div>
|
||||
|
||||
{loading ? <CircularProgress /> : null}
|
||||
@ -112,8 +135,9 @@ function UploadImage({ onChange }: UploadImageProps) {
|
||||
}}
|
||||
className={`mt-5 text-sm text-text-caption`}
|
||||
>
|
||||
The maximum file size is 5MB. Supported formats: JPG, PNG, GIF, SVG.
|
||||
{t('document.imageBlock.support')}
|
||||
</div>
|
||||
{message.contentHolder}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import AddSvg from '../../_shared/svg/AddSvg';
|
||||
|
||||
export const GridAddView = () => {
|
||||
return (
|
||||
<button className='flex cursor-pointer items-center rounded-lg p-2 text-sm hover:bg-fill-hover'>
|
||||
<button className='flex cursor-pointer items-center rounded-lg p-2 text-sm hover:bg-fill-list-hover'>
|
||||
<i className='mr-2 h-5 w-5'>
|
||||
<AddSvg />
|
||||
</i>
|
||||
|
@ -18,15 +18,15 @@ export const GridTableHeader = ({ controller }: { controller: DatabaseController
|
||||
return <GridTableHeaderItem field={field} controller={controller} key={i} />;
|
||||
})}
|
||||
|
||||
<th className='m-0 w-40 border border-r-0 border-line-border p-0'>
|
||||
<th className='m-0 w-40 border border-r-0 border-line-divider p-0'>
|
||||
<div
|
||||
className='flex cursor-pointer items-center px-4 py-2 text-text-caption hover:bg-fill-hover hover:text-text-title'
|
||||
className='flex cursor-pointer items-center px-4 py-2 text-text-caption hover:bg-fill-list-hover hover:text-text-title'
|
||||
onClick={onAddField}
|
||||
>
|
||||
<i className='mr-2 h-5 w-5'>
|
||||
<AddSvg />
|
||||
</i>
|
||||
<span>{t('grid.newCol')}</span>
|
||||
<span>{t('grid.field.newProperty')}</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
|
@ -61,9 +61,9 @@ export const GridTableHeaderItem = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<th key={field.fieldId} className='m-0 border border-l-0 border-line-border p-0'>
|
||||
<th key={field.fieldId} className='m-0 border border-l-0 border-line-divider p-0'>
|
||||
<div
|
||||
className={'flex w-full cursor-pointer items-center px-4 py-2 hover:bg-fill-hover'}
|
||||
className={'flex w-full cursor-pointer items-center px-4 py-2 hover:bg-fill-list-hover'}
|
||||
ref={ref}
|
||||
onClick={() => {
|
||||
if (!ref.current) return;
|
||||
|
@ -21,7 +21,7 @@ export const GridTableRow = ({
|
||||
<tr className='group'>
|
||||
{cells.map((cell, cellIndex) => {
|
||||
return (
|
||||
<td className='m-0 border border-l-0 border-line-border p-0 ' key={cellIndex}>
|
||||
<td className='m-0 border border-l-0 border-line-divider p-0 ' key={cellIndex}>
|
||||
<div className='flex w-full items-center justify-end'>
|
||||
<GridCell
|
||||
cellIdentifier={cell.cellIdentifier}
|
||||
@ -32,7 +32,7 @@ export const GridTableRow = ({
|
||||
{cellIndex === 0 && (
|
||||
<div
|
||||
onClick={() => onOpenRow(row)}
|
||||
className='mr-1 hidden h-8 w-8 cursor-pointer rounded p-1.5 text-text-caption hover:bg-fill-hover group-hover:block '
|
||||
className='mr-1 hidden h-8 w-8 cursor-pointer rounded p-1.5 text-text-caption hover:bg-fill-list-hover group-hover:block '
|
||||
>
|
||||
<FullView />
|
||||
</div>
|
||||
|
@ -5,7 +5,7 @@ export const FooterPanel = () => {
|
||||
© 2023 AppFlowy. <a href={'https://github.com/AppFlowy-IO/AppFlowy'}>GitHub</a>
|
||||
</div>
|
||||
<div>
|
||||
<button className={'h-8 w-8 rounded bg-fill-selector text-text-title hover:bg-fill-hover'}>?</button>
|
||||
<button className={'h-8 w-8 rounded bg-content-blue-50 text-text-title hover:bg-content-blue-100'}>?</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -38,10 +38,16 @@ export const Breadcrumbs = ({ menuHidden, onShowMenuClick }: { menuHidden: boole
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button className={'h-6 w-6 rounded p-1 text-text-title hover:bg-fill-hover'} onClick={() => history.back()}>
|
||||
<button
|
||||
className={'h-6 w-6 rounded p-1 text-text-title hover:bg-fill-list-hover'}
|
||||
onClick={() => history.back()}
|
||||
>
|
||||
<ArrowLeftSvg />
|
||||
</button>
|
||||
<button className={'h-6 w-6 rounded p-1 text-text-title hover:bg-fill-hover'} onClick={() => history.forward()}>
|
||||
<button
|
||||
className={'h-6 w-6 rounded p-1 text-text-title hover:bg-fill-list-hover'}
|
||||
onClick={() => history.forward()}
|
||||
>
|
||||
<ArrowRightSvg />
|
||||
</button>
|
||||
</div>
|
||||
|
@ -3,7 +3,7 @@ import { PageOptions } from './PageOptions';
|
||||
|
||||
export const HeaderPanel = ({ menuHidden, onShowMenuClick }: { menuHidden: boolean; onShowMenuClick: () => void }) => {
|
||||
return (
|
||||
<div className={'flex h-[60px] items-center justify-between border-b border-line-border px-8'}>
|
||||
<div className={'flex h-[60px] items-center justify-between border-b border-line-divider px-8'}>
|
||||
<Breadcrumbs menuHidden={menuHidden} onShowMenuClick={onShowMenuClick}></Breadcrumbs>
|
||||
<PageOptions></PageOptions>
|
||||
</div>
|
||||
|
@ -1,16 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { LanguageSelectPopup } from '$app/components/_shared/LanguageSelectPopup';
|
||||
import { LanguageOutlined } from '@mui/icons-material';
|
||||
|
||||
export const LanguageButton = () => {
|
||||
const [showPopup, setShowPopup] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => setShowPopup(!showPopup)} className={'h-8 w-8 rounded text-text-title hover:bg-fill-hover'}>
|
||||
<LanguageOutlined />
|
||||
</button>
|
||||
{showPopup && <LanguageSelectPopup onClose={() => setShowPopup(false)}></LanguageSelectPopup>}
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,45 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { LogoutSvg } from '$app/components/_shared/svg/LogoutSvg';
|
||||
import { useAuth } from '$app/components/auth/auth.hooks';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function MoreMenu({ onClose }: { onClose: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const { logout } = useAuth();
|
||||
const onSignOutClick = useCallback(async () => {
|
||||
await logout();
|
||||
onClose();
|
||||
}, [onClose, logout]);
|
||||
|
||||
const items = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
title: t('button.signOut'),
|
||||
icon: (
|
||||
<i className={'block h-5 w-5 flex-shrink-0'}>
|
||||
<LogoutSvg></LogoutSvg>
|
||||
</i>
|
||||
),
|
||||
onClick: onSignOutClick,
|
||||
},
|
||||
];
|
||||
}, [onSignOutClick, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{items.map((item, index) => {
|
||||
return (
|
||||
<MenuItem key={index} onClick={item.onClick}>
|
||||
<div className={'flex items-center gap-2'}>
|
||||
{item.icon}
|
||||
<span className={'flex-shrink-0'}>{item.title}</span>
|
||||
</div>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default MoreMenu;
|
@ -1,23 +0,0 @@
|
||||
import { IPopupItem, PopupSelect } from '../../_shared/PopupSelect';
|
||||
import { LogoutSvg } from '../../_shared/svg/LogoutSvg';
|
||||
|
||||
export const OptionsPopup = ({ onSignOutClick, onClose }: { onSignOutClick: () => void; onClose: () => void }) => {
|
||||
const items: IPopupItem[] = [
|
||||
{
|
||||
title: 'Sign out',
|
||||
icon: (
|
||||
<i className={'block h-5 w-5 flex-shrink-0'}>
|
||||
<LogoutSvg></LogoutSvg>
|
||||
</i>
|
||||
),
|
||||
onClick: onSignOutClick,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<PopupSelect
|
||||
className={'absolute top-[50px] right-[30px] z-10 whitespace-nowrap'}
|
||||
items={items}
|
||||
onOutsideClick={onClose}
|
||||
></PopupSelect>
|
||||
);
|
||||
};
|
@ -1,27 +1,20 @@
|
||||
import { useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useAuth } from '../../auth/auth.hooks';
|
||||
|
||||
export const usePageOptions = () => {
|
||||
const [showOptionsPopup, setShowOptionsPopup] = useState(false);
|
||||
const { logout } = useAuth();
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLDivElement | HTMLButtonElement>();
|
||||
|
||||
const onOptionsClick = () => {
|
||||
setShowOptionsPopup(true);
|
||||
};
|
||||
const onOptionsClick = useCallback((el: HTMLDivElement | HTMLButtonElement) => {
|
||||
setAnchorEl(el);
|
||||
}, []);
|
||||
|
||||
const onClose = () => {
|
||||
setShowOptionsPopup(false);
|
||||
};
|
||||
|
||||
const onSignOutClick = async () => {
|
||||
await logout();
|
||||
onClose();
|
||||
setAnchorEl(undefined);
|
||||
};
|
||||
|
||||
return {
|
||||
showOptionsPopup,
|
||||
anchorEl,
|
||||
onOptionsClick,
|
||||
onClose,
|
||||
onSignOutClick,
|
||||
};
|
||||
};
|
||||
|
@ -1,30 +1,73 @@
|
||||
import { Button } from '../../_shared/Button';
|
||||
import { Details2Svg } from '../../_shared/svg/Details2Svg';
|
||||
import { usePageOptions } from './PageOptions.hooks';
|
||||
import { OptionsPopup } from './OptionsPopup';
|
||||
import { LanguageButton } from '$app/components/layout/HeaderPanel/LanguageButton';
|
||||
import { Button, IconButton, List } from '@mui/material';
|
||||
import Popover from '@mui/material/Popover';
|
||||
import { useCallback, useState } from 'react';
|
||||
import MoreMenu from '$app/components/layout/HeaderPanel/MoreMenu';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
enum PageOptionsEnum {
|
||||
Share = 'Share',
|
||||
More = 'More',
|
||||
}
|
||||
export const PageOptions = () => {
|
||||
const { showOptionsPopup, onOptionsClick, onClose, onSignOutClick } = usePageOptions();
|
||||
const { t } = useTranslation();
|
||||
const { anchorEl, onOptionsClick, onClose } = usePageOptions();
|
||||
const open = Boolean(anchorEl);
|
||||
const [option, setOption] = useState<PageOptionsEnum>();
|
||||
const renderMenu = useCallback(() => {
|
||||
switch (option) {
|
||||
case PageOptionsEnum.Share:
|
||||
return <div>Share</div>;
|
||||
default:
|
||||
return <MoreMenu onClose={onClose} />;
|
||||
}
|
||||
}, [onClose, option]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'relative flex items-center gap-4'}>
|
||||
<Button size={'small'} onClick={() => console.log('share click')}>
|
||||
Share
|
||||
<Button
|
||||
variant={'contained'}
|
||||
onClick={(e) => {
|
||||
const el = e.currentTarget;
|
||||
|
||||
setOption(PageOptionsEnum.Share);
|
||||
onOptionsClick(el);
|
||||
}}
|
||||
>
|
||||
{t('shareAction.buttonText')}
|
||||
</Button>
|
||||
|
||||
<LanguageButton></LanguageButton>
|
||||
|
||||
<button
|
||||
<IconButton
|
||||
id='option-button'
|
||||
className={'relative h-8 w-8 rounded text-text-title hover:bg-fill-hover'}
|
||||
onClick={onOptionsClick}
|
||||
size={'small'}
|
||||
className={'h-8 w-8 rounded text-text-title hover:bg-fill-list-hover'}
|
||||
onClick={(e) => {
|
||||
const el = e.currentTarget;
|
||||
|
||||
setOption(PageOptionsEnum.More);
|
||||
onOptionsClick(el);
|
||||
}}
|
||||
>
|
||||
<Details2Svg></Details2Svg>
|
||||
</button>
|
||||
</IconButton>
|
||||
</div>
|
||||
{showOptionsPopup && <OptionsPopup onSignOutClick={onSignOutClick} onClose={onClose}></OptionsPopup>}
|
||||
<Popover
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={onClose}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
>
|
||||
<List>{renderMenu()}</List>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,84 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { EditSvg } from '$app/components/_shared/svg/EditSvg';
|
||||
import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
|
||||
import { CopySvg } from '$app/components/_shared/svg/CopySvg';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import RenameDialog from '$app/components/layout/NavigationPanel/RenameDialog';
|
||||
import { IPage } from '$app_reducers/pages/slice';
|
||||
|
||||
function MoreMenu({
|
||||
selectedPage,
|
||||
onRename,
|
||||
onDeleteClick,
|
||||
onDuplicateClick,
|
||||
}: {
|
||||
selectedPage: IPage;
|
||||
onRename: (name: string) => Promise<void>;
|
||||
onDeleteClick: () => void;
|
||||
onDuplicateClick: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [renameDialogOpen, setRenameDialogOpen] = useState(false);
|
||||
|
||||
const items = useMemo(
|
||||
() => [
|
||||
{
|
||||
icon: (
|
||||
<i className={'h-[16px] w-[16px] text-text-title'}>
|
||||
<EditSvg></EditSvg>
|
||||
</i>
|
||||
),
|
||||
onClick: () => {
|
||||
setRenameDialogOpen(true);
|
||||
},
|
||||
title: t('disclosureAction.rename'),
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<i className={'h-[16px] w-[16px] text-text-title'}>
|
||||
<TrashSvg></TrashSvg>
|
||||
</i>
|
||||
),
|
||||
onClick: onDeleteClick,
|
||||
title: t('disclosureAction.delete'),
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<i className={'h-[16px] w-[16px] text-text-title'}>
|
||||
<CopySvg></CopySvg>
|
||||
</i>
|
||||
),
|
||||
onClick: onDuplicateClick,
|
||||
title: t('disclosureAction.duplicate'),
|
||||
},
|
||||
],
|
||||
[onDeleteClick, onDuplicateClick, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{items.map((item, index) => {
|
||||
return (
|
||||
<MenuItem key={index} onClick={item.onClick}>
|
||||
<div className={'flex items-center gap-2'}>
|
||||
{item.icon}
|
||||
<span className={'flex-shrink-0'}>{item.title}</span>
|
||||
</div>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
<RenameDialog
|
||||
defaultValue={selectedPage.title}
|
||||
open={renameDialogOpen}
|
||||
onClose={() => setRenameDialogOpen(false)}
|
||||
onOk={async (val: string) => {
|
||||
await onRename(val);
|
||||
setRenameDialogOpen(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default MoreMenu;
|
@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||
import { IPage, pagesActions } from '$app_reducers/pages/slice';
|
||||
import { ViewLayoutPB } from '@/services/backend';
|
||||
@ -10,23 +10,26 @@ import { INITIAL_FOLDER_HEIGHT, PAGE_ITEM_HEIGHT } from '../../_shared/constants
|
||||
import { ViewBackendService } from '$app/stores/effects/folder/view/view_bd_svc';
|
||||
import { ViewObserver } from '$app/stores/effects/folder/view/view_observer';
|
||||
|
||||
export enum NavItemOptions {
|
||||
More = 'More',
|
||||
NewPage = 'NewPage',
|
||||
}
|
||||
export const useNavItem = (page: IPage) => {
|
||||
const appDispatch = useAppDispatch();
|
||||
const workspace = useAppSelector((state) => state.workspace);
|
||||
const currentLocation = useLocation();
|
||||
const [activePageId, setActivePageId] = useState<string>('');
|
||||
const pages = useAppSelector((state) => state.pages);
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement>();
|
||||
const menuOpen = Boolean(anchorEl);
|
||||
const [menuOption, setMenuOption] = useState<NavItemOptions>();
|
||||
const [selectedPage, setSelectedPage] = useState<IPage>();
|
||||
const onClickMenuBtn = useCallback((page: IPage, option: NavItemOptions) => {
|
||||
setSelectedPage(page);
|
||||
setMenuOption(option);
|
||||
}, []);
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Actions
|
||||
const [showPageOptions, setShowPageOptions] = useState(false);
|
||||
const [showNewPageOptions, setShowNewPageOptions] = useState(false);
|
||||
const [showRenamePopup, setShowRenamePopup] = useState(false);
|
||||
|
||||
// UI configurations
|
||||
const [folderHeight, setFolderHeight] = useState(`${INITIAL_FOLDER_HEIGHT}px`);
|
||||
|
||||
// backend
|
||||
const service = new ViewBackendService(page.id);
|
||||
const observer = new ViewObserver(page.id);
|
||||
@ -68,14 +71,6 @@ export const useNavItem = (page: IPage) => {
|
||||
setActivePageId(pageId);
|
||||
}, [currentLocation]);
|
||||
|
||||
useEffect(() => {
|
||||
if (page.showPagesInside) {
|
||||
setFolderHeight(`${PAGE_ITEM_HEIGHT + getChildCount(page) * PAGE_ITEM_HEIGHT}px`);
|
||||
} else {
|
||||
setFolderHeight(`${PAGE_ITEM_HEIGHT}px`);
|
||||
}
|
||||
}, [page, pages]);
|
||||
|
||||
// recursively get all unfolded child pages
|
||||
const getChildCount: (startPage: IPage) => number = (startPage: IPage) => {
|
||||
let count = 0;
|
||||
@ -95,42 +90,21 @@ export const useNavItem = (page: IPage) => {
|
||||
appDispatch(pagesActions.toggleShowPages({ id: page.id }));
|
||||
};
|
||||
|
||||
const onPageOptionsClick = () => {
|
||||
setShowPageOptions((prevState) => !prevState);
|
||||
};
|
||||
|
||||
const startPageRename = () => {
|
||||
setShowRenamePopup(true);
|
||||
closePopup();
|
||||
};
|
||||
|
||||
const onNewPageClick = () => {
|
||||
setShowNewPageOptions(!showNewPageOptions);
|
||||
};
|
||||
|
||||
const changePageTitle = async (newTitle: string) => {
|
||||
await service.update({ name: newTitle });
|
||||
appDispatch(pagesActions.renamePage({ id: page.id, newTitle }));
|
||||
};
|
||||
|
||||
const closeRenamePopup = () => {
|
||||
setShowRenamePopup(false);
|
||||
setAnchorEl(undefined);
|
||||
};
|
||||
|
||||
const deletePage = async () => {
|
||||
closePopup();
|
||||
await service.delete();
|
||||
appDispatch(pagesActions.deletePage({ id: page.id }));
|
||||
setAnchorEl(undefined);
|
||||
};
|
||||
|
||||
const duplicatePage = async () => {
|
||||
closePopup();
|
||||
await service.duplicate();
|
||||
};
|
||||
|
||||
const closePopup = () => {
|
||||
setShowPageOptions(false);
|
||||
setShowNewPageOptions(false);
|
||||
setAnchorEl(undefined);
|
||||
};
|
||||
|
||||
const onPageClick = (eventPage: IPage) => {
|
||||
@ -151,7 +125,6 @@ export const useNavItem = (page: IPage) => {
|
||||
};
|
||||
|
||||
const onAddNewPage = async (pageType: ViewLayoutPB) => {
|
||||
closePopup();
|
||||
if (!workspace?.id) return;
|
||||
|
||||
let newPageName = '';
|
||||
@ -199,24 +172,15 @@ export const useNavItem = (page: IPage) => {
|
||||
showPagesInside: false,
|
||||
})
|
||||
);
|
||||
|
||||
setAnchorEl(undefined);
|
||||
navigate(`/page/${pageTypeRoute}/${newView.id}`);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
onUnfoldClick,
|
||||
onNewPageClick,
|
||||
onPageOptionsClick,
|
||||
startPageRename,
|
||||
|
||||
changePageTitle,
|
||||
closeRenamePopup,
|
||||
closePopup,
|
||||
|
||||
showNewPageOptions,
|
||||
showPageOptions,
|
||||
showRenamePopup,
|
||||
|
||||
deletePage,
|
||||
duplicatePage,
|
||||
@ -225,7 +189,12 @@ export const useNavItem = (page: IPage) => {
|
||||
|
||||
onAddNewPage,
|
||||
|
||||
folderHeight,
|
||||
activePageId,
|
||||
menuOpen,
|
||||
anchorEl,
|
||||
setAnchorEl,
|
||||
menuOption,
|
||||
selectedPage,
|
||||
onClickMenuBtn,
|
||||
};
|
||||
};
|
||||
|
@ -1,123 +1,124 @@
|
||||
import { Details2Svg } from '../../_shared/svg/Details2Svg';
|
||||
import AddSvg from '../../_shared/svg/AddSvg';
|
||||
import { NavItemOptionsPopup } from './NavItemOptionsPopup';
|
||||
import { NewPagePopup } from './NewPagePopup';
|
||||
import { IPage } from '$app_reducers/pages/slice';
|
||||
import { Button } from '../../_shared/Button';
|
||||
import { RenamePopup } from './RenamePopup';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { DropDownShowSvg } from '../../_shared/svg/DropDownShowSvg';
|
||||
import { ANIMATION_DURATION, PAGE_ITEM_HEIGHT } from '../../_shared/constants';
|
||||
import { useNavItem } from '$app/components/layout/NavigationPanel/NavItem.hooks';
|
||||
import { ANIMATION_DURATION } from '../../_shared/constants';
|
||||
import { NavItemOptions, useNavItem } from '$app/components/layout/NavigationPanel/NavItem.hooks';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import { ViewLayoutPB } from '@/services/backend';
|
||||
import Popover from '@mui/material/Popover';
|
||||
import { IconButton, List } from '@mui/material';
|
||||
import MoreMenu from '$app/components/layout/NavigationPanel/MoreMenu';
|
||||
import NewPageMenu from '$app/components/layout/NavigationPanel/NewPageMenu';
|
||||
|
||||
export const NavItem = ({ page }: { page: IPage }) => {
|
||||
const pages = useAppSelector((state) => state.pages);
|
||||
const {
|
||||
onUnfoldClick,
|
||||
onNewPageClick,
|
||||
onPageOptionsClick,
|
||||
startPageRename,
|
||||
|
||||
changePageTitle,
|
||||
closeRenamePopup,
|
||||
closePopup,
|
||||
|
||||
showNewPageOptions,
|
||||
showPageOptions,
|
||||
showRenamePopup,
|
||||
|
||||
deletePage,
|
||||
duplicatePage,
|
||||
|
||||
onAddNewPage,
|
||||
|
||||
folderHeight,
|
||||
activePageId,
|
||||
|
||||
onPageClick,
|
||||
onClickMenuBtn,
|
||||
menuOpen,
|
||||
menuOption,
|
||||
setAnchorEl,
|
||||
selectedPage,
|
||||
anchorEl,
|
||||
} = useNavItem(page);
|
||||
|
||||
const [popupY, setPopupY] = useState(0);
|
||||
|
||||
const el = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (el.current) {
|
||||
const { top } = el.current.getBoundingClientRect();
|
||||
|
||||
setPopupY(top);
|
||||
}
|
||||
}, [showPageOptions, showNewPageOptions, showRenamePopup]);
|
||||
|
||||
return (
|
||||
<div ref={el}>
|
||||
<div
|
||||
className={`overflow-hidden transition-all`}
|
||||
style={{ height: folderHeight, transitionDuration: `${ANIMATION_DURATION}ms` }}
|
||||
>
|
||||
<div style={{ height: PAGE_ITEM_HEIGHT }} className={`cursor-pointer px-1 py-1`}>
|
||||
<div
|
||||
className={`flex items-center justify-between rounded-lg px-2 py-1 hover:bg-fill-active ${
|
||||
activePageId === page.id ? 'bg-fill-active' : ''
|
||||
}`}
|
||||
>
|
||||
<div className={'flex h-full min-w-0 flex-1 items-center'}>
|
||||
<button
|
||||
onClick={() => onUnfoldClick()}
|
||||
className={`mr-2 h-5 w-5 transition-transform duration-200 ${page.showPagesInside && 'rotate-180'}`}
|
||||
>
|
||||
<DropDownShowSvg></DropDownShowSvg>
|
||||
</button>
|
||||
<div onClick={() => onPageClick(page)} className={'mr-1 flex h-full min-w-0 items-center text-left'}>
|
||||
<span className={'w-[100%] overflow-hidden overflow-ellipsis whitespace-nowrap'}>{page.title}</span>
|
||||
<>
|
||||
<div ref={el}>
|
||||
<div className={`transition-all`} style={{ transitionDuration: `${ANIMATION_DURATION}ms` }}>
|
||||
<div className={`cursor-pointer px-1 py-1`}>
|
||||
<div
|
||||
className={`flex items-center justify-between rounded-lg px-2 py-1 hover:bg-fill-list-hover ${
|
||||
activePageId === page.id ? 'bg-fill-list-hover' : ''
|
||||
}`}
|
||||
>
|
||||
<div className={'flex h-full min-w-0 flex-1 items-center'}>
|
||||
<button
|
||||
onClick={() => onUnfoldClick()}
|
||||
className={`mr-2 h-5 w-5 transition-transform duration-200 ${
|
||||
page.showPagesInside ? 'rotate-180' : ''
|
||||
}`}
|
||||
>
|
||||
<DropDownShowSvg></DropDownShowSvg>
|
||||
</button>
|
||||
<div
|
||||
onClick={() => onPageClick(page)}
|
||||
className={'mr-1 flex h-full min-w-0 flex-1 items-center text-left'}
|
||||
>
|
||||
<span className={'w-[100%] overflow-hidden overflow-ellipsis whitespace-nowrap'}>{page.title}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'flex items-center'}>
|
||||
<IconButton
|
||||
className={'h-6 w-6'}
|
||||
size={'small'}
|
||||
onClick={(e) => {
|
||||
setAnchorEl(e.currentTarget);
|
||||
onClickMenuBtn(page, NavItemOptions.More);
|
||||
}}
|
||||
>
|
||||
<Details2Svg></Details2Svg>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
className={'h-6 w-6'}
|
||||
size={'small'}
|
||||
onClick={(e) => {
|
||||
setAnchorEl(e.currentTarget);
|
||||
onClickMenuBtn(page, NavItemOptions.NewPage);
|
||||
}}
|
||||
>
|
||||
<AddSvg></AddSvg>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'flex items-center'}>
|
||||
<Button size={'box-small-transparent'} onClick={() => onPageOptionsClick()}>
|
||||
<Details2Svg></Details2Svg>
|
||||
</Button>
|
||||
<Button size={'box-small-transparent'} onClick={() => onNewPageClick()}>
|
||||
<AddSvg></AddSvg>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${page.showPagesInside ? '' : 'hidden'} pl-4`}>
|
||||
{useMemo(() => pages.filter((insidePage) => insidePage.parentPageId === page.id), [pages, page]).map(
|
||||
(insidePage, insideIndex) => (
|
||||
<NavItem key={insideIndex} page={insidePage}></NavItem>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={'pl-4'}>
|
||||
{useMemo(() => pages.filter((insidePage) => insidePage.parentPageId === page.id), [pages, page]).map(
|
||||
(insidePage, insideIndex) => (
|
||||
<NavItem key={insideIndex} page={insidePage}></NavItem>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{showPageOptions && (
|
||||
<NavItemOptionsPopup
|
||||
onRenameClick={() => startPageRename()}
|
||||
onDeleteClick={() => deletePage()}
|
||||
onDuplicateClick={() => duplicatePage()}
|
||||
onClose={() => closePopup()}
|
||||
top={popupY - 124 + 58}
|
||||
></NavItemOptionsPopup>
|
||||
)}
|
||||
{showNewPageOptions && (
|
||||
<NewPagePopup
|
||||
onDocumentClick={() => onAddNewPage(ViewLayoutPB.Document)}
|
||||
onBoardClick={() => onAddNewPage(ViewLayoutPB.Board)}
|
||||
onGridClick={() => onAddNewPage(ViewLayoutPB.Grid)}
|
||||
onClose={() => closePopup()}
|
||||
top={popupY - 124 + 58}
|
||||
></NewPagePopup>
|
||||
)}
|
||||
{showRenamePopup && (
|
||||
<RenamePopup
|
||||
value={page.title}
|
||||
onChange={(newTitle) => changePageTitle(newTitle)}
|
||||
onClose={closeRenamePopup}
|
||||
top={popupY - 124 + 40}
|
||||
></RenamePopup>
|
||||
)}
|
||||
</div>
|
||||
<Popover
|
||||
open={menuOpen}
|
||||
anchorEl={anchorEl}
|
||||
onClose={() => setAnchorEl(undefined)}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
|
||||
>
|
||||
<List>
|
||||
{menuOption === NavItemOptions.More && selectedPage && (
|
||||
<MoreMenu
|
||||
selectedPage={selectedPage}
|
||||
onRename={changePageTitle}
|
||||
onDeleteClick={() => deletePage()}
|
||||
onDuplicateClick={() => duplicatePage()}
|
||||
/>
|
||||
)}
|
||||
{menuOption === NavItemOptions.NewPage && (
|
||||
<NewPageMenu
|
||||
onDocumentClick={() => onAddNewPage(ViewLayoutPB.Document)}
|
||||
onBoardClick={() => onAddNewPage(ViewLayoutPB.Board)}
|
||||
onGridClick={() => onAddNewPage(ViewLayoutPB.Grid)}
|
||||
/>
|
||||
)}
|
||||
</List>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,57 +0,0 @@
|
||||
import { IPopupItem, PopupSelect } from '../../_shared/PopupSelect';
|
||||
import { EditSvg } from '../../_shared/svg/EditSvg';
|
||||
import { TrashSvg } from '../../_shared/svg/TrashSvg';
|
||||
import { CopySvg } from '../../_shared/svg/CopySvg';
|
||||
|
||||
export const NavItemOptionsPopup = ({
|
||||
onRenameClick,
|
||||
onDeleteClick,
|
||||
onDuplicateClick,
|
||||
onClose,
|
||||
top,
|
||||
}: {
|
||||
onRenameClick: () => void;
|
||||
onDeleteClick: () => void;
|
||||
onDuplicateClick: () => void;
|
||||
onClose?: () => void;
|
||||
top: number;
|
||||
}) => {
|
||||
const items: IPopupItem[] = [
|
||||
{
|
||||
icon: (
|
||||
<i className={'h-[16px] w-[16px] text-text-title'}>
|
||||
<EditSvg></EditSvg>
|
||||
</i>
|
||||
),
|
||||
onClick: onRenameClick,
|
||||
title: 'Rename',
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<i className={'h-[16px] w-[16px] text-text-title'}>
|
||||
<TrashSvg></TrashSvg>
|
||||
</i>
|
||||
),
|
||||
onClick: onDeleteClick,
|
||||
title: 'Delete',
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<i className={'h-[16px] w-[16px] text-text-title'}>
|
||||
<CopySvg></CopySvg>
|
||||
</i>
|
||||
),
|
||||
onClick: onDuplicateClick,
|
||||
title: 'Duplicate',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PopupSelect
|
||||
onOutsideClick={() => onClose && onClose()}
|
||||
items={items}
|
||||
className={`absolute right-0`}
|
||||
style={{ top: `${top}px` }}
|
||||
></PopupSelect>
|
||||
);
|
||||
};
|
@ -54,24 +54,16 @@ export const NavigationPanel = ({
|
||||
left: `${menuHidden ? -width : 0}px`,
|
||||
}}
|
||||
>
|
||||
<div className={'flex flex-col'}>
|
||||
<AppLogo iconToShow={'hide'} onHideMenuClick={onHideMenuClick}></AppLogo>
|
||||
<WorkspaceUser></WorkspaceUser>
|
||||
<div className={'relative flex flex-1 flex-col'}>
|
||||
<div
|
||||
className={'flex flex-col overflow-auto px-2'}
|
||||
style={{
|
||||
maxHeight: 'calc(100vh - 350px)',
|
||||
}}
|
||||
ref={el}
|
||||
>
|
||||
<WorkspaceApps pages={pages.filter((p) => p.parentPageId === workspace.id)} />
|
||||
</div>
|
||||
<AppLogo iconToShow={'hide'} onHideMenuClick={onHideMenuClick}></AppLogo>
|
||||
<WorkspaceUser></WorkspaceUser>
|
||||
<div className={'relative flex flex-1 flex-col'}>
|
||||
<div className={'flex h-[100%] flex-col overflow-auto px-2'} ref={el}>
|
||||
<WorkspaceApps pages={pages.filter((p) => p.parentPageId === workspace.id)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={'flex max-h-[215px] flex-col'}>
|
||||
<div className={'border-b border-line-border px-2 pb-4'}>
|
||||
<div className={'flex max-h-[240px] flex-col'}>
|
||||
<div className={'border-b border-line-divider px-2 pb-4'}>
|
||||
{/*<PluginsButton></PluginsButton>*/}
|
||||
|
||||
{/*<DesignSpec></DesignSpec>*/}
|
||||
@ -105,7 +97,7 @@ export const TestBackendButton = () => {
|
||||
return (
|
||||
<button
|
||||
onClick={() => navigate('/page/api-test')}
|
||||
className={'flex w-full items-center rounded-lg px-4 py-2 hover:bg-fill-active'}
|
||||
className={'hover:bg-fill-active flex w-full items-center rounded-lg px-4 py-2'}
|
||||
>
|
||||
API Test
|
||||
</button>
|
||||
@ -118,7 +110,7 @@ export const DesignSpec = () => {
|
||||
return (
|
||||
<button
|
||||
onClick={() => navigate('page/colors')}
|
||||
className={'flex w-full items-center rounded-lg px-4 py-2 hover:bg-fill-active'}
|
||||
className={'hover:bg-fill-active flex w-full items-center rounded-lg px-4 py-2'}
|
||||
>
|
||||
Color Palette
|
||||
</button>
|
||||
@ -131,7 +123,7 @@ export const AllIcons = () => {
|
||||
return (
|
||||
<button
|
||||
onClick={() => navigate('page/all-icons')}
|
||||
className={'flex w-full items-center rounded-lg px-4 py-2 hover:bg-fill-active'}
|
||||
className={'hover:bg-fill-active flex w-full items-center rounded-lg px-4 py-2'}
|
||||
>
|
||||
All Icons
|
||||
</button>
|
||||
|
@ -0,0 +1,67 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { DocumentSvg } from '$app/components/_shared/svg/DocumentSvg';
|
||||
import { BoardSvg } from '$app/components/_shared/svg/BoardSvg';
|
||||
import { GridSvg } from '$app/components/_shared/svg/GridSvg';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function NewPageMenu({
|
||||
onDocumentClick,
|
||||
onGridClick,
|
||||
onBoardClick,
|
||||
}: {
|
||||
onDocumentClick: () => void;
|
||||
onGridClick: () => void;
|
||||
onBoardClick: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const items = useMemo(
|
||||
() => [
|
||||
{
|
||||
icon: (
|
||||
<i className={'h-[16px] w-[16px] text-text-title'}>
|
||||
<DocumentSvg></DocumentSvg>
|
||||
</i>
|
||||
),
|
||||
onClick: onDocumentClick,
|
||||
title: t('document.menuName'),
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<i className={'h-[16px] w-[16px] text-text-title'}>
|
||||
<BoardSvg></BoardSvg>
|
||||
</i>
|
||||
),
|
||||
onClick: onBoardClick,
|
||||
title: t('board.menuName'),
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<i className={'h-[16px] w-[16px] text-text-title'}>
|
||||
<GridSvg></GridSvg>
|
||||
</i>
|
||||
),
|
||||
onClick: onGridClick,
|
||||
title: t('grid.menuName'),
|
||||
},
|
||||
],
|
||||
[onBoardClick, onDocumentClick, onGridClick, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{items.map((item, index) => {
|
||||
return (
|
||||
<MenuItem key={index} onClick={item.onClick}>
|
||||
<div className={'flex items-center gap-2'}>
|
||||
{item.icon}
|
||||
<span className={'flex-shrink-0'}>{item.title}</span>
|
||||
</div>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewPageMenu;
|
@ -1,57 +0,0 @@
|
||||
import { IPopupItem, PopupSelect } from '../../_shared/PopupSelect';
|
||||
import { DocumentSvg } from '../../_shared/svg/DocumentSvg';
|
||||
import { BoardSvg } from '../../_shared/svg/BoardSvg';
|
||||
import { GridSvg } from '../../_shared/svg/GridSvg';
|
||||
|
||||
export const NewPagePopup = ({
|
||||
onDocumentClick,
|
||||
onGridClick,
|
||||
onBoardClick,
|
||||
onClose,
|
||||
top,
|
||||
}: {
|
||||
onDocumentClick: () => void;
|
||||
onGridClick: () => void;
|
||||
onBoardClick: () => void;
|
||||
onClose?: () => void;
|
||||
top: number;
|
||||
}) => {
|
||||
const items: IPopupItem[] = [
|
||||
{
|
||||
icon: (
|
||||
<i className={'h-[16px] w-[16px] text-text-title'}>
|
||||
<DocumentSvg></DocumentSvg>
|
||||
</i>
|
||||
),
|
||||
onClick: onDocumentClick,
|
||||
title: 'Document',
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<i className={'h-[16px] w-[16px] text-text-title'}>
|
||||
<BoardSvg></BoardSvg>
|
||||
</i>
|
||||
),
|
||||
onClick: onBoardClick,
|
||||
title: 'Board',
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<i className={'h-[16px] w-[16px] text-text-title'}>
|
||||
<GridSvg></GridSvg>
|
||||
</i>
|
||||
),
|
||||
onClick: onGridClick,
|
||||
title: 'Grid',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PopupSelect
|
||||
onOutsideClick={() => onClose && onClose()}
|
||||
items={items}
|
||||
className={'absolute right-0'}
|
||||
style={{ top: `${top}px` }}
|
||||
></PopupSelect>
|
||||
);
|
||||
};
|
@ -10,10 +10,10 @@ export const NewViewButton = ({ scrollDown }: { scrollDown: () => void }) => {
|
||||
void onNewRootView();
|
||||
scrollDown();
|
||||
}}
|
||||
className={'flex h-[50px] w-full items-center px-6 hover:bg-fill-active'}
|
||||
className={'flex h-[50px] w-full items-center px-6 hover:bg-fill-list-active'}
|
||||
>
|
||||
<div className={'mr-2 rounded-full bg-fill-default'}>
|
||||
<div className={'h-[24px] w-[24px] text-content-onfill'}>
|
||||
<div className={'h-[24px] w-[24px] text-content-on-fill'}>
|
||||
<AddSvg></AddSvg>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -0,0 +1,52 @@
|
||||
import React, { useState } from 'react';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import { Button, DialogActions } from '@mui/material';
|
||||
|
||||
function RenameDialog({
|
||||
defaultValue,
|
||||
open,
|
||||
onClose,
|
||||
onOk,
|
||||
}: {
|
||||
defaultValue: string;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onOk: (val: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
|
||||
return (
|
||||
<Dialog keepMounted={false} onMouseDown={(e) => e.stopPropagation()} open={open} onClose={onClose}>
|
||||
<DialogTitle>{t('menuAppHeader.renameDialog')}</DialogTitle>
|
||||
<DialogContent className={'flex w-[540px]'}>
|
||||
<TextField
|
||||
autoFocus
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value);
|
||||
}}
|
||||
margin='dense'
|
||||
fullWidth
|
||||
variant='standard'
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>{t('button.Cancel')}</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onOk(value);
|
||||
}}
|
||||
>
|
||||
{t('button.OK')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default RenameDialog;
|
@ -1,47 +0,0 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import useOutsideClick from '../../_shared/useOutsideClick';
|
||||
|
||||
export const RenamePopup = ({
|
||||
value,
|
||||
onChange,
|
||||
onClose,
|
||||
className = '',
|
||||
top,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (newTitle: string) => void;
|
||||
onClose: () => void;
|
||||
className?: string;
|
||||
top?: number;
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
useOutsideClick(ref, () => onClose && onClose());
|
||||
|
||||
useEffect(() => {
|
||||
if (!inputRef || !inputRef.current) return;
|
||||
|
||||
const { current: el } = inputRef;
|
||||
|
||||
el.focus();
|
||||
el.selectionStart = 0;
|
||||
el.selectionEnd = el.value.length;
|
||||
}, [inputRef]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={
|
||||
'absolute left-[50px] top-[40px] z-10 flex w-[300px] rounded bg-white py-1 px-1.5 shadow-md ' + className
|
||||
}
|
||||
style={{ top: `${top}px` }}
|
||||
>
|
||||
<input
|
||||
ref={inputRef}
|
||||
className={'border-shades-3 flex-1 rounded border bg-main-selector p-1'}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -3,7 +3,7 @@ import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
|
||||
|
||||
export const TrashButton = () => {
|
||||
return (
|
||||
<button className={'flex w-full items-center rounded-lg px-4 py-2 text-text-title hover:bg-fill-active'}>
|
||||
<button className={'flex w-full items-center rounded-lg px-4 py-2 text-text-title hover:bg-fill-list-active'}>
|
||||
<span className={'h-[23px] w-[23px]'}>
|
||||
<TrashSvg />
|
||||
</span>
|
||||
|
@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import Select from '@mui/material/Select';
|
||||
import { Theme, ThemeMode, UserSetting } from '$app/interfaces';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function AppearanceSetting({
|
||||
theme = Theme.Default,
|
||||
@ -12,6 +13,8 @@ function AppearanceSetting({
|
||||
themeMode?: ThemeMode;
|
||||
onChange: (setting: UserSetting) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
const html = document.documentElement;
|
||||
|
||||
@ -23,14 +26,14 @@ function AppearanceSetting({
|
||||
() => [
|
||||
{
|
||||
value: ThemeMode.Light,
|
||||
content: 'Light',
|
||||
content: t('settings.appearance.themeMode.light'),
|
||||
},
|
||||
{
|
||||
value: ThemeMode.Dark,
|
||||
content: 'Dark',
|
||||
content: t('settings.appearance.themeMode.dark'),
|
||||
},
|
||||
],
|
||||
[]
|
||||
[t]
|
||||
);
|
||||
|
||||
const themeOptions = useMemo(
|
||||
@ -96,7 +99,7 @@ function AppearanceSetting({
|
||||
{renderSelect([
|
||||
{
|
||||
options: themeModeOptions,
|
||||
label: 'Theme Mode',
|
||||
label: t('settings.appearance.themeMode.label'),
|
||||
value: themeMode,
|
||||
onChange: (newValue) => {
|
||||
onChange({
|
||||
@ -106,7 +109,7 @@ function AppearanceSetting({
|
||||
},
|
||||
{
|
||||
options: themeOptions,
|
||||
label: 'Theme',
|
||||
label: t('settings.appearance.theme'),
|
||||
value: theme,
|
||||
onChange: (newValue) => {
|
||||
onChange({
|
||||
|
@ -1,7 +1,74 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Select from '@mui/material/Select';
|
||||
import { UserSetting } from '$app/interfaces';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
|
||||
function LanguageSetting() {
|
||||
return <div></div>;
|
||||
const languages = [
|
||||
{
|
||||
key: 'ar-SA',
|
||||
title: 'العربية',
|
||||
},
|
||||
{ key: 'ca-ES', title: 'Català' },
|
||||
{ key: 'de-DE', title: 'Deutsch' },
|
||||
{ key: 'en', title: 'English' },
|
||||
{ key: 'es-VE', title: 'Español (Venezuela)' },
|
||||
{ key: 'eu-ES', title: 'Español' },
|
||||
{ key: 'fr-FR', title: 'Français' },
|
||||
{ key: 'hu-HU', title: 'Magyar' },
|
||||
{ key: 'id-ID', title: 'Bahasa Indonesia' },
|
||||
{ key: 'it-IT', title: 'Italiano' },
|
||||
{ key: 'ja-JP', title: '日本語' },
|
||||
{ key: 'ko-KR', title: '한국어' },
|
||||
{ key: 'pl-PL', title: 'Polski' },
|
||||
{ key: 'pt-BR', title: 'Português' },
|
||||
{ key: 'pt-PT', title: 'Português' },
|
||||
{ key: 'ru-RU', title: 'Русский' },
|
||||
{ key: 'sv', title: 'Svenska' },
|
||||
{ key: 'tr-TR', title: 'Türkçe' },
|
||||
{ key: 'zh-CN', title: '简体中文' },
|
||||
{ key: 'zh-TW', title: '繁體中文' },
|
||||
];
|
||||
|
||||
function LanguageSetting({
|
||||
language = 'en',
|
||||
onChange,
|
||||
}: {
|
||||
language?: string;
|
||||
onChange: (setting: UserSetting) => void;
|
||||
}) {
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col'}>
|
||||
<div className={'mb-2 flex items-center justify-between text-sm'}>
|
||||
<div className={'flex-1 text-text-title'}>{t('settings.menu.language')}</div>
|
||||
<div className={'flex items-center'}>
|
||||
<Select
|
||||
sx={{
|
||||
fontSize: '0.85rem',
|
||||
}}
|
||||
variant={'standard'}
|
||||
value={language}
|
||||
onChange={(e) => {
|
||||
const language = e.target.value;
|
||||
|
||||
onChange({
|
||||
language,
|
||||
});
|
||||
i18n.changeLanguage(language);
|
||||
}}
|
||||
>
|
||||
{languages.map((option) => (
|
||||
<MenuItem key={option.key} value={option.key}>
|
||||
{option.title}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LanguageSetting;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import LanguageIcon from '@mui/icons-material/Language';
|
||||
import PaletteOutlined from '@mui/icons-material/PaletteOutlined';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export enum MenuItem {
|
||||
Appearance = 'Appearance',
|
||||
@ -8,23 +9,25 @@ export enum MenuItem {
|
||||
}
|
||||
|
||||
function UserSettingMenu({ selected, onSelect }: { onSelect: (selected: MenuItem) => void; selected: MenuItem }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const options = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
label: 'Appearance',
|
||||
label: t('settings.menu.appearance'),
|
||||
value: MenuItem.Appearance,
|
||||
icon: <PaletteOutlined />,
|
||||
},
|
||||
{
|
||||
label: 'Language',
|
||||
label: t('settings.menu.language'),
|
||||
value: MenuItem.Language,
|
||||
icon: <LanguageIcon />,
|
||||
},
|
||||
];
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
return (
|
||||
<div className={'h-[300px] w-[200px] border-r border-solid border-r-line-border pr-2 text-sm'}>
|
||||
<div className={'h-[300px] w-[200px] border-r border-solid border-r-line-border pr-4 text-sm'}>
|
||||
{options.map((option) => {
|
||||
return (
|
||||
<div
|
||||
@ -33,7 +36,7 @@ function UserSettingMenu({ selected, onSelect }: { onSelect: (selected: MenuItem
|
||||
onSelect(option.value);
|
||||
}}
|
||||
className={`my-1 flex h-10 w-full cursor-pointer items-center justify-start rounded-md px-4 py-2 text-text-title ${
|
||||
selected === option.value ? 'bg-fill-hover' : 'hover:bg-fill-hover'
|
||||
selected === option.value ? 'bg-fill-list-hover' : 'hover:text-content-blue-300'
|
||||
}`}
|
||||
>
|
||||
<div className={'mr-2'}>{option.icon}</div>
|
||||
|
@ -14,7 +14,7 @@ function UserSettingPanel({
|
||||
userSettingState?: UserSetting;
|
||||
onChange: (setting: Partial<UserSetting>) => void;
|
||||
}) {
|
||||
const { theme, themeMode } = userSettingState;
|
||||
const { theme, themeMode, language } = userSettingState;
|
||||
|
||||
const options = useMemo(() => {
|
||||
return [
|
||||
@ -24,14 +24,14 @@ function UserSettingPanel({
|
||||
},
|
||||
{
|
||||
value: MenuItem.Language,
|
||||
icon: <LanguageSetting />,
|
||||
content: <LanguageSetting onChange={onChange} language={language} />,
|
||||
},
|
||||
];
|
||||
}, [onChange, theme, themeMode]);
|
||||
}, [language, onChange, theme, themeMode]);
|
||||
|
||||
const option = options.find((option) => option.value === selected);
|
||||
|
||||
return <div className={'flex-1 pl-2'}>{option?.content}</div>;
|
||||
return <div className={'flex-1 pl-4'}>{option?.content}</div>;
|
||||
}
|
||||
|
||||
export default UserSettingPanel;
|
||||
|
@ -10,6 +10,7 @@ import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||
import { currentUserActions } from '$app_reducers/current-user/slice';
|
||||
import { useUserSettingControllerContext } from '$app/components/_shared/app-hooks/useUserSettingControllerContext';
|
||||
import { ThemeModePB } from '@/services/backend';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const SlideTransition = React.forwardRef((props: SlideProps, ref) => {
|
||||
return <Slide {...props} direction='up' ref={ref} />;
|
||||
@ -19,7 +20,7 @@ function UserSettings({ open, onClose }: { open: boolean; onClose: () => void })
|
||||
const userSettingState = useAppSelector((state) => state.currentUser.userSetting);
|
||||
const dispatch = useAppDispatch();
|
||||
const userSettingController = useUserSettingControllerContext();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [selected, setSelected] = useState<MenuItem>(MenuItem.Appearance);
|
||||
const handleChange = useCallback(
|
||||
(setting: Partial<UserSetting>) => {
|
||||
@ -27,9 +28,15 @@ function UserSettings({ open, onClose }: { open: boolean; onClose: () => void })
|
||||
|
||||
dispatch(currentUserActions.setUserSetting(newSetting));
|
||||
if (userSettingController) {
|
||||
const language = newSetting.language || 'en';
|
||||
|
||||
userSettingController.setAppearanceSetting({
|
||||
theme: newSetting.theme || Theme.Default,
|
||||
mode: newSetting.themeMode || ThemeModePB.Light,
|
||||
theme_mode: newSetting.themeMode || ThemeModePB.Light,
|
||||
locale: {
|
||||
language_code: language.split('-')[0],
|
||||
country_code: language.split('-')[1],
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
@ -44,7 +51,7 @@ function UserSettings({ open, onClose }: { open: boolean; onClose: () => void })
|
||||
keepMounted
|
||||
onClose={onClose}
|
||||
>
|
||||
<DialogTitle>{'Settings'}</DialogTitle>
|
||||
<DialogTitle>{t('settings.title')}</DialogTitle>
|
||||
<DialogContent className={'flex w-[540px]'}>
|
||||
<UserSettingMenu
|
||||
onSelect={(selected) => {
|
||||
|
@ -29,7 +29,7 @@ export const WorkspaceUser = () => {
|
||||
<PersonOutline />
|
||||
</Avatar>
|
||||
<span className={'ml-2'}>{currentUser.displayName}</span>
|
||||
<button className={'ml-1 rounded hover:bg-fill-hover'}>
|
||||
<button className={'ml-1 rounded hover:bg-fill-list-hover'}>
|
||||
<ArrowDropDown />
|
||||
</button>
|
||||
</div>
|
||||
|
@ -5,24 +5,24 @@ export const ColorPalette = () => {
|
||||
<h2 className={'mb-4'}>Main</h2>
|
||||
<div className={'mb-8 flex flex-wrap items-center'}>
|
||||
<div title={'main-accent'} className={'m-2 h-[100px] w-[100px] bg-fill-default'}></div>
|
||||
<div title={'main-hovered'} className={'m-2 h-[100px] w-[100px] bg-fill-hover'}></div>
|
||||
<div title={'main-secondary'} className={'m-2 h-[100px] w-[100px] bg-fill-hover'}></div>
|
||||
<div title={'main-hovered'} className={'m-2 h-[100px] w-[100px] bg-fill-list-hover'}></div>
|
||||
<div title={'main-secondary'} className={'m-2 h-[100px] w-[100px] bg-fill-list-hover'}></div>
|
||||
<div title={'main-selector'} className={'m-2 h-[100px] w-[100px] bg-fill-selector'}></div>
|
||||
<div title={'main-alert'} className={'m-2 h-[100px] w-[100px] bg-function-info'}></div>
|
||||
<div title={'main-warning'} className={'m-2 h-[100px] w-[100px] bg-function-warning'}></div>
|
||||
<div title={'main-success'} className={'m-2 h-[100px] w-[100px] bg-function-success'}></div>
|
||||
</div>
|
||||
<h2 className={'mb-4'}>Tint</h2>
|
||||
<div className={'mb-8 flex flex-wrap items-center text-content-onfill'}>
|
||||
<div title={'tint-1'} className={'m-2 h-[100px] w-[100px] bg-tint-1'}></div>
|
||||
<div title={'tint-2'} className={'m-2 h-[100px] w-[100px] bg-tint-2'}></div>
|
||||
<div title={'tint-3'} className={'m-2 h-[100px] w-[100px] bg-tint-3'}></div>
|
||||
<div title={'tint-4'} className={'m-2 h-[100px] w-[100px] bg-tint-4'}></div>
|
||||
<div title={'tint-5'} className={'m-2 h-[100px] w-[100px] bg-tint-5'}></div>
|
||||
<div title={'tint-6'} className={'m-2 h-[100px] w-[100px] bg-tint-6'}></div>
|
||||
<div title={'tint-7'} className={'m-2 h-[100px] w-[100px] bg-tint-7'}></div>
|
||||
<div title={'tint-8'} className={'m-2 h-[100px] w-[100px] bg-tint-8'}></div>
|
||||
<div title={'tint-9'} className={'m-2 h-[100px] w-[100px] bg-tint-9'}></div>
|
||||
<div className={'mb-8 flex flex-wrap items-center text-text-title'}>
|
||||
<div title={'tint-1'} className={'m-2 h-[100px] w-[100px] bg-tint-pink'}></div>
|
||||
<div title={'tint-2'} className={'m-2 h-[100px] w-[100px] bg-tint-purple'}></div>
|
||||
<div title={'tint-3'} className={'m-2 h-[100px] w-[100px] bg-tint-red'}></div>
|
||||
<div title={'tint-4'} className={'m-2 h-[100px] w-[100px] bg-tint-green'}></div>
|
||||
<div title={'tint-5'} className={'m-2 h-[100px] w-[100px] bg-tint-blue'}></div>
|
||||
<div title={'tint-6'} className={'m-2 h-[100px] w-[100px] bg-tint-yellow'}></div>
|
||||
<div title={'tint-7'} className={'m-2 h-[100px] w-[100px] bg-tint-aqua'}></div>
|
||||
<div title={'tint-8'} className={'m-2 h-[100px] w-[100px] bg-tint-lime'}></div>
|
||||
<div title={'tint-9'} className={'m-2 h-[100px] w-[100px] bg-tint-pink'}></div>
|
||||
</div>
|
||||
<h2 className={'mb-4'}>Shades</h2>
|
||||
<div className={'mb-8 flex flex-wrap items-center'}>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user