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

@ -25,3 +25,4 @@ dist-ssr
**/src/services/backend/models/ **/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", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "pnpm sync:i18n && tsc && vite build",
"preview": "vite preview", "preview": "vite preview",
"format": "prettier --write .", "format": "prettier --write .",
"test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .", "test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
"test:errors": "tsc --noEmit && eslint --quiet --ext .js,.ts,.tsx .", "test:errors": "pnpm sync:i18n && tsc --noEmit && eslint --quiet --ext .js,.ts,.tsx .",
"test:prettier": "yarn prettier --list-different src", "test:prettier": "pnpm prettier --list-different src",
"tauri:clean": "cargo make --cwd .. tauri_clean", "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": { "dependencies": {
"@emoji-mart/data": "^1.1.2", "@emoji-mart/data": "^1.1.2",
@ -25,13 +27,14 @@
"@slate-yjs/core": "^1.0.0", "@slate-yjs/core": "^1.0.0",
"@tanstack/react-virtual": "3.0.0-beta.54", "@tanstack/react-virtual": "3.0.0-beta.54",
"@tauri-apps/api": "^1.2.0", "@tauri-apps/api": "^1.2.0",
"dayjs": "^1.11.7", "dayjs": "^1.11.9",
"emoji-mart": "^5.5.2", "emoji-mart": "^5.5.2",
"emoji-regex": "^10.2.1", "emoji-regex": "^10.2.1",
"events": "^3.3.0", "events": "^3.3.0",
"google-protobuf": "^3.21.2", "google-protobuf": "^3.21.2",
"i18next": "^22.4.10", "i18next": "^22.4.10",
"i18next-browser-languagedetector": "^7.0.1", "i18next-browser-languagedetector": "^7.0.1",
"i18next-resources-to-backend": "^1.1.4",
"is-hotkey": "^0.2.0", "is-hotkey": "^0.2.0",
"jest": "^29.5.0", "jest": "^29.5.0",
"katex": "^0.16.7", "katex": "^0.16.7",
@ -56,7 +59,6 @@
"slate-react": "^0.94.2", "slate-react": "^0.94.2",
"ts-results": "^3.3.0", "ts-results": "^3.3.0",
"utf8": "^3.0.0", "utf8": "^3.0.0",
"y-indexeddb": "^9.0.9",
"yjs": "^13.5.51" "yjs": "^13.5.51"
}, },
"devDependencies": { "devDependencies": {
@ -83,6 +85,7 @@
"postcss": "^8.4.21", "postcss": "^8.4.21",
"prettier": "2.8.4", "prettier": "2.8.4",
"prettier-plugin-tailwindcss": "^0.2.2", "prettier-plugin-tailwindcss": "^0.2.2",
"style-dictionary": "^3.8.0",
"tailwindcss": "^3.2.7", "tailwindcss": "^3.2.7",
"typescript": "^4.6.4", "typescript": "^4.6.4",
"uuid": "^9.0.0", "uuid": "^9.0.0",

View File

@ -32,8 +32,8 @@ dependencies:
specifier: ^1.2.0 specifier: ^1.2.0
version: 1.3.0 version: 1.3.0
dayjs: dayjs:
specifier: ^1.11.7 specifier: ^1.11.9
version: 1.11.7 version: 1.11.9
emoji-mart: emoji-mart:
specifier: ^5.5.2 specifier: ^5.5.2
version: 5.5.2 version: 5.5.2
@ -52,6 +52,9 @@ dependencies:
i18next-browser-languagedetector: i18next-browser-languagedetector:
specifier: ^7.0.1 specifier: ^7.0.1
version: 7.0.1 version: 7.0.1
i18next-resources-to-backend:
specifier: ^1.1.4
version: 1.1.4
is-hotkey: is-hotkey:
specifier: ^0.2.0 specifier: ^0.2.0
version: 0.2.0 version: 0.2.0
@ -124,9 +127,6 @@ dependencies:
utf8: utf8:
specifier: ^3.0.0 specifier: ^3.0.0
version: 3.0.0 version: 3.0.0
y-indexeddb:
specifier: ^9.0.9
version: 9.0.11(yjs@13.6.1)
yjs: yjs:
specifier: ^13.5.51 specifier: ^13.5.51
version: 13.6.1 version: 13.6.1
@ -201,6 +201,9 @@ devDependencies:
prettier-plugin-tailwindcss: prettier-plugin-tailwindcss:
specifier: ^0.2.2 specifier: ^0.2.2
version: 0.2.8(prettier@2.8.4) version: 0.2.8(prettier@2.8.4)
style-dictionary:
specifier: ^3.8.0
version: 3.8.0
tailwindcss: tailwindcss:
specifier: ^3.2.7 specifier: ^3.2.7
version: 3.3.2 version: 3.3.2
@ -2193,6 +2196,13 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'} 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: /camelcase-css@2.0.1:
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@ -2211,6 +2221,14 @@ packages:
/caniuse-lite@1.0.30001487: /caniuse-lite@1.0.30001487:
resolution: {integrity: sha512-83564Z3yWGqXsh2vaH/mhXfEM0wX+NlBCm1jYHOb97TrTWJEmPTccZgeLTPBUUb0PNVo+oomb7wkimZBIERClA==} 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: /chalk@2.4.2:
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
engines: {node: '>=4'} engines: {node: '>=4'}
@ -2226,6 +2244,23 @@ packages:
ansi-styles: 4.3.0 ansi-styles: 4.3.0
supports-color: 7.2.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: /char-regex@1.0.2:
resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -2308,7 +2343,6 @@ packages:
/commander@8.3.0: /commander@8.3.0:
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
engines: {node: '>= 12'} engines: {node: '>= 12'}
dev: false
/compute-scroll-into-view@1.0.20: /compute-scroll-into-view@1.0.20:
resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==} resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==}
@ -2317,6 +2351,14 @@ packages:
/concat-map@0.0.1: /concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 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: /convert-source-map@1.9.0:
resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
@ -2358,8 +2400,8 @@ packages:
/csstype@3.1.2: /csstype@3.1.2:
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
/dayjs@1.11.7: /dayjs@1.11.9:
resolution: {integrity: sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==} resolution: {integrity: sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==}
dev: false dev: false
/debug@4.3.4: /debug@4.3.4:
@ -2455,6 +2497,13 @@ packages:
csstype: 3.1.2 csstype: 3.1.2
dev: false 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: /electron-to-chromium@1.4.394:
resolution: {integrity: sha512-0IbC2cfr8w5LxTz+nmn2cJTGafsK9iauV2r5A5scfzyovqLrxuLoxOHE5OBobP3oVIggJT+0JfKnw9sm87c8Hw==} resolution: {integrity: sha512-0IbC2cfr8w5LxTz+nmn2cJTGafsK9iauV2r5A5scfzyovqLrxuLoxOHE5OBobP3oVIggJT+0JfKnw9sm87c8Hw==}
@ -2884,6 +2933,15 @@ packages:
resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==} resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==}
dev: true 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: /fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
@ -3029,7 +3087,6 @@ packages:
/graceful-fs@4.2.11: /graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
dev: false
/grapheme-splitter@1.0.4: /grapheme-splitter@1.0.4:
resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==}
@ -3072,6 +3129,13 @@ packages:
dependencies: dependencies:
function-bind: 1.1.1 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: /hoist-non-react-statics@3.3.2:
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
dependencies: dependencies:
@ -3099,6 +3163,12 @@ packages:
'@babel/runtime': 7.21.5 '@babel/runtime': 7.21.5
dev: false 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: /i18next@22.4.15:
resolution: {integrity: sha512-yYudtbFrrmWKLEhl6jvKUYyYunj4bTBCe2qIUYAxbXoPusY7YmdwPvOE6fx6UIfWvmlbCWDItr7wIs8KEBZ5Zg==} resolution: {integrity: sha512-yYudtbFrrmWKLEhl6jvKUYyYunj4bTBCe2qIUYAxbXoPusY7YmdwPvOE6fx6UIfWvmlbCWDItr7wIs8KEBZ5Zg==}
dependencies: dependencies:
@ -3825,6 +3895,18 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
hasBin: true 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: /jsx-ast-utils@3.3.3:
resolution: {integrity: sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==} resolution: {integrity: sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==}
engines: {node: '>=4.0'} engines: {node: '>=4.0'}
@ -3904,7 +3986,6 @@ packages:
/lodash@4.17.21: /lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
dev: false
/loose-envify@1.4.0: /loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
@ -3912,6 +3993,12 @@ packages:
dependencies: dependencies:
js-tokens: 4.0.0 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: /lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
dependencies: dependencies:
@ -4003,6 +4090,13 @@ packages:
/natural-compare@1.4.0: /natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} 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: /node-int64@0.4.0:
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
dev: false dev: false
@ -4151,6 +4245,13 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
dev: false 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: /parchment@1.1.4:
resolution: {integrity: sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==} resolution: {integrity: sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==}
@ -4170,6 +4271,20 @@ packages:
lines-and-columns: 1.2.4 lines-and-columns: 1.2.4
dev: false 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: /path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -4799,6 +4914,14 @@ packages:
dependencies: dependencies:
lru-cache: 6.0.0 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: /shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -4858,6 +4981,13 @@ packages:
tiny-warning: 1.0.3 tiny-warning: 1.0.3
dev: false 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: /source-map-js@1.0.2:
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -4966,6 +5096,22 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'} 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: /stylis@4.2.0:
resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==}
dev: false dev: false
@ -5077,6 +5223,10 @@ packages:
resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
dev: false dev: false
/tinycolor2@1.6.0:
resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==}
dev: true
/tmpl@1.0.5: /tmpl@1.0.5:
resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==}
dev: false dev: false
@ -5105,7 +5255,6 @@ packages:
/tslib@2.5.0: /tslib@2.5.0:
resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==}
dev: false
/tsutils@3.21.0(typescript@4.9.5): /tsutils@3.21.0(typescript@4.9.5):
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
@ -5161,6 +5310,11 @@ packages:
which-boxed-primitive: 1.0.2 which-boxed-primitive: 1.0.2
dev: true 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): /update-browserslist-db@1.0.11(browserslist@4.21.5):
resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==}
hasBin: true hasBin: true
@ -5171,6 +5325,18 @@ packages:
escalade: 3.1.1 escalade: 3.1.1
picocolors: 1.0.0 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: /uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
dependencies: dependencies:
@ -5313,15 +5479,6 @@ packages:
signal-exit: 3.0.7 signal-exit: 3.0.7
dev: false 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: /y-protocols@1.0.5:
resolution: {integrity: sha512-Wil92b7cGk712lRHDqS4T90IczF6RkcvCwAD0A2OPg+adKmOe+nOiT/N2hvpQIWS3zfjmtL4CPaH5sIW1Hkm/A==} resolution: {integrity: sha512-Wil92b7cGk712lRHDqS4T90IczF6RkcvCwAD0A2OPg+adKmOe+nOiT/N2hvpQIWS3zfjmtL4CPaH5sIW1Hkm/A==}
dependencies: 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 { store } from './stores/store';
import { ErrorHandlerPage } from './components/error/ErrorHandlerPage'; import { ErrorHandlerPage } from './components/error/ErrorHandlerPage';
import initializeI18n from './stores/i18n/initializeI18n'; import '$app/i18n/config';
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
import AppMain from '$app/AppMain'; import AppMain from '$app/AppMain';
initializeI18n();
const App = () => { const App = () => {
return ( return (
<BrowserRouter> <BrowserRouter>

View File

@ -19,10 +19,20 @@ export function useUserSetting() {
useEffect(() => { useEffect(() => {
userSettingController?.getAppearanceSetting().then((res) => { userSettingController?.getAppearanceSetting().then((res) => {
if (!res) return; 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( dispatch(
currentUserActions.setUserSetting({ currentUserActions.setUserSetting({
themeMode: res.theme_mode, themeMode: res.theme_mode,
theme: res.theme as Theme, theme: res.theme as Theme,
language: language,
}) })
); );
}); });

View File

@ -14,23 +14,23 @@ export const Button = ({
useEffect(() => { useEffect(() => {
switch (size) { switch (size) {
case 'primary': 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; break;
case 'medium': 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; break;
case 'small': case 'small':
setCls( 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; break;
case 'medium-transparent': case 'medium-transparent':
setCls( 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; break;
case 'box-small-transparent': 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; break;
} }
}, [size]); }, [size]);

View File

@ -32,7 +32,7 @@ export const ChangeFieldTypePopup = ({
<button <button
onClick={() => onClick(t)} onClick={() => onClick(t)}
key={i} 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'}> <i className={'h-5 w-5'}>
<FieldTypeIcon fieldType={t}></FieldTypeIcon> <FieldTypeIcon fieldType={t}></FieldTypeIcon>

View File

@ -37,7 +37,7 @@ export const CheckListOption = ({
return ( return (
<div <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={() => onClick={() =>
onToggleOptionClick( onToggleOptionClick(
new SelectOptionPB({ new SelectOptionPB({

View File

@ -69,7 +69,7 @@ export const EditCheckListPopup = ({
top={top} top={top}
> >
<div onKeyDown={onKeyDownWrapper} className={'flex flex-col gap-2 p-2'}> <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 <input
ref={inputRef} ref={inputRef}
className={'py-2'} className={'py-2'}
@ -78,11 +78,13 @@ export const EditCheckListPopup = ({
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
onBlur={() => onBlur()} 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> </div>
<button <button
onClick={() => onDeleteOptionClick()} 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'}> <i className={'h-5 w-5'}>
<TrashSvg></TrashSvg> <TrashSvg></TrashSvg>

View File

@ -80,7 +80,9 @@ function PopupItem({
return ( return (
<button <button
onClick={() => changeFormat(format)} 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} {text}

View File

@ -87,20 +87,24 @@ export const DateTypeOptions = ({
return ( return (
<div className={'flex flex-col'}> <div className={'flex flex-col'}>
<hr className={'-mx-2 my-2 border-shade-6'} /> <hr className={'border-shade-6 -mx-2 my-2'} />
<button <button
onClick={_onDateFormatClick} 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> <span>{t('grid.field.dateFormat')}</span>
<i className={'h-5 w-5'}> <i className={'h-5 w-5'}>
<MoreSvg></MoreSvg> <MoreSvg></MoreSvg>
</i> </i>
</button> </button>
<hr className={'-mx-2 my-2 border-line-border'} /> <hr className={'-mx-2 my-2 border-line-divider'} />
<button <button
onClick={() => toggleIncludeTime()} 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'}> <div className={'flex items-center gap-2'}>
<span>{t('grid.field.includeTime')}</span> <span>{t('grid.field.includeTime')}</span>
@ -112,7 +116,9 @@ export const DateTypeOptions = ({
<button <button
onClick={_onTimeFormatClick} 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> <span>{t('grid.field.timeFormat')}</span>
<i className={'h-5 w-5'}> <i className={'h-5 w-5'}>

View File

@ -93,7 +93,9 @@ const FormatButton = ({ title, checked, onClick }: { title: string; checked: boo
return ( return (
<button <button
onClick={() => onClick()} 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> <span className={'block pr-8'}>{title}</span>
{checked && ( {checked && (

View File

@ -41,7 +41,9 @@ export const TimeFormatPopup = ({
<PopupWindow className={'p-2 text-xs'} onOutsideClick={onOutsideClick} left={left} top={top}> <PopupWindow className={'p-2 text-xs'} onOutsideClick={onOutsideClick} left={left} top={top}>
<button <button
onClick={() => changeFormat(TimeFormatPB.TwelveHour)} 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')} {t('grid.field.timeFormatTwelveHour')}
@ -53,7 +55,9 @@ export const TimeFormatPopup = ({
</button> </button>
<button <button
onClick={() => changeFormat(TimeFormatPB.TwentyFourHour)} 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')} {t('grid.field.timeFormatTwentyFourHour')}

View File

@ -59,7 +59,7 @@ export const EditCellWrapper = ({
<div <div
ref={el} ref={el}
onClick={() => onClick()} 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> <DragElementSvg></DragElementSvg>
</div> </div>
@ -72,7 +72,7 @@ export const EditCellWrapper = ({
</span> </span>
</div> </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.SingleSelect ||
cellIdentifier.fieldType === FieldType.MultiSelect) && cellIdentifier.fieldType === FieldType.MultiSelect) &&
cellController && ( cellController && (

View File

@ -101,7 +101,7 @@ export const EditFieldPopup = ({
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
onBlur={() => save()} onBlur={() => save()}
className={ 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} ref={changeTypeButtonRef}
onClick={() => onChangeFieldTypeClick()} onClick={() => onChangeFieldTypeClick()}
className={ 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'}> <button className={'flex cursor-pointer items-center gap-2 rounded-lg pl-2'}>
@ -129,10 +129,12 @@ export const EditFieldPopup = ({
{cellIdentifier.fieldType === FieldType.Number && ( {cellIdentifier.fieldType === FieldType.Number && (
<> <>
<hr className={'-mx-2 border-line-border'} /> <hr className={'-mx-2 border-line-divider'} />
<button <button
onClick={onNumberFormatClick} 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={'pl-2'}>{t('grid.field.numberFormat')}</span>
<span className={'pr-2'}> <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 `} 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'}> <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> <CloseSvg></CloseSvg>
</button> </button>
</div> </div>
<div className={'flex h-full'}> <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'}> <div className={'pb-4 pl-12'}>
<button className={'flex items-center gap-2 p-4'}> <button className={'flex items-center gap-2 p-4'}>
<i className={'h-5 w-5'}> <i className={'h-5 w-5'}>
@ -254,10 +254,10 @@ export const EditRow = ({
</Droppable> </Droppable>
</DragDropContext> </DragDropContext>
<div className={'border-t border-line-border px-8 pt-2'}> <div className={'border-t border-line-divider px-8 pt-2'}>
<button <button
onClick={() => onNewColumnClick()} 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'}> <i className={'h-5 w-5'}>
<AddSvg></AddSvg> <AddSvg></AddSvg>

View File

@ -53,9 +53,9 @@ export const CellOption = ({
return ( return (
<div <div
onClick={onToggleOptionClick} 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'}> <div className={'flex items-center'}>
{checked && ( {checked && (
<button className={'h-5 w-5 p-1'}> <button className={'h-5 w-5 p-1'}>

View File

@ -19,13 +19,9 @@ export const CellOptions = ({
}; };
return ( return (
<div <div ref={ref} onClick={onClick} className={'flex w-full flex-wrap items-center gap-2 px-4 py-1 text-xs'}>
ref={ref}
onClick={onClick}
className={'flex w-full flex-wrap items-center gap-2 px-4 py-1 text-xs text-content-onfill'}
>
{data?.select_options?.map((option, index) => ( {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 ?? ''} {option?.name ?? ''}
</div> </div>
))} ))}

View File

@ -59,7 +59,7 @@ export const CellOptionsPopup = ({
<div onKeyDown={onKeyDownWrapper} className={'flex flex-col gap-2 p-2'}> <div onKeyDown={onKeyDownWrapper} className={'flex flex-col gap-2 p-2'}>
<div <div
className={ 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'}> <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 onKeyDown={onKeyDownWrapper} className={'flex flex-col gap-2 p-2'}>
<div <div
className={ 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 <input
@ -101,7 +101,9 @@ export const EditCellOptionPopup = ({
</div> </div>
<button <button
onClick={() => onDeleteOptionClick()} 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'}> <i className={'h-5 w-5'}>
<TrashSvg></TrashSvg> <TrashSvg></TrashSvg>
@ -184,7 +186,7 @@ const ColorItem = ({
}) => { }) => {
return ( return (
<div <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()} onClick={() => onClick()}
> >
<div className={'flex items-center gap-2'}> <div className={'flex items-center gap-2'}>

View File

@ -20,7 +20,7 @@ export const SelectedOption = ({
}; };
return ( 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> <span>{option?.name ?? ''}</span>
<button onClick={onUnselectOptionClick} className={'h-5 w-5 cursor-pointer'}> <button onClick={onUnselectOptionClick} className={'h-5 w-5 cursor-pointer'}>
<CloseSvg></CloseSvg> <CloseSvg></CloseSvg>

View File

@ -103,7 +103,7 @@ export const PropertiesPanel = ({
<div <div
onClick={() => setShowAddedProperties(!showAddedProperties)} onClick={() => setShowAddedProperties(!showAddedProperties)}
className={ 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> <div className={'text-sm'}>Added Properties</div>
@ -118,7 +118,7 @@ export const PropertiesPanel = ({
key={cellIndex} key={cellIndex}
onMouseEnter={() => setHoveredPropertyIndex(cellIndex)} onMouseEnter={() => setHoveredPropertyIndex(cellIndex)}
className={ 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 '}> <div className={'flex items-center gap-2 text-text-title '}>
@ -148,7 +148,9 @@ export const PropertiesPanel = ({
</div> </div>
<div <div
onClick={() => setShowBasicProperties(!showBasicProperties)} 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> <div className={'text-sm'}>Basic Properties</div>
<i className={`h-5 w-5 transition-transform duration-500 ${showBasicProperties && 'rotate-180'}`}> <i className={`h-5 w-5 transition-transform duration-500 ${showBasicProperties && 'rotate-180'}`}>
@ -162,7 +164,7 @@ export const PropertiesPanel = ({
<button <button
onClick={() => addSelectedFieldType(type)} onClick={() => addSelectedFieldType(type)}
key={i} 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'}> <i className={'h-5 w-5'}>
<FieldTypeIcon fieldType={type}></FieldTypeIcon> <FieldTypeIcon fieldType={type}></FieldTypeIcon>
@ -177,7 +179,9 @@ export const PropertiesPanel = ({
</div> </div>
<div <div
onClick={() => setShowAdvancedProperties(!showAdvancedProperties)} 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> <div className={'text-sm'}>Advanced Properties</div>
<i className={`h-5 w-5 transition-transform duration-500 ${showAdvancedProperties && 'rotate-180'}`}> <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'}> <div className={'flex flex-col gap-2 text-xs'}>
{showAdvancedProperties && ( {showAdvancedProperties && (
<div className={'flex flex-col'}> <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'}> <i className={'h-5 w-5'}>
<MultiSelectTypeSvg></MultiSelectTypeSvg> <MultiSelectTypeSvg></MultiSelectTypeSvg>
</i> </i>
<span>Last edited time</span> <span>Last edited time</span>
</button> </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'}> <i className={'h-5 w-5'}>
<DocumentSvg></DocumentSvg> <DocumentSvg></DocumentSvg>
</i> </i>
<span>Document</span> <span>Document</span>
</button> </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'}> <i className={'h-5 w-5'}>
<SingleSelectTypeSvg></SingleSelectTypeSvg> <SingleSelectTypeSvg></SingleSelectTypeSvg>
</i> </i>

View File

@ -39,7 +39,7 @@ export const PopupSelect = ({
{items.map((item, index) => ( {items.map((item, index) => (
<button <button
key={index} 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)} onClick={(e) => handleClick(e, item)}
> >
<> <>

View File

@ -42,7 +42,7 @@ export const PopupWindow = ({
<div <div
ref={ref} ref={ref}
className={ 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 ') + (adjustedTop === -100 && adjustedLeft === -100 ? 'opacity-0 ' : 'opacity-100 ') +
(className ?? '') (className ?? '')
} }

View File

@ -5,7 +5,7 @@ export const SearchInput = () => {
const [active, setActive] = useState(false); const [active, setActive] = useState(false);
return ( 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'> <i className='mr-2 h-5 w-5'>
<SearchSvg /> <SearchSvg />
</i> </i>

View File

@ -3,23 +3,23 @@ import { SelectOptionColorPB } from '../../../services/backend';
export const getBgColor = (color: SelectOptionColorPB | undefined): string => { export const getBgColor = (color: SelectOptionColorPB | undefined): string => {
switch (color) { switch (color) {
case SelectOptionColorPB.Purple: case SelectOptionColorPB.Purple:
return 'bg-tint-1'; return 'bg-tint-purple';
case SelectOptionColorPB.Pink: case SelectOptionColorPB.Pink:
return 'bg-tint-2'; return 'bg-tint-pink';
case SelectOptionColorPB.LightPink: case SelectOptionColorPB.LightPink:
return 'bg-tint-3'; return 'bg-tint-red';
case SelectOptionColorPB.Orange: case SelectOptionColorPB.Orange:
return 'bg-tint-4'; return 'bg-tint-orange';
case SelectOptionColorPB.Yellow: case SelectOptionColorPB.Yellow:
return 'bg-tint-5'; return 'bg-tint-yellow';
case SelectOptionColorPB.Lime: case SelectOptionColorPB.Lime:
return 'bg-tint-6'; return 'bg-tint-lime';
case SelectOptionColorPB.Green: case SelectOptionColorPB.Green:
return 'bg-tint-7'; return 'bg-tint-green';
case SelectOptionColorPB.Aqua: case SelectOptionColorPB.Aqua:
return 'bg-tint-8'; return 'bg-tint-aqua';
case SelectOptionColorPB.Blue: case SelectOptionColorPB.Blue:
return 'bg-tint-9'; return 'bg-tint-blue';
default: default:
return ''; return '';
} }

View File

@ -1,10 +1,10 @@
export const EditorCheckSvg = () => { export const EditorCheckSvg = () => {
return ( return (
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'> <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 <path
d='M6 8L7.61538 9.5L10.5 6.5' d='M6 8L7.61538 9.5L10.5 6.5'
stroke={'var(--color-content-onfill)'} stroke={'var(--content-on-fill)'}
strokeLinecap='round' strokeLinecap='round'
strokeLinejoin='round' strokeLinejoin='round'
/> />

View File

@ -1,7 +1,7 @@
export const EditorUncheckSvg = () => { export const EditorUncheckSvg = () => {
return ( return (
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'> <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> </svg>
); );
}; };

View File

@ -1,10 +1,10 @@
export const FullView = () => { export const FullView = () => {
return ( return (
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'> <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='M6 13H3V10' stroke='var(--text-title)' strokeLinecap='round' strokeLinejoin='round' />
<path d='M10 3H13V6' stroke='var(--color-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(--color-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(--color-text-title)' strokeLinecap='round' strokeLinejoin='round' /> <path d='M13 3L9 7' stroke='var(--text-title)' strokeLinecap='round' strokeLinejoin='round' />
</svg> </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'> <svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path <path
d='M10 2H13C13.5523 2 14 2.44772 14 3V6' d='M10 2H13C13.5523 2 14 2.44772 14 3V6'
stroke='var(--color-text-title)' stroke='var(--text-title)'
strokeLinecap='round' strokeLinecap='round'
strokeLinejoin='round' strokeLinejoin='round'
/> />
<path <path
d='M6 2H3C2.44772 2 2 2.44772 2 3V6' d='M6 2H3C2.44772 2 2 2.44772 2 3V6'
stroke='var(--color-text-title)' stroke='var(--text-title)'
strokeLinecap='round' strokeLinecap='round'
strokeLinejoin='round' strokeLinejoin='round'
/> />
<path <path
d='M6 14H3C2.44772 14 2 13.5523 2 13V10' d='M6 14H3C2.44772 14 2 13.5523 2 13V10'
stroke='var(--color-text-title)' stroke='var(--text-title)'
strokeLinecap='round' strokeLinecap='round'
strokeLinejoin='round' strokeLinejoin='round'
/> />
<path <path
d='M10 14H13C13.5523 14 14 13.5523 14 13V10' d='M10 14H13C13.5523 14 14 13.5523 14 13V10'
stroke='var(--color-text-title)' stroke='var(--text-title)'
strokeLinecap='round' strokeLinecap='round'
strokeLinejoin='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> </svg>
); );
}; };

View File

@ -64,9 +64,12 @@ export const BoardCard = ({
{...provided.draggableProps} {...provided.draggableProps}
{...provided.dragHandleProps} {...provided.dragHandleProps}
onClick={() => onOpenRow(rowInfo)} 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-list-hover'}
> >
<button onClick={onDetailClick} className={'absolute right-4 top-2.5 h-5 w-5 rounded hover:bg-fill-hover'}>
<Details2Svg></Details2Svg> <Details2Svg></Details2Svg>
</button> </button>
<div className={'flex flex-col gap-3'}> <div className={'flex flex-col gap-3'}>
@ -95,7 +98,7 @@ export const BoardCard = ({
> >
<button <button
key={index} 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()} onClick={() => onDeleteRowClick()}
> >
<i className={'h-5 w-5'}> <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'}> <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) => ( {columns.map((column, index) => (
<div <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} key={index}
> >
<div className={'flex items-center gap-2 '}> <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> <span className={'text-shade-4'}>({group.rows.length})</span>
</div> </div>
<div className={'flex items-center gap-2'}> <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> <Details2Svg></Details2Svg>
</button> </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> <AddSvg></AddSvg>
</button> </button>
</div> </div>
@ -86,7 +86,7 @@ export const BoardGroup = ({
<div className={'p-2'}> <div className={'p-2'}>
<button <button
onClick={onNewRowClick} 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'}> <span className={'h-5 w-5'}>
<AddSvg></AddSvg> <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'}> <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) => ( {columns.map((column, index) => (
<div <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} key={index}
> >
<div className={'flex items-center gap-2 '}> <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 { selectOptionByUpDown } from '$app/utils/document/menu';
import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks'; import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks';
import { BlockType } from '$app/interfaces/document'; import { BlockType } from '$app/interfaces/document';
import { useTranslation } from 'react-i18next';
enum BlockMenuOption { enum BlockMenuOption {
Duplicate = 'Duplicate', Duplicate = 'Duplicate',
@ -27,6 +28,7 @@ function BlockMenu({ id, onClose }: { id: string; onClose: () => void }) {
const { node } = useSubscribeNode(id); const { node } = useSubscribeNode(id);
const [subMenuOpened, setSubMenuOpened] = useState(false); const [subMenuOpened, setSubMenuOpened] = useState(false);
const [hovered, setHovered] = useState<BlockMenuOption | null>(null); const [hovered, setHovered] = useState<BlockMenuOption | null>(null);
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
if (hovered !== BlockMenuOption.TurnInto) { if (hovered !== BlockMenuOption.TurnInto) {
@ -53,7 +55,7 @@ function BlockMenu({ id, onClose }: { id: string; onClose: () => void }) {
operate: () => { operate: () => {
return handleClick({ operate: handleDelete }); return handleClick({ operate: handleDelete });
}, },
title: 'Delete', title: t('document.plugins.optionAction.delete'),
icon: <Delete />, icon: <Delete />,
key: BlockMenuOption.Delete, key: BlockMenuOption.Delete,
}, },
@ -61,7 +63,7 @@ function BlockMenu({ id, onClose }: { id: string; onClose: () => void }) {
operate: () => { operate: () => {
return handleClick({ operate: handleDuplicate }); return handleClick({ operate: handleDuplicate });
}, },
title: 'Duplicate', title: t('document.plugins.optionAction.duplicate'),
icon: <ContentCopy />, icon: <ContentCopy />,
key: BlockMenuOption.Duplicate, key: BlockMenuOption.Duplicate,
}, },
@ -69,9 +71,10 @@ function BlockMenu({ id, onClose }: { id: string; onClose: () => void }) {
? null ? null
: { : {
key: BlockMenuOption.TurnInto, key: BlockMenuOption.TurnInto,
title: t('document.plugins.optionAction.turnInto'),
}, },
].filter((item) => item !== null) as Option[], ].filter((item) => item !== null) as Option[],
[excludeTurnIntoBlock, handleClick, handleDelete, handleDuplicate] [excludeTurnIntoBlock, handleClick, handleDelete, handleDuplicate, t]
); );
const onKeyDown = useCallback( const onKeyDown = useCallback(
@ -128,13 +131,19 @@ function BlockMenu({ id, onClose }: { id: string; onClose: () => void }) {
}} }}
> >
<div className={'p-2'}> <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> </div>
{options.map((option) => { {options.map((option) => {
if (option.key === BlockMenuOption.TurnInto) { if (option.key === BlockMenuOption.TurnInto) {
return ( return (
<BlockMenuTurnInto <BlockMenuTurnInto
key={option.key} key={option.key}
lable={option.title}
onHovered={() => { onHovered={() => {
setHovered(BlockMenuOption.TurnInto); setHovered(BlockMenuOption.TurnInto);
setSubMenuOpened(true); setSubMenuOpened(true);

View File

@ -9,12 +9,14 @@ function BlockMenuTurnInto({
onHovered, onHovered,
isHovered, isHovered,
menuOpened, menuOpened,
lable,
}: { }: {
id: string; id: string;
onClose: () => void; onClose: () => void;
onHovered: (e: MouseEvent) => void; onHovered: (e: MouseEvent) => void;
isHovered: boolean; isHovered: boolean;
menuOpened: boolean; menuOpened: boolean;
lable?: string;
}) { }) {
const ref = useRef<HTMLDivElement | null>(null); const ref = useRef<HTMLDivElement | null>(null);
const [anchorPosition, setAnchorPosition] = React.useState<{ top: number; left: number }>(); const [anchorPosition, setAnchorPosition] = React.useState<{ top: number; left: number }>();
@ -37,7 +39,7 @@ function BlockMenuTurnInto({
<> <>
<MenuItem <MenuItem
ref={ref} ref={ref}
title='Turn into' title={lable}
isHovered={isHovered} isHovered={isHovered}
icon={<Transform />} icon={<Transform />}
extra={<ArrowRight />} 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 { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
import { RANGE_NAME, RECT_RANGE_NAME } from '$app/constants/document/name'; import { RANGE_NAME, RECT_RANGE_NAME } from '$app/constants/document/name';
import { setRectSelectionThunk } from '$app_reducers/document/async-actions/rect_selection'; import { setRectSelectionThunk } from '$app_reducers/document/async-actions/rect_selection';
import { useTranslation } from 'react-i18next';
export default function BlockSideToolbar({ container }: { container: HTMLDivElement }) { export default function BlockSideToolbar({ container }: { container: HTMLDivElement }) {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { docId, controller } = useSubscribeDocument(); const { docId, controller } = useSubscribeDocument();
const { t } = useTranslation();
const { nodeId, style, ref } = useBlockSideToolbar({ container }); const { nodeId, style, ref } = useBlockSideToolbar({ container });
const isDragging = useAppSelector( const isDragging = useAppSelector(
@ -42,7 +44,7 @@ export default function BlockSideToolbar({ container }: { container: HTMLDivElem
> >
{/** Add Block below */} {/** Add Block below */}
<ToolbarButton <ToolbarButton
tooltip={'Add a new block below'} tooltip={t('tooltip.addBlockBelow')}
onClick={(e: React.MouseEvent<HTMLButtonElement>) => { onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
if (!nodeId || !controller) return; if (!nodeId || !controller) return;
dispatch( dispatch(
@ -58,7 +60,7 @@ export default function BlockSideToolbar({ container }: { container: HTMLDivElem
{/** Open menu or drag */} {/** Open menu or drag */}
<ToolbarButton <ToolbarButton
tooltip={'Click to open Menu'} tooltip={t('tooltip.openMenu')}
onClick={(e: React.MouseEvent<HTMLButtonElement>) => { onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
if (!nodeId) return; if (!nodeId) return;
dispatch( dispatch(

View File

@ -27,7 +27,6 @@ import { useSubscribeDocument } from '$app/components/document/_shared/Subscribe
import { slashCommandActions } from '$app_reducers/document/slice'; import { slashCommandActions } from '$app_reducers/document/slice';
import { Keyboard } from '$app/constants/document/keyboard'; import { Keyboard } from '$app/constants/document/keyboard';
import { selectOptionByUpDown } from '$app/utils/document/menu'; import { selectOptionByUpDown } from '$app/utils/document/menu';
import { blockEditActions } from '$app_reducers/document/block_edit_slice';
function BlockSlashMenu({ function BlockSlashMenu({
id, id,
@ -60,7 +59,7 @@ function BlockSlashMenu({
); );
onClose?.(); onClose?.();
}, },
[controller, dispatch, docId, id, onClose] [controller, dispatch, id, onClose]
); );
const options: (SlashCommandOption & { const options: (SlashCommandOption & {
@ -293,7 +292,7 @@ function BlockSlashMenu({
<div ref={ref} className={'min-h-0 flex-1 overflow-y-auto overflow-x-hidden'}> <div ref={ref} className={'min-h-0 flex-1 overflow-y-auto overflow-x-hidden'}>
{Object.entries(optionsByGroup).map(([group, options]) => ( {Object.entries(optionsByGroup).map(([group, options]) => (
<div key={group}> <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> <div>
{options.map((option) => { {options.map((option) => {
return ( return (

View File

@ -1,8 +1,7 @@
import React, { useEffect } from 'react'; import React from 'react';
import Popover from '@mui/material/Popover'; import Popover from '@mui/material/Popover';
import BlockSlashMenu from '$app/components/document/BlockSlash/BlockSlashMenu'; import BlockSlashMenu from '$app/components/document/BlockSlash/BlockSlashMenu';
import { useBlockSlash } from '$app/components/document/BlockSlash/index.hooks'; import { useBlockSlash } from '$app/components/document/BlockSlash/index.hooks';
import { Keyboard } from '$app/constants/document/keyboard';
function BlockSlash({ container }: { container: HTMLDivElement }) { function BlockSlash({ container }: { container: HTMLDivElement }) {
const { blockId, open, onClose, anchorPosition, searchText, hoverOption } = useBlockSlash(); 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); const { openEmojiSelect, open, closeEmojiSelect, id, anchorEl, onEmojiSelect } = useCalloutBlock(node.id);
return ( 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={'w-[1.5em]'} onMouseDown={(e) => e.stopPropagation()}>
<div className={'flex h-[calc(1.5em_+_2px)] w-[24px] select-none items-center justify-start'}> <div className={'flex h-[calc(1.5em_+_2px)] w-[24px] select-none items-center justify-start'}>
<IconButton <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 MenuItem from '@mui/material/MenuItem';
import FormControl from '@mui/material/FormControl'; import FormControl from '@mui/material/FormControl';
import Select, { SelectChangeEvent } from '@mui/material/Select'; 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 { useAppDispatch } from '$app/stores/store';
import { supportLanguage } from '$app/constants/document/code'; import { supportLanguage } from '$app/constants/document/code';
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks'; import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
import { useTranslation } from 'react-i18next';
function SelectLanguage({ id, language }: { id: string; language: string }) { function SelectLanguage({ id, language }: { id: string; language: string }) {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { controller } = useSubscribeDocument(); const { controller } = useSubscribeDocument();
const { t } = useTranslation();
const onLanguageSelect = useCallback( const onLanguageSelect = useCallback(
(event: SelectChangeEvent) => { (event: SelectChangeEvent) => {
if (!controller) return; if (!controller) return;
const language = event.target.value; const language = event.target.value;
dispatch( dispatch(
updateNodeDataThunk({ updateNodeDataThunk({
id, id,
@ -34,7 +36,8 @@ function SelectLanguage({ id, language }: { id: string; language: string }) {
className={'h-[28px] w-[150px]'} className={'h-[28px] w-[150px]'}
value={language || 'javascript'} value={language || 'javascript'}
onChange={onLanguageSelect} onChange={onLanguageSelect}
label='Language' placeholder={t('document.codeBlock.language.placeholder')}
label={t('document.codeBlock.language.label')}
> >
{supportLanguage.map((item) => ( {supportLanguage.map((item) => (
<MenuItem key={item.id} value={item.id}> <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); const isDark = useAppSelector((state) => state.currentUser.userSetting.themeMode === ThemeMode.Dark);
return ( 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%]'}> <div className={'mb-2 w-[100%]'}>
<SelectLanguage id={id} language={language} /> <SelectLanguage id={id} language={language} />
</div> </div>

View File

@ -1,13 +1,16 @@
import React from 'react'; import React from 'react';
import { useDocumentTitle } from './DocumentTitle.hooks'; import { useDocumentTitle } from './DocumentTitle.hooks';
import TextBlock from '../TextBlock'; import TextBlock from '../TextBlock';
import { useTranslation } from 'react-i18next';
export default function DocumentTitle({ id }: { id: string }) { export default function DocumentTitle({ id }: { id: string }) {
const { node } = useDocumentTitle(id); const { node } = useDocumentTitle(id);
const { t } = useTranslation();
if (!node) return null; if (!node) return null;
return ( return (
<div data-block-id={node.id} className='doc-title relative mb-2 pt-[50px] text-4xl font-bold'> <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> </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 { updateNodeDataThunk } from '$app_reducers/document/async-actions';
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks'; import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
import { useAppDispatch } from '$app/stores/store'; import { useAppDispatch } from '$app/stores/store';
import { useTranslation } from 'react-i18next';
function EquationBlock({ node }: { node: NestedBlock<BlockType.EquationBlock> }) { function EquationBlock({ node }: { node: NestedBlock<BlockType.EquationBlock> }) {
const formula = node.data.formula; const formula = node.data.formula;
@ -60,19 +61,21 @@ function EquationBlock({ node }: { node: NestedBlock<BlockType.EquationBlock> })
}); });
const displayFormula = open ? value : formula; const displayFormula = open ? value : formula;
const { t } = useTranslation();
return ( return (
<> <>
<div <div
ref={anchorElRef} ref={anchorElRef}
onClick={openPopover} 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 ? ( {displayFormula ? (
<KatexMath latex={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 /> <Functions />
<span>Add a TeX equation</span> <span>{t('document.plugins.mathEquation.addMathEquation')}</span>
</div> </div>
)} )}
</div> </div>

View File

@ -4,7 +4,7 @@ import { useAppDispatch } from '$app/stores/store';
import { updateNodeDataThunk } from '$app_reducers/document/async-actions'; import { updateNodeDataThunk } from '$app_reducers/document/async-actions';
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks'; import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
import UploadImage from '$app/components/document/_shared/UploadImage'; import UploadImage from '$app/components/document/_shared/UploadImage';
import { isTauri } from '$app/utils/env'; import { useTranslation } from 'react-i18next';
enum TAB_KEYS { enum TAB_KEYS {
UPLOAD = 'upload', UPLOAD = 'upload',
@ -13,6 +13,7 @@ enum TAB_KEYS {
function EditImage({ id, url, onClose }: { id: string; url: string; onClose: () => void }) { function EditImage({ id, url, onClose }: { id: string; url: string; onClose: () => void }) {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation();
const { controller } = useSubscribeDocument(); const { controller } = useSubscribeDocument();
const [linkVal, setLinkVal] = useState<string>(url); const [linkVal, setLinkVal] = useState<string>(url);
const [tabKey, setTabKey] = useState<TAB_KEYS>(TAB_KEYS.UPLOAD); 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]'}> <div className={'w-[540px]'}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}> <Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={tabKey} onChange={handleChange}> <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> </Tabs>
</Box> </Box>
{isTauri() && (
<TabPanel value={tabKey} index={TAB_KEYS.UPLOAD}> <TabPanel value={tabKey} index={TAB_KEYS.UPLOAD}>
<UploadImage onChange={handleConfirmUrl} /> <UploadImage onChange={handleConfirmUrl} />
</TabPanel> </TabPanel>
)}
<TabPanel className={'flex flex-col p-3'} value={tabKey} index={TAB_KEYS.LINK}> <TabPanel className={'flex flex-col p-3'} value={tabKey} index={TAB_KEYS.LINK}>
<TextField <TextField
value={linkVal} value={linkVal}
onChange={(e) => setLinkVal(e.target.value)} onChange={(e) => setLinkVal(e.target.value)}
variant='outlined' variant='outlined'
label={'URL'} label={t('document.imageBlock.url.label')}
autoFocus={true} autoFocus={true}
style={{ style={{
marginBottom: '10px', marginBottom: '10px',
}} }}
placeholder={'Please enter the URL of the image'} placeholder={t('document.imageBlock.url.placeholder')}
/> />
<Button onClick={() => handleConfirmUrl(linkVal)} variant='contained'> <Button onClick={() => handleConfirmUrl(linkVal)} variant='contained'>
Upload {t('button.upload')}
</Button> </Button>
</TabPanel> </TabPanel>
</div> </div>

View File

@ -4,8 +4,9 @@ import { useSubscribeDocument } from '$app/components/document/_shared/Subscribe
import { Align } from '$app/interfaces/document'; import { Align } from '$app/interfaces/document';
import { FormatAlignCenter, FormatAlignLeft, FormatAlignRight } from '@mui/icons-material'; import { FormatAlignCenter, FormatAlignLeft, FormatAlignRight } from '@mui/icons-material';
import { updateNodeDataThunk } from '$app_reducers/document/async-actions'; 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 Popover from '@mui/material/Popover';
import { useTranslation } from 'react-i18next';
function ImageAlign({ function ImageAlign({
id, id,
@ -21,6 +22,7 @@ function ImageAlign({
const ref = useRef<HTMLDivElement | null>(null); const ref = useRef<HTMLDivElement | null>(null);
const [anchorEl, setAnchorEl] = useState<HTMLDivElement>(); const [anchorEl, setAnchorEl] = useState<HTMLDivElement>();
const popoverOpen = Boolean(anchorEl); const popoverOpen = Boolean(anchorEl);
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
if (popoverOpen) { if (popoverOpen) {
@ -61,7 +63,7 @@ function ImageAlign({
return ( return (
<> <>
<MenuTooltip title='Align'> <ToolbarTooltip title={t('document.plugins.optionAction.align')}>
<div <div
ref={ref} ref={ref}
className='flex items-center justify-center p-1' className='flex items-center justify-center p-1'
@ -71,7 +73,7 @@ function ImageAlign({
> >
{renderAlign(align)} {renderAlign(align)}
</div> </div>
</MenuTooltip> </ToolbarTooltip>
<Popover <Popover
open={popoverOpen} open={popoverOpen}
anchorOrigin={{ anchorOrigin={{
@ -87,7 +89,7 @@ function ImageAlign({
onClose={() => setAnchorEl(undefined)} onClose={() => setAnchorEl(undefined)}
PaperProps={{ PaperProps={{
style: { style: {
backgroundColor: 'var(--color-bg-body)', backgroundColor: 'var(--bg-body)',
}, },
}} }}
> >

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { Alert, CircularProgress } from '@mui/material'; import { Alert, CircularProgress } from '@mui/material';
import { ImageSvg } from '$app/components/_shared/svg/ImageSvg'; import { ImageSvg } from '$app/components/_shared/svg/ImageSvg';
import { useTranslation } from 'react-i18next';
function ImagePlaceholder({ function ImagePlaceholder({
error, error,
@ -20,6 +21,7 @@ function ImagePlaceholder({
openPopover: () => void; openPopover: () => void;
}) { }) {
const visible = loading || error || isEmpty; const visible = loading || error || isEmpty;
const { t } = useTranslation();
return ( return (
<div <div
@ -40,12 +42,12 @@ function ImagePlaceholder({
{isEmpty && ( {isEmpty && (
<div <div
onClick={openPopover} 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'}> <i className={'mx-2 h-5 w-5'}>
<ImageSvg /> <ImageSvg />
</i> </i>
<span>Add an image</span> <span>{t('document.imageBlock.placeholder')}</span>
</div> </div>
)} )}
</div> </div>

View File

@ -29,7 +29,7 @@ function ImageRender({
} top-0 flex h-[100%] w-[15px] cursor-col-resize items-center justify-center`} } top-0 flex h-[100%] w-[15px] cursor-col-resize items-center justify-center`}
> >
<div <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' toolbarOpen ? 'opacity-1' : 'opacity-0'
} transition-opacity duration-300 `} } transition-opacity duration-300 `}
/> />

View File

@ -1,11 +1,12 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Align } from '$app/interfaces/document'; import { Align } from '$app/interfaces/document';
import ImageAlign from '$app/components/document/ImageBlock/ImageAlign'; 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 { DeleteOutline } from '@mui/icons-material';
import { useAppDispatch } from '$app/stores/store'; import { useAppDispatch } from '$app/stores/store';
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks'; import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
import { deleteNodeThunk } from '$app_reducers/document/async-actions'; import { deleteNodeThunk } from '$app_reducers/document/async-actions';
import { useTranslation } from 'react-i18next';
function ImageToolbar({ id, open, align }: { id: string; open: boolean; align: Align }) { function ImageToolbar({ id, open, align }: { id: string; open: boolean; align: Align }) {
const [popoverOpen, setPopoverOpen] = useState(false); const [popoverOpen, setPopoverOpen] = useState(false);
@ -13,6 +14,8 @@ function ImageToolbar({ id, open, align }: { id: string; open: boolean; align: A
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { controller } = useSubscribeDocument(); const { controller } = useSubscribeDocument();
const { t } = useTranslation();
return ( return (
<> <>
<div <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`} } 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)} /> <ImageAlign id={id} align={align} onOpen={() => setPopoverOpen(true)} onClose={() => setPopoverOpen(false)} />
<MenuTooltip title={'Delete'}> <ToolbarTooltip title={t('button.delete')}>
<div <div
onClick={() => { onClick={() => {
dispatch(deleteNodeThunk({ id, controller })); dispatch(deleteNodeThunk({ id, controller }));
@ -30,7 +33,7 @@ function ImageToolbar({ id, open, align }: { id: string; open: boolean; align: A
> >
<DeleteOutline /> <DeleteOutline />
</div> </div>
</MenuTooltip> </ToolbarTooltip>
</div> </div>
</> </>
); );

View File

@ -19,6 +19,7 @@ import CodeBlock from '$app/components/document/CodeBlock';
import { NodeIdContext } from '$app/components/document/_shared/SubscribeNode.hooks'; import { NodeIdContext } from '$app/components/document/_shared/SubscribeNode.hooks';
import EquationBlock from '$app/components/document/EquationBlock'; import EquationBlock from '$app/components/document/EquationBlock';
import ImageBlock from '$app/components/document/ImageBlock'; import ImageBlock from '$app/components/document/ImageBlock';
import { useTranslation } from 'react-i18next';
function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<HTMLDivElement>) { function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<HTMLDivElement>) {
const { node, childIds, isSelected, ref } = useNode(id); const { node, childIds, isSelected, ref } = useNode(id);
@ -82,7 +83,7 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<H
{renderBlock()} {renderBlock()}
<BlockOverlay id={id} /> <BlockOverlay id={id} />
{isSelected ? ( {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} ) : null}
</div> </div>
</NodeIdContext.Provider> </NodeIdContext.Provider>
@ -94,9 +95,11 @@ const NodeWithErrorBoundary = withErrorBoundary(NodeComponent, {
}); });
const UnSupportedBlock = () => { const UnSupportedBlock = () => {
const { t } = useTranslation();
return ( return (
<Alert severity='info' className='mb-2'> <Alert severity='info' className='mb-2'>
<p>The current version does not support this Block.</p> <p>{t('unSupportBlock')}</p>
</Alert> </Alert>
); );
}; };

View File

@ -18,7 +18,7 @@ const TextActionComponent = ({ container }: { container: HTMLDivElement }) => {
style={{ style={{
opacity: 0, 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) => { onMouseDown={(e) => {
// prevent toolbar from taking focus away from editor // prevent toolbar from taking focus away from editor
e.preventDefault(); e.preventDefault();

View File

@ -1,7 +1,6 @@
import IconButton from '@mui/material/IconButton';
import React, { useCallback, useEffect, useMemo } from 'react'; import React, { useCallback, useEffect, useMemo } from 'react';
import { TemporaryType, TextAction } from '$app/interfaces/document'; 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 { getFormatActiveThunk, toggleFormatThunk } from '$app_reducers/document/async-actions/format';
import { useAppDispatch, useAppSelector } from '$app/stores/store'; import { useAppDispatch, useAppSelector } from '$app/stores/store';
import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks'; import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks';
@ -18,18 +17,19 @@ import {
StrikethroughSOutlined, StrikethroughSOutlined,
} from '@mui/icons-material'; } from '@mui/icons-material';
import LinkIcon from '@mui/icons-material/AddLink'; import LinkIcon from '@mui/icons-material/AddLink';
import { useTranslation } from 'react-i18next';
export const iconSize = { width: 18, height: 18 }; export const iconSize = { width: 18, height: 18 };
const FormatButton = ({ format, icon }: { format: TextAction; icon: string }) => { const FormatButton = ({ format, icon }: { format: TextAction; icon: string }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { docId, controller } = useSubscribeDocument(); const { docId, controller } = useSubscribeDocument();
const { t } = useTranslation();
const focusId = useAppSelector((state) => state[RANGE_NAME][docId]?.focus?.id || ''); const focusId = useAppSelector((state) => state[RANGE_NAME][docId]?.focus?.id || '');
const { node: focusNode } = useSubscribeNode(focusId); const { node: focusNode } = useSubscribeNode(focusId);
const [isActive, setIsActive] = React.useState(false); 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 () => { const isFormatActive = useCallback(async () => {
if (!focusNode) return false; if (!focusNode) return false;
@ -82,15 +82,15 @@ const FormatButton = ({ format, icon }: { format: TextAction; icon: string }) =>
const formatTooltips: Record<string, string> = useMemo( const formatTooltips: Record<string, string> = useMemo(
() => ({ () => ({
[TextAction.Bold]: 'Bold', [TextAction.Bold]: t('toolbar.bold'),
[TextAction.Italic]: 'Italic', [TextAction.Italic]: t('toolbar.italic'),
[TextAction.Underline]: 'Underline', [TextAction.Underline]: t('toolbar.underline'),
[TextAction.Strikethrough]: 'Strike through', [TextAction.Strikethrough]: t('toolbar.strike'),
[TextAction.Code]: 'Mark as Code', [TextAction.Code]: t('toolbar.inlineCode'),
[TextAction.Link]: 'Add Link', [TextAction.Link]: t('toolbar.addLink'),
[TextAction.Equation]: 'Create equation', [TextAction.Equation]: t('document.plugins.mathEquation.addMathEquation'),
}), }),
[] [t]
); );
const formatClick = useCallback( const formatClick = useCallback(
@ -132,7 +132,7 @@ const FormatButton = ({ format, icon }: { format: TextAction; icon: string }) =>
marginRight: '0.25rem', marginRight: '0.25rem',
}} }}
/> />
<div className={'underline'}>Link</div> <div className={'underline'}>{t('toolbar.link')}</div>
</div> </div>
); );
case TextAction.Equation: case TextAction.Equation:
@ -140,14 +140,14 @@ const FormatButton = ({ format, icon }: { format: TextAction; icon: string }) =>
default: default:
return null; return null;
} }
}, [icon]); }, [icon, t]);
return ( return (
<MenuTooltip title={formatTooltips[format]}> <ToolbarTooltip title={formatTooltips[format]}>
<div className={`${color} cursor-pointer px-1 hover:text-fill-default`} onClick={() => formatClick(format)}> <div className={`${color} cursor-pointer px-1 hover:text-fill-default`} onClick={() => formatClick(format)}>
{formatIcon} {formatIcon}
</div> </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 React, { useCallback } from 'react';
import TurnIntoPopover from '$app/components/document/_shared/TurnInto'; import TurnIntoPopover from '$app/components/document/_shared/TurnInto';
import Button from '@mui/material/Button';
import ArrowDropDown from '@mui/icons-material/ArrowDropDown'; import ArrowDropDown from '@mui/icons-material/ArrowDropDown';
import MenuTooltip from './MenuTooltip';
import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks'; import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks';
import { useTranslation } from 'react-i18next';
import ToolbarTooltip from '../../_shared/ToolbarTooltip';
function TurnIntoSelect({ id }: { id: string }) { function TurnIntoSelect({ id }: { id: string }) {
const [anchorPosition, setAnchorPosition] = React.useState<{ const [anchorPosition, setAnchorPosition] = React.useState<{
@ -26,15 +26,16 @@ function TurnIntoSelect({ id }: { id: string }) {
}, []); }, []);
const open = Boolean(anchorPosition); const open = Boolean(anchorPosition);
const { t } = useTranslation();
return ( 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'> <div onClick={handleClick} className='flex cursor-pointer items-center px-2 text-sm text-fill-default'>
<span>{node.type}</span> <span>{node.type}</span>
<ArrowDropDown /> <ArrowDropDown />
</div> </div>
</MenuTooltip> </ToolbarTooltip>
<TurnIntoPopover <TurnIntoPopover
id={id} id={id}
open={open} open={open}

View File

@ -31,7 +31,7 @@ function TextActionMenuList() {
{groupItems.map( {groupItems.map(
(group, i: number) => (group, i: number) =>
group.length > 0 && ( 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) => ( {group.map((item) => (
<div key={item} className={'flex items-center'}> <div key={item} className={'flex items-center'}>
{renderNode(item)} {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 NodeChildren from '$app/components/document/Node/NodeChildren';
import { useKeyDown } from '$app/components/document/TextBlock/useKeyDown'; import { useKeyDown } from '$app/components/document/TextBlock/useKeyDown';
import { useSelection } from '$app/components/document/_shared/EditorHooks/useSelection'; import { useSelection } from '$app/components/document/_shared/EditorHooks/useSelection';
import { useTranslation } from 'react-i18next';
interface Props { interface Props {
node: NestedBlock; node: NestedBlock;
@ -15,10 +16,17 @@ function TextBlock({ node, childIds, placeholder }: Props) {
const { value, onChange } = useChange(node); const { value, onChange } = useChange(node);
const selectionProps = useSelection(node.id); const selectionProps = useSelection(node.id);
const { onKeyDown } = useKeyDown(node.id); const { onKeyDown } = useKeyDown(node.id);
const { t } = useTranslation();
return ( 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} /> <NodeChildren className='pl-[1.5em]' childIds={childIds} />
</> </>
); );

View File

@ -17,15 +17,18 @@ export function useBlockPopover({
onAfterOpen?: () => void; onAfterOpen?: () => void;
renderContent: ({ onClose }: { onClose: () => void }) => React.ReactNode; renderContent: ({ onClose }: { onClose: () => void }) => React.ReactNode;
}) { }) {
const anchorElRef = useRef<HTMLDivElement>(null); const anchorElRef = useRef<HTMLDivElement | null>(null);
const { docId } = useSubscribeDocument(); const { docId } = useSubscribeDocument();
const [anchorEl, setAnchorEl] = useState<HTMLDivElement | null>(null); const [anchorPosition, setAnchorPosition] = useState<{
const open = Boolean(anchorEl); top: number;
left: number;
}>();
const open = Boolean(anchorPosition);
const editing = useEditingState(id); const editing = useEditingState(id);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const closePopover = useCallback(() => { const closePopover = useCallback(() => {
setAnchorEl(null); setAnchorPosition(undefined);
dispatch( dispatch(
blockEditActions.setBlockEditState({ blockEditActions.setBlockEditState({
id: docId, id: docId,
@ -48,7 +51,14 @@ export function useBlockPopover({
}, [dispatch, docId, id]); }, [dispatch, docId, id]);
const openPopover = useCallback(() => { 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(); selectBlock();
onAfterOpen?.(); onAfterOpen?.();
}, [onAfterOpen, selectBlock]); }, [onAfterOpen, selectBlock]);
@ -68,21 +78,18 @@ export function useBlockPopover({
vertical: 'top', vertical: 'top',
horizontal: 'center', horizontal: 'center',
}} }}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center',
}}
onMouseDown={(e) => e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()}
onClose={closePopover} onClose={closePopover}
open={open} open={open}
anchorEl={anchorEl} anchorReference={'anchorPosition'}
anchorPosition={anchorPosition}
> >
{renderContent({ {renderContent({
onClose: closePopover, onClose: closePopover,
})} })}
</Popover> </Popover>
); );
}, [anchorEl, closePopover, open, renderContent]); }, [anchorPosition, closePopover, open, renderContent]);
useEffect(() => { useEffect(() => {
if (!anchorElRef.current) { if (!anchorElRef.current) {

View File

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

View File

@ -1,6 +1,5 @@
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { Portal, Snackbar } from '@mui/material'; import { Alert, Portal, Snackbar } from '@mui/material';
import { TransitionProps } from '@mui/material/transitions';
import Slide, { SlideProps } from '@mui/material/Slide'; import Slide, { SlideProps } from '@mui/material/Slide';
function SlideTransition(props: SlideProps) { function SlideTransition(props: SlideProps) {
@ -11,6 +10,7 @@ interface MessageProps {
message?: string; message?: string;
key?: string; key?: string;
duration?: number; duration?: number;
type?: 'success' | 'error';
} }
export function useMessage() { export function useMessage() {
const [state, setState] = useState<MessageProps>(); const [state, setState] = useState<MessageProps>();
@ -23,6 +23,7 @@ export function useMessage() {
const contentHolder = useMemo(() => { const contentHolder = useMemo(() => {
const open = !!state; const open = !!state;
return ( return (
<Portal> <Portal>
<Snackbar <Snackbar
@ -31,10 +32,19 @@ export function useMessage() {
open={open} open={open}
onClose={hide} onClose={hide}
TransitionProps={{ onExited: hide }} TransitionProps={{ onExited: hide }}
message={state?.message}
key={state?.key} key={state?.key}
TransitionComponent={SlideTransition} TransitionComponent={SlideTransition}
/> >
<>
{state?.type ? (
<Alert severity={state.type} sx={{ width: '100%' }}>
{state.message}
</Alert>
) : (
<span>{state?.message}</span>
)}
</>
</Snackbar>
</Portal> </Portal>
); );
}, [hide, state]); }, [hide, state]);

View File

@ -37,10 +37,10 @@ const TextLeaf = (props: TextLeafProps) => {
}; };
let newChildren = children; let newChildren = children;
if (leaf.code) { if (leaf.code && !leaf.temporary) {
newChildren = ( newChildren = (
<span <span
className={`bg-fill-selector text-text-title`} className={`bg-content-blue-50 text-text-title`}
style={{ style={{
fontSize: '85%', fontSize: '85%',
lineHeight: 'normal', lineHeight: 'normal',
@ -97,9 +97,9 @@ const TextLeaf = (props: TextLeafProps) => {
isCodeBlock && 'token', isCodeBlock && 'token',
leaf.prism_token && leaf.prism_token, leaf.prism_token && leaf.prism_token,
leaf.strikethrough && 'line-through', leaf.strikethrough && 'line-through',
leaf.selection_high_lighted && 'bg-fill-selector', leaf.selection_high_lighted && 'bg-content-blue-100',
leaf.link_selection_lighted && 'text-text-link-selector bg-fill-selector', leaf.link_selection_lighted && 'text-text-link-selector bg-content-blue-100',
leaf.code && 'inline-code', leaf.code && !leaf.temporary && 'inline-code',
leaf.bold && 'font-bold', leaf.bold && 'font-bold',
leaf.italic && 'italic', leaf.italic && 'italic',
leaf.underline && 'underline', leaf.underline && 'underline',
@ -114,7 +114,11 @@ const TextLeaf = (props: TextLeafProps) => {
} }
if (leaf.temporary) { if (leaf.temporary) {
newChildren = <TemporaryInput leaf={leaf}>{newChildren}</TemporaryInput>; newChildren = (
<TemporaryInput getSelection={getSelection} leaf={leaf}>
{newChildren}
</TemporaryInput>
);
} }
return ( return (

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import TextField from '@mui/material/TextField'; import TextField from '@mui/material/TextField';
import { CheckOutlined, FunctionsOutlined } from '@mui/icons-material'; import { CheckOutlined, FunctionsOutlined } from '@mui/icons-material';
import { Divider, IconButton, InputAdornment } from '@mui/material'; import { IconButton, InputAdornment } from '@mui/material';
function EquationEditContent({ function EquationEditContent({
value, value,

View File

@ -4,7 +4,7 @@ import KatexMath from '$app/components/document/_shared/KatexMath';
function TemporaryEquation({ latex }: { latex: string }) { function TemporaryEquation({ latex }: { latex: string }) {
return ( 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 ? ( {latex ? (
<KatexMath latex={latex} isInline /> <KatexMath latex={latex} isInline />
) : ( ) : (

View File

@ -1,33 +1,36 @@
import React, { useCallback, useEffect, useMemo, useRef } from 'react'; 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 TemporaryEquation from '$app/components/document/_shared/TemporaryInput/TemporaryEquation';
import { useSubscribeTemporary } from '$app/components/document/_shared/SubscribeTemporary.hooks'; import { useSubscribeTemporary } from '$app/components/document/_shared/SubscribeTemporary.hooks';
import { isOverlappingPrefix } from '$app/utils/document/temporary';
import { PopoverPosition } from '@mui/material'; import { PopoverPosition } from '@mui/material';
import { useAppDispatch } from '$app/stores/store'; import { useAppDispatch } from '$app/stores/store';
import { temporaryActions } from '$app_reducers/document/temporary_slice'; import { temporaryActions } from '$app_reducers/document/temporary_slice';
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks'; 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 temporaryState = useSubscribeTemporary();
const id = temporaryState?.id; const id = temporaryState?.id;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const ref = useRef<HTMLSpanElement>(null); const ref = useRef<HTMLSpanElement>(null);
const { docId } = useSubscribeDocument(); const { docId } = useSubscribeDocument();
const match = useMemo(() => { const match = useMemo(() => {
if (!ref.current) return false;
if (!leaf.text) return false; if (!leaf.text) return false;
if (!temporaryState) return false; if (!temporaryState) return false;
const { selectedText, type } = temporaryState; const { selectedText } = temporaryState;
const selection = getSelection(ref.current);
switch (type) { if (!selection) return false;
case TemporaryType.Equation: return leaf.text === selectedText || selection.index <= temporaryState.selection.index;
// when the leaf is split, the placeholder is not the same as the leaf text, }, [leaf.text, temporaryState, getSelection]);
// 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]);
const renderPlaceholder = useCallback(() => { const renderPlaceholder = useCallback(() => {
if (!temporaryState) return null; if (!temporaryState) return null;
@ -69,7 +72,7 @@ function TemporaryInput({ leaf, children }: { leaf: { text: string }; children:
return ( return (
<span ref={ref}> <span ref={ref}>
{match ? renderPlaceholder() : null} {match ? renderPlaceholder() : null}
<span className={'absolute opacity-0'}>{children}</span> <span className={`absolute opacity-0 ${match ? 'w-0' : ''}`}>{children}</span>
</span> </span>
); );
} }

View File

@ -5,6 +5,7 @@ import LanguageIcon from '@mui/icons-material/Language';
import CopyIcon from '@mui/icons-material/CopyAll'; import CopyIcon from '@mui/icons-material/CopyAll';
import { copyText } from '$app/utils/document/copy_paste'; import { copyText } from '$app/utils/document/copy_paste';
import { useMessage } from '$app/components/document/_shared/Message'; import { useMessage } from '$app/components/document/_shared/Message';
import { useTranslation } from 'react-i18next';
const iconSize = { const iconSize = {
width: '1rem', width: '1rem',
@ -28,6 +29,7 @@ function EditLinkToolbar({
editing: boolean; editing: boolean;
onEdit: () => void; onEdit: () => void;
}) { }) {
const { t } = useTranslation();
const { show, contentHolder } = useMessage(); const { show, contentHolder } = useMessage();
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
@ -70,9 +72,9 @@ function EditLinkToolbar({
onClick={async () => { onClick={async () => {
try { try {
await copyText(href); await copyText(href);
show({ message: 'Copied!', duration: 6000 }); show({ message: t('message.copy.success'), duration: 6000 });
} catch { } catch {
show({ message: 'Copy failed!', duration: 6000 }); show({ message: t('message.copy.fail'), duration: 6000 });
} }
}} }}
className={'mr-2 cursor-pointer'} className={'mr-2 cursor-pointer'}
@ -80,7 +82,7 @@ function EditLinkToolbar({
<CopyIcon sx={iconSize} /> <CopyIcon sx={iconSize} />
</div> </div>
<div onClick={onEdit} className={'cursor-pointer'}> <div onClick={onEdit} className={'cursor-pointer'}>
Edit {t('button.edit')}
</div> </div>
</div> </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 { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
import { useSubscribeLinkPopover } from '$app/components/document/_shared/SubscribeLinkPopover.hooks'; import { useSubscribeLinkPopover } from '$app/components/document/_shared/SubscribeLinkPopover.hooks';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import { useTranslation } from 'react-i18next';
function LinkEditPopover() { function LinkEditPopover() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { docId, controller } = useSubscribeDocument(); const { docId, controller } = useSubscribeDocument();
const { t } = useTranslation();
const popoverState = useSubscribeLinkPopover(); const popoverState = useSubscribeLinkPopover();
const { anchorPosition, id, selection, title = '', href = '', open = false } = popoverState; const { anchorPosition, id, selection, title = '', href = '', open = false } = popoverState;
@ -101,7 +102,7 @@ function LinkEditPopover() {
> >
<div className='flex flex-col p-3'> <div className='flex flex-col p-3'>
<EditLink <EditLink
text={'URL'} text={t('document.inlineLink.url.label')}
value={href} value={href}
onChange={(link) => { onChange={(link) => {
onChange({ onChange({
@ -111,7 +112,7 @@ function LinkEditPopover() {
}} }}
/> />
<EditLink <EditLink
text={'Link title'} text={t('document.inlineLink.title.label')}
value={title} value={title}
onChange={(text) => onChange={(text) =>
onChange({ onChange({
@ -123,7 +124,7 @@ function LinkEditPopover() {
<div className={'flex items-center justify-end'}> <div className={'flex items-center justify-end'}>
<Button onClick={onDone}> <Button onClick={onDone}>
<Done /> <Done />
Done {t('button.done')}
</Button> </Button>
</div> </div>
</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 { ImageSvg } from '$app/components/_shared/svg/ImageSvg';
import { CircularProgress } from '@mui/material'; import { CircularProgress } from '@mui/material';
import { writeImage } from '$app/utils/document/image'; 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 { export interface UploadImageProps {
onChange: (filePath: string) => void; onChange: (filePath: string) => void;
} }
function UploadImage({ onChange }: UploadImageProps) { function UploadImage({ onChange }: UploadImageProps) {
const { t } = useTranslation();
const message = useMessage();
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
const beforeUpload = useCallback((file: File) => { const beforeUpload = useCallback(
(file: File) => {
// check file size and type // check file size and type
const sizeMatched = file.size / 1024 / 1024 < 5; // 5MB const sizeMatched = file.size / 1024 / 1024 < 5; // 5MB
const typeMatched = /image\/(png|jpg|jpeg|gif)/.test(file.type); // png, jpg, jpeg, gif const typeMatched = /image\/(png|jpg|jpeg|gif)/.test(file.type); // png, jpg, jpeg, gif
if (!sizeMatched) {
setError(t('document.imageBlock.error.invalidImageSize'));
}
if (!typeMatched) {
setError(t('document.imageBlock.error.invalidImageFormat'));
}
return sizeMatched && typeMatched; return sizeMatched && typeMatched;
}, []); },
[t]
);
useEffect(() => {
if (!error) return;
message.show({
message: error,
duration: 3000,
type: 'error',
});
}, [error]);
const handleUpload = useCallback( const handleUpload = useCallback(
async (file: File) => { async (file: File) => {
if (!file) return; if (!file) return;
if (!beforeUpload(file)) { if (!beforeUpload(file)) {
setError('Image should be less than 5MB and in png, jpg, jpeg, gif format');
return; return;
} }
@ -38,10 +61,10 @@ function UploadImage({ onChange }: UploadImageProps) {
onChange(filePath); onChange(filePath);
} catch { } catch {
setLoading(false); setLoading(false);
setError('Upload failed'); setError(t('document.imageBlock.error.invalidImage'));
} }
}, },
[beforeUpload, onChange] [beforeUpload, onChange, t]
); );
const handleChange = useCallback( const handleChange = useCallback(
@ -88,7 +111,7 @@ function UploadImage({ onChange }: UploadImageProps) {
<input onChange={handleChange} ref={inputRef} type='file' className={'hidden'} accept={'image/*'} /> <input onChange={handleChange} ref={inputRef} type='file' className={'hidden'} accept={'image/*'} />
<div <div
className={ 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={{ style={{
borderColor: errorColor, borderColor: errorColor,
@ -101,7 +124,7 @@ function UploadImage({ onChange }: UploadImageProps) {
<div className={'h-8 w-8'}> <div className={'h-8 w-8'}>
<ImageSvg /> <ImageSvg />
</div> </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> </div>
{loading ? <CircularProgress /> : null} {loading ? <CircularProgress /> : null}
@ -112,8 +135,9 @@ function UploadImage({ onChange }: UploadImageProps) {
}} }}
className={`mt-5 text-sm text-text-caption`} 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> </div>
{message.contentHolder}
</div> </div>
); );
} }

View File

@ -2,7 +2,7 @@ import AddSvg from '../../_shared/svg/AddSvg';
export const GridAddView = () => { export const GridAddView = () => {
return ( 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'> <i className='mr-2 h-5 w-5'>
<AddSvg /> <AddSvg />
</i> </i>

View File

@ -18,15 +18,15 @@ export const GridTableHeader = ({ controller }: { controller: DatabaseController
return <GridTableHeaderItem field={field} controller={controller} key={i} />; 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 <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} onClick={onAddField}
> >
<i className='mr-2 h-5 w-5'> <i className='mr-2 h-5 w-5'>
<AddSvg /> <AddSvg />
</i> </i>
<span>{t('grid.newCol')}</span> <span>{t('grid.field.newProperty')}</span>
</div> </div>
</th> </th>
</tr> </tr>

View File

@ -61,9 +61,9 @@ export const GridTableHeaderItem = ({
}; };
return ( 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 <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} ref={ref}
onClick={() => { onClick={() => {
if (!ref.current) return; if (!ref.current) return;

View File

@ -21,7 +21,7 @@ export const GridTableRow = ({
<tr className='group'> <tr className='group'>
{cells.map((cell, cellIndex) => { {cells.map((cell, cellIndex) => {
return ( 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'> <div className='flex w-full items-center justify-end'>
<GridCell <GridCell
cellIdentifier={cell.cellIdentifier} cellIdentifier={cell.cellIdentifier}
@ -32,7 +32,7 @@ export const GridTableRow = ({
{cellIndex === 0 && ( {cellIndex === 0 && (
<div <div
onClick={() => onOpenRow(row)} 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 /> <FullView />
</div> </div>

View File

@ -5,7 +5,7 @@ export const FooterPanel = () => {
&copy; 2023 AppFlowy. <a href={'https://github.com/AppFlowy-IO/AppFlowy'}>GitHub</a> &copy; 2023 AppFlowy. <a href={'https://github.com/AppFlowy-IO/AppFlowy'}>GitHub</a>
</div> </div>
<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>
</div> </div>
); );

View File

@ -38,10 +38,16 @@ export const Breadcrumbs = ({ menuHidden, onShowMenuClick }: { menuHidden: boole
</button> </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 /> <ArrowLeftSvg />
</button> </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 /> <ArrowRightSvg />
</button> </button>
</div> </div>

View File

@ -3,7 +3,7 @@ import { PageOptions } from './PageOptions';
export const HeaderPanel = ({ menuHidden, onShowMenuClick }: { menuHidden: boolean; onShowMenuClick: () => void }) => { export const HeaderPanel = ({ menuHidden, onShowMenuClick }: { menuHidden: boolean; onShowMenuClick: () => void }) => {
return ( 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> <Breadcrumbs menuHidden={menuHidden} onShowMenuClick={onShowMenuClick}></Breadcrumbs>
<PageOptions></PageOptions> <PageOptions></PageOptions>
</div> </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'; import { useAuth } from '../../auth/auth.hooks';
export const usePageOptions = () => { export const usePageOptions = () => {
const [showOptionsPopup, setShowOptionsPopup] = useState(false); const [anchorEl, setAnchorEl] = useState<HTMLDivElement | HTMLButtonElement>();
const { logout } = useAuth();
const onOptionsClick = () => { const onOptionsClick = useCallback((el: HTMLDivElement | HTMLButtonElement) => {
setShowOptionsPopup(true); setAnchorEl(el);
}; }, []);
const onClose = () => { const onClose = () => {
setShowOptionsPopup(false); setAnchorEl(undefined);
};
const onSignOutClick = async () => {
await logout();
onClose();
}; };
return { return {
showOptionsPopup, anchorEl,
onOptionsClick, onOptionsClick,
onClose, onClose,
onSignOutClick,
}; };
}; };

View File

@ -1,30 +1,73 @@
import { Button } from '../../_shared/Button';
import { Details2Svg } from '../../_shared/svg/Details2Svg'; import { Details2Svg } from '../../_shared/svg/Details2Svg';
import { usePageOptions } from './PageOptions.hooks'; import { usePageOptions } from './PageOptions.hooks';
import { OptionsPopup } from './OptionsPopup'; import { Button, IconButton, List } from '@mui/material';
import { LanguageButton } from '$app/components/layout/HeaderPanel/LanguageButton'; 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 = () => { 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 ( return (
<> <>
<div className={'relative flex items-center gap-4'}> <div className={'relative flex items-center gap-4'}>
<Button size={'small'} onClick={() => console.log('share click')}> <Button
Share variant={'contained'}
onClick={(e) => {
const el = e.currentTarget;
setOption(PageOptionsEnum.Share);
onOptionsClick(el);
}}
>
{t('shareAction.buttonText')}
</Button> </Button>
<LanguageButton></LanguageButton> <IconButton
<button
id='option-button' id='option-button'
className={'relative h-8 w-8 rounded text-text-title hover:bg-fill-hover'} size={'small'}
onClick={onOptionsClick} 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> <Details2Svg></Details2Svg>
</button> </IconButton>
</div> </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 { useAppDispatch, useAppSelector } from '$app/stores/store';
import { IPage, pagesActions } from '$app_reducers/pages/slice'; import { IPage, pagesActions } from '$app_reducers/pages/slice';
import { ViewLayoutPB } from '@/services/backend'; 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 { ViewBackendService } from '$app/stores/effects/folder/view/view_bd_svc';
import { ViewObserver } from '$app/stores/effects/folder/view/view_observer'; import { ViewObserver } from '$app/stores/effects/folder/view/view_observer';
export enum NavItemOptions {
More = 'More',
NewPage = 'NewPage',
}
export const useNavItem = (page: IPage) => { export const useNavItem = (page: IPage) => {
const appDispatch = useAppDispatch(); const appDispatch = useAppDispatch();
const workspace = useAppSelector((state) => state.workspace); const workspace = useAppSelector((state) => state.workspace);
const currentLocation = useLocation(); const currentLocation = useLocation();
const [activePageId, setActivePageId] = useState<string>(''); const [activePageId, setActivePageId] = useState<string>('');
const pages = useAppSelector((state) => state.pages); 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(); 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 // backend
const service = new ViewBackendService(page.id); const service = new ViewBackendService(page.id);
const observer = new ViewObserver(page.id); const observer = new ViewObserver(page.id);
@ -68,14 +71,6 @@ export const useNavItem = (page: IPage) => {
setActivePageId(pageId); setActivePageId(pageId);
}, [currentLocation]); }, [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 // recursively get all unfolded child pages
const getChildCount: (startPage: IPage) => number = (startPage: IPage) => { const getChildCount: (startPage: IPage) => number = (startPage: IPage) => {
let count = 0; let count = 0;
@ -95,42 +90,21 @@ export const useNavItem = (page: IPage) => {
appDispatch(pagesActions.toggleShowPages({ id: page.id })); 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) => { const changePageTitle = async (newTitle: string) => {
await service.update({ name: newTitle }); await service.update({ name: newTitle });
appDispatch(pagesActions.renamePage({ id: page.id, newTitle })); appDispatch(pagesActions.renamePage({ id: page.id, newTitle }));
}; setAnchorEl(undefined);
const closeRenamePopup = () => {
setShowRenamePopup(false);
}; };
const deletePage = async () => { const deletePage = async () => {
closePopup();
await service.delete(); await service.delete();
appDispatch(pagesActions.deletePage({ id: page.id })); appDispatch(pagesActions.deletePage({ id: page.id }));
setAnchorEl(undefined);
}; };
const duplicatePage = async () => { const duplicatePage = async () => {
closePopup();
await service.duplicate(); await service.duplicate();
}; setAnchorEl(undefined);
const closePopup = () => {
setShowPageOptions(false);
setShowNewPageOptions(false);
}; };
const onPageClick = (eventPage: IPage) => { const onPageClick = (eventPage: IPage) => {
@ -151,7 +125,6 @@ export const useNavItem = (page: IPage) => {
}; };
const onAddNewPage = async (pageType: ViewLayoutPB) => { const onAddNewPage = async (pageType: ViewLayoutPB) => {
closePopup();
if (!workspace?.id) return; if (!workspace?.id) return;
let newPageName = ''; let newPageName = '';
@ -199,24 +172,15 @@ export const useNavItem = (page: IPage) => {
showPagesInside: false, showPagesInside: false,
}) })
); );
setAnchorEl(undefined);
navigate(`/page/${pageTypeRoute}/${newView.id}`); navigate(`/page/${pageTypeRoute}/${newView.id}`);
} }
}; };
return { return {
onUnfoldClick, onUnfoldClick,
onNewPageClick,
onPageOptionsClick,
startPageRename,
changePageTitle, changePageTitle,
closeRenamePopup,
closePopup,
showNewPageOptions,
showPageOptions,
showRenamePopup,
deletePage, deletePage,
duplicatePage, duplicatePage,
@ -225,7 +189,12 @@ export const useNavItem = (page: IPage) => {
onAddNewPage, onAddNewPage,
folderHeight,
activePageId, activePageId,
menuOpen,
anchorEl,
setAnchorEl,
menuOption,
selectedPage,
onClickMenuBtn,
}; };
}; };

View File

@ -1,90 +1,91 @@
import { Details2Svg } from '../../_shared/svg/Details2Svg'; import { Details2Svg } from '../../_shared/svg/Details2Svg';
import AddSvg from '../../_shared/svg/AddSvg'; import AddSvg from '../../_shared/svg/AddSvg';
import { NavItemOptionsPopup } from './NavItemOptionsPopup';
import { NewPagePopup } from './NewPagePopup';
import { IPage } from '$app_reducers/pages/slice'; import { IPage } from '$app_reducers/pages/slice';
import { Button } from '../../_shared/Button'; import { useMemo, useRef } from 'react';
import { RenamePopup } from './RenamePopup';
import { useEffect, useMemo, useRef, useState } from 'react';
import { DropDownShowSvg } from '../../_shared/svg/DropDownShowSvg'; import { DropDownShowSvg } from '../../_shared/svg/DropDownShowSvg';
import { ANIMATION_DURATION, PAGE_ITEM_HEIGHT } from '../../_shared/constants'; import { ANIMATION_DURATION } from '../../_shared/constants';
import { useNavItem } from '$app/components/layout/NavigationPanel/NavItem.hooks'; import { NavItemOptions, useNavItem } from '$app/components/layout/NavigationPanel/NavItem.hooks';
import { useAppSelector } from '$app/stores/store'; import { useAppSelector } from '$app/stores/store';
import { ViewLayoutPB } from '@/services/backend'; 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 }) => { export const NavItem = ({ page }: { page: IPage }) => {
const pages = useAppSelector((state) => state.pages); const pages = useAppSelector((state) => state.pages);
const { const {
onUnfoldClick, onUnfoldClick,
onNewPageClick,
onPageOptionsClick,
startPageRename,
changePageTitle, changePageTitle,
closeRenamePopup,
closePopup,
showNewPageOptions,
showPageOptions,
showRenamePopup,
deletePage, deletePage,
duplicatePage, duplicatePage,
onAddNewPage, onAddNewPage,
folderHeight,
activePageId, activePageId,
onPageClick, onPageClick,
onClickMenuBtn,
menuOpen,
menuOption,
setAnchorEl,
selectedPage,
anchorEl,
} = useNavItem(page); } = useNavItem(page);
const [popupY, setPopupY] = useState(0);
const el = useRef<HTMLDivElement>(null); const el = useRef<HTMLDivElement>(null);
useEffect(() => {
if (el.current) {
const { top } = el.current.getBoundingClientRect();
setPopupY(top);
}
}, [showPageOptions, showNewPageOptions, showRenamePopup]);
return ( return (
<>
<div ref={el}> <div ref={el}>
<div className={`transition-all`} style={{ transitionDuration: `${ANIMATION_DURATION}ms` }}>
<div className={`cursor-pointer px-1 py-1`}>
<div <div
className={`overflow-hidden transition-all`} className={`flex items-center justify-between rounded-lg px-2 py-1 hover:bg-fill-list-hover ${
style={{ height: folderHeight, transitionDuration: `${ANIMATION_DURATION}ms` }} activePageId === page.id ? 'bg-fill-list-hover' : ''
>
<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'}> <div className={'flex h-full min-w-0 flex-1 items-center'}>
<button <button
onClick={() => onUnfoldClick()} onClick={() => onUnfoldClick()}
className={`mr-2 h-5 w-5 transition-transform duration-200 ${page.showPagesInside && 'rotate-180'}`} className={`mr-2 h-5 w-5 transition-transform duration-200 ${
page.showPagesInside ? 'rotate-180' : ''
}`}
> >
<DropDownShowSvg></DropDownShowSvg> <DropDownShowSvg></DropDownShowSvg>
</button> </button>
<div onClick={() => onPageClick(page)} className={'mr-1 flex h-full min-w-0 items-center text-left'}> <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> <span className={'w-[100%] overflow-hidden overflow-ellipsis whitespace-nowrap'}>{page.title}</span>
</div> </div>
</div> </div>
<div className={'flex items-center'}> <div className={'flex items-center'}>
<Button size={'box-small-transparent'} onClick={() => onPageOptionsClick()}> <IconButton
className={'h-6 w-6'}
size={'small'}
onClick={(e) => {
setAnchorEl(e.currentTarget);
onClickMenuBtn(page, NavItemOptions.More);
}}
>
<Details2Svg></Details2Svg> <Details2Svg></Details2Svg>
</Button> </IconButton>
<Button size={'box-small-transparent'} onClick={() => onNewPageClick()}> <IconButton
className={'h-6 w-6'}
size={'small'}
onClick={(e) => {
setAnchorEl(e.currentTarget);
onClickMenuBtn(page, NavItemOptions.NewPage);
}}
>
<AddSvg></AddSvg> <AddSvg></AddSvg>
</Button> </IconButton>
</div> </div>
</div> </div>
</div> </div>
<div className={'pl-4'}> <div className={`${page.showPagesInside ? '' : 'hidden'} pl-4`}>
{useMemo(() => pages.filter((insidePage) => insidePage.parentPageId === page.id), [pages, page]).map( {useMemo(() => pages.filter((insidePage) => insidePage.parentPageId === page.id), [pages, page]).map(
(insidePage, insideIndex) => ( (insidePage, insideIndex) => (
<NavItem key={insideIndex} page={insidePage}></NavItem> <NavItem key={insideIndex} page={insidePage}></NavItem>
@ -92,32 +93,32 @@ export const NavItem = ({ page }: { page: IPage }) => {
)} )}
</div> </div>
</div> </div>
{showPageOptions && ( </div>
<NavItemOptionsPopup <Popover
onRenameClick={() => startPageRename()} 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()} onDeleteClick={() => deletePage()}
onDuplicateClick={() => duplicatePage()} onDuplicateClick={() => duplicatePage()}
onClose={() => closePopup()} />
top={popupY - 124 + 58}
></NavItemOptionsPopup>
)} )}
{showNewPageOptions && ( {menuOption === NavItemOptions.NewPage && (
<NewPagePopup <NewPageMenu
onDocumentClick={() => onAddNewPage(ViewLayoutPB.Document)} onDocumentClick={() => onAddNewPage(ViewLayoutPB.Document)}
onBoardClick={() => onAddNewPage(ViewLayoutPB.Board)} onBoardClick={() => onAddNewPage(ViewLayoutPB.Board)}
onGridClick={() => onAddNewPage(ViewLayoutPB.Grid)} onGridClick={() => onAddNewPage(ViewLayoutPB.Grid)}
onClose={() => closePopup()} />
top={popupY - 124 + 58}
></NewPagePopup>
)} )}
{showRenamePopup && ( </List>
<RenamePopup </Popover>
value={page.title} </>
onChange={(newTitle) => changePageTitle(newTitle)}
onClose={closeRenamePopup}
top={popupY - 124 + 40}
></RenamePopup>
)}
</div>
); );
}; };

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`, left: `${menuHidden ? -width : 0}px`,
}} }}
> >
<div className={'flex flex-col'}>
<AppLogo iconToShow={'hide'} onHideMenuClick={onHideMenuClick}></AppLogo> <AppLogo iconToShow={'hide'} onHideMenuClick={onHideMenuClick}></AppLogo>
<WorkspaceUser></WorkspaceUser> <WorkspaceUser></WorkspaceUser>
<div className={'relative flex flex-1 flex-col'}> <div className={'relative flex flex-1 flex-col'}>
<div <div className={'flex h-[100%] flex-col overflow-auto px-2'} ref={el}>
className={'flex flex-col overflow-auto px-2'}
style={{
maxHeight: 'calc(100vh - 350px)',
}}
ref={el}
>
<WorkspaceApps pages={pages.filter((p) => p.parentPageId === workspace.id)} /> <WorkspaceApps pages={pages.filter((p) => p.parentPageId === workspace.id)} />
</div> </div>
</div> </div>
</div>
<div className={'flex max-h-[215px] flex-col'}> <div className={'flex max-h-[240px] flex-col'}>
<div className={'border-b border-line-border px-2 pb-4'}> <div className={'border-b border-line-divider px-2 pb-4'}>
{/*<PluginsButton></PluginsButton>*/} {/*<PluginsButton></PluginsButton>*/}
{/*<DesignSpec></DesignSpec>*/} {/*<DesignSpec></DesignSpec>*/}
@ -105,7 +97,7 @@ export const TestBackendButton = () => {
return ( return (
<button <button
onClick={() => navigate('/page/api-test')} 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 API Test
</button> </button>
@ -118,7 +110,7 @@ export const DesignSpec = () => {
return ( return (
<button <button
onClick={() => navigate('page/colors')} 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 Color Palette
</button> </button>
@ -131,7 +123,7 @@ export const AllIcons = () => {
return ( return (
<button <button
onClick={() => navigate('page/all-icons')} 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 All Icons
</button> </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(); void onNewRootView();
scrollDown(); 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={'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> <AddSvg></AddSvg>
</div> </div>
</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 = () => { export const TrashButton = () => {
return ( 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]'}> <span className={'h-[23px] w-[23px]'}>
<TrashSvg /> <TrashSvg />
</span> </span>

View File

@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo } from 'react';
import Select from '@mui/material/Select'; import Select from '@mui/material/Select';
import { Theme, ThemeMode, UserSetting } from '$app/interfaces'; import { Theme, ThemeMode, UserSetting } from '$app/interfaces';
import MenuItem from '@mui/material/MenuItem'; import MenuItem from '@mui/material/MenuItem';
import { useTranslation } from 'react-i18next';
function AppearanceSetting({ function AppearanceSetting({
theme = Theme.Default, theme = Theme.Default,
@ -12,6 +13,8 @@ function AppearanceSetting({
themeMode?: ThemeMode; themeMode?: ThemeMode;
onChange: (setting: UserSetting) => void; onChange: (setting: UserSetting) => void;
}) { }) {
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
const html = document.documentElement; const html = document.documentElement;
@ -23,14 +26,14 @@ function AppearanceSetting({
() => [ () => [
{ {
value: ThemeMode.Light, value: ThemeMode.Light,
content: 'Light', content: t('settings.appearance.themeMode.light'),
}, },
{ {
value: ThemeMode.Dark, value: ThemeMode.Dark,
content: 'Dark', content: t('settings.appearance.themeMode.dark'),
}, },
], ],
[] [t]
); );
const themeOptions = useMemo( const themeOptions = useMemo(
@ -96,7 +99,7 @@ function AppearanceSetting({
{renderSelect([ {renderSelect([
{ {
options: themeModeOptions, options: themeModeOptions,
label: 'Theme Mode', label: t('settings.appearance.themeMode.label'),
value: themeMode, value: themeMode,
onChange: (newValue) => { onChange: (newValue) => {
onChange({ onChange({
@ -106,7 +109,7 @@ function AppearanceSetting({
}, },
{ {
options: themeOptions, options: themeOptions,
label: 'Theme', label: t('settings.appearance.theme'),
value: theme, value: theme,
onChange: (newValue) => { onChange: (newValue) => {
onChange({ onChange({

View File

@ -1,7 +1,74 @@
import React from 'react'; 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() { const languages = [
return <div></div>; {
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; export default LanguageSetting;

View File

@ -1,6 +1,7 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import LanguageIcon from '@mui/icons-material/Language'; import LanguageIcon from '@mui/icons-material/Language';
import PaletteOutlined from '@mui/icons-material/PaletteOutlined'; import PaletteOutlined from '@mui/icons-material/PaletteOutlined';
import { useTranslation } from 'react-i18next';
export enum MenuItem { export enum MenuItem {
Appearance = 'Appearance', Appearance = 'Appearance',
@ -8,23 +9,25 @@ export enum MenuItem {
} }
function UserSettingMenu({ selected, onSelect }: { onSelect: (selected: MenuItem) => void; selected: MenuItem }) { function UserSettingMenu({ selected, onSelect }: { onSelect: (selected: MenuItem) => void; selected: MenuItem }) {
const { t } = useTranslation();
const options = useMemo(() => { const options = useMemo(() => {
return [ return [
{ {
label: 'Appearance', label: t('settings.menu.appearance'),
value: MenuItem.Appearance, value: MenuItem.Appearance,
icon: <PaletteOutlined />, icon: <PaletteOutlined />,
}, },
{ {
label: 'Language', label: t('settings.menu.language'),
value: MenuItem.Language, value: MenuItem.Language,
icon: <LanguageIcon />, icon: <LanguageIcon />,
}, },
]; ];
}, []); }, [t]);
return ( 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) => { {options.map((option) => {
return ( return (
<div <div
@ -33,7 +36,7 @@ function UserSettingMenu({ selected, onSelect }: { onSelect: (selected: MenuItem
onSelect(option.value); 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 ${ 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> <div className={'mr-2'}>{option.icon}</div>

View File

@ -14,7 +14,7 @@ function UserSettingPanel({
userSettingState?: UserSetting; userSettingState?: UserSetting;
onChange: (setting: Partial<UserSetting>) => void; onChange: (setting: Partial<UserSetting>) => void;
}) { }) {
const { theme, themeMode } = userSettingState; const { theme, themeMode, language } = userSettingState;
const options = useMemo(() => { const options = useMemo(() => {
return [ return [
@ -24,14 +24,14 @@ function UserSettingPanel({
}, },
{ {
value: MenuItem.Language, 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); 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; 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 { currentUserActions } from '$app_reducers/current-user/slice';
import { useUserSettingControllerContext } from '$app/components/_shared/app-hooks/useUserSettingControllerContext'; import { useUserSettingControllerContext } from '$app/components/_shared/app-hooks/useUserSettingControllerContext';
import { ThemeModePB } from '@/services/backend'; import { ThemeModePB } from '@/services/backend';
import { useTranslation } from 'react-i18next';
const SlideTransition = React.forwardRef((props: SlideProps, ref) => { const SlideTransition = React.forwardRef((props: SlideProps, ref) => {
return <Slide {...props} direction='up' ref={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 userSettingState = useAppSelector((state) => state.currentUser.userSetting);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const userSettingController = useUserSettingControllerContext(); const userSettingController = useUserSettingControllerContext();
const { t } = useTranslation();
const [selected, setSelected] = useState<MenuItem>(MenuItem.Appearance); const [selected, setSelected] = useState<MenuItem>(MenuItem.Appearance);
const handleChange = useCallback( const handleChange = useCallback(
(setting: Partial<UserSetting>) => { (setting: Partial<UserSetting>) => {
@ -27,9 +28,15 @@ function UserSettings({ open, onClose }: { open: boolean; onClose: () => void })
dispatch(currentUserActions.setUserSetting(newSetting)); dispatch(currentUserActions.setUserSetting(newSetting));
if (userSettingController) { if (userSettingController) {
const language = newSetting.language || 'en';
userSettingController.setAppearanceSetting({ userSettingController.setAppearanceSetting({
theme: newSetting.theme || Theme.Default, 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 keepMounted
onClose={onClose} onClose={onClose}
> >
<DialogTitle>{'Settings'}</DialogTitle> <DialogTitle>{t('settings.title')}</DialogTitle>
<DialogContent className={'flex w-[540px]'}> <DialogContent className={'flex w-[540px]'}>
<UserSettingMenu <UserSettingMenu
onSelect={(selected) => { onSelect={(selected) => {

View File

@ -29,7 +29,7 @@ export const WorkspaceUser = () => {
<PersonOutline /> <PersonOutline />
</Avatar> </Avatar>
<span className={'ml-2'}>{currentUser.displayName}</span> <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 /> <ArrowDropDown />
</button> </button>
</div> </div>

View File

@ -5,24 +5,24 @@ export const ColorPalette = () => {
<h2 className={'mb-4'}>Main</h2> <h2 className={'mb-4'}>Main</h2>
<div className={'mb-8 flex flex-wrap items-center'}> <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-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-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-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-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-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-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 title={'main-success'} className={'m-2 h-[100px] w-[100px] bg-function-success'}></div>
</div> </div>
<h2 className={'mb-4'}>Tint</h2> <h2 className={'mb-4'}>Tint</h2>
<div className={'mb-8 flex flex-wrap items-center text-content-onfill'}> <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-1'}></div> <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-2'}></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-3'}></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-4'}></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-5'}></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-6'}></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-7'}></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-8'}></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-9'}></div> <div title={'tint-9'} className={'m-2 h-[100px] w-[100px] bg-tint-pink'}></div>
</div> </div>
<h2 className={'mb-4'}>Shades</h2> <h2 className={'mb-4'}>Shades</h2>
<div className={'mb-8 flex flex-wrap items-center'}> <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