feat: support i18n in typescript (#2948)

This commit is contained in:
Kilu.He 2023-07-12 11:54:50 +08:00 committed by GitHub
parent 3d72b6fa12
commit 0dae8cf2f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
123 changed files with 2455 additions and 937 deletions

View File

@ -24,4 +24,5 @@ dist-ssr
*.sw?
**/src/services/backend/models/
**/src/services/backend/events/
**/src/services/backend/events/
**/src/appflowy_app/i18n/translations/

View File

@ -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",

View File

@ -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:

View 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;
}

View File

@ -0,0 +1,8 @@
import resources from './resources';
declare module 'i18next' {
interface CustomTypeOptions {
defaultNS: 'translation';
resources: typeof resources;
}
}

View File

@ -0,0 +1,7 @@
import translation from '$app/i18n/translations/en.json';
const resources = {
translation,
} as const;
export default resources;

View File

@ -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>

View File

@ -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,
})
);
});

View File

@ -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]);

View File

@ -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>

View File

@ -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({

View File

@ -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>

View File

@ -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}

View File

@ -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'}>

View File

@ -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 && (

View File

@ -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')}

View File

@ -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 && (

View File

@ -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'}>

View File

@ -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>

View File

@ -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'}>

View File

@ -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>
))}

View File

@ -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'}>

View File

@ -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'}>

View File

@ -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>

View File

@ -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>

View File

@ -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)}
>
<>

View File

@ -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 ?? '')
}

View File

@ -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>

View File

@ -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 '';
}

View File

@ -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'
/>

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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'}>

View File

@ -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 '}>

View File

@ -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>

View File

@ -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 '}>

View File

@ -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);

View File

@ -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 />}

View File

@ -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(

View File

@ -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 (

View File

@ -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();

View File

@ -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

View File

@ -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}>

View File

@ -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>

View File

@ -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>
);
}

View File

@ -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>

View File

@ -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>

View File

@ -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)',
},
}}
>

View File

@ -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>

View File

@ -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 `}
/>

View File

@ -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>
</>
);

View File

@ -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>
);
};

View File

@ -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();

View File

@ -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>
);
};

View File

@ -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;

View File

@ -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}

View File

@ -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)}

View File

@ -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} />
</>
);

View File

@ -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) {

View File

@ -132,6 +132,7 @@ function InlineContainer({
style={{
pointerEvents: 'none',
}}
className={'inline-block-content'}
>
{renderNode()}
</span>

View File

@ -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]);

View File

@ -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 (

View File

@ -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,

View File

@ -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 />
) : (

View File

@ -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>
);
}

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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>

View File

@ -5,7 +5,7 @@ export const FooterPanel = () => {
&copy; 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>
);

View File

@ -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>

View File

@ -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>

View File

@ -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>}
</>
);
};

View File

@ -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;

View File

@ -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>
);
};

View File

@ -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,
};
};

View File

@ -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>
</>
);
};

View File

@ -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;

View File

@ -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,
};
};

View File

@ -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>
</>
);
};

View File

@ -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>
);
};

View File

@ -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>

View File

@ -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;

View File

@ -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>
);
};

View File

@ -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>

View File

@ -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;

View File

@ -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>
);
};

View File

@ -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>

View File

@ -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({

View File

@ -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;

View File

@ -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>

View File

@ -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;

View File

@ -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) => {

View File

@ -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>

View File

@ -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