diff --git a/package-lock.json b/package-lock.json index 89d4d722..92d91375 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cinny", - "version": "4.10.0", + "version": "4.9.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cinny", - "version": "4.10.0", + "version": "4.9.1", "license": "AGPL-3.0-only", "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "1.1.6", @@ -16,6 +16,7 @@ "@tanstack/react-query": "5.24.1", "@tanstack/react-query-devtools": "5.24.1", "@tanstack/react-virtual": "3.2.0", + "@tippyjs/react": "4.2.6", "@vanilla-extract/css": "1.9.3", "@vanilla-extract/recipes": "0.3.0", "@vanilla-extract/vite-plugin": "3.7.1", @@ -31,8 +32,10 @@ "emojibase": "15.3.1", "emojibase-data": "15.3.2", "file-saver": "2.0.5", + "flux": "4.0.3", "focus-trap-react": "10.0.2", "folds": "2.2.0", + "formik": "2.4.6", "html-dom-parser": "4.0.0", "html-react-parser": "4.2.0", "i18next": "23.12.2", @@ -47,14 +50,17 @@ "millify": "6.1.0", "pdfjs-dist": "4.2.67", "prismjs": "1.30.0", + "prop-types": "15.8.1", "react": "18.2.0", "react-aria": "3.29.1", + "react-autosize-textarea": "7.1.0", "react-blurhash": "0.2.0", "react-colorful": "5.6.1", "react-dom": "18.2.0", "react-error-boundary": "4.0.13", "react-google-recaptcha": "2.1.0", "react-i18next": "15.0.0", + "react-modal": "3.16.1", "react-range": "1.8.14", "react-router-dom": "6.20.0", "sanitize-html": "2.12.1", @@ -62,6 +68,7 @@ "slate-dom": "0.112.2", "slate-history": "0.110.3", "slate-react": "0.112.1", + "tippy.js": "6.3.7", "ua-parser-js": "1.0.35" }, "devDependencies": { @@ -90,6 +97,7 @@ "eslint-plugin-react": "7.31.11", "eslint-plugin-react-hooks": "4.6.0", "prettier": "2.8.1", + "sass": "1.56.2", "typescript": "4.9.4", "vite": "5.4.19", "vite-plugin-pwa": "0.20.5", @@ -2305,6 +2313,15 @@ "node": ">= 8" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@react-aria/breadcrumbs": { "version": "3.5.20", "resolved": "https://registry.npmjs.org/@react-aria/breadcrumbs/-/breadcrumbs-3.5.20.tgz", @@ -4507,6 +4524,18 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tippyjs/react": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@tippyjs/react/-/react-4.2.6.tgz", + "integrity": "sha512-91RicDR+H7oDSyPycI13q3b7o4O60wa2oRbjlz2fyRLmHImc4vyDwuUP8NtZaN0VARJY5hybvDYrFzhY9+Lbyw==", + "dependencies": { + "tippy.js": "^6.3.1" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -4572,6 +4601,15 @@ "integrity": "sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==", "dev": true }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz", + "integrity": "sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/is-hotkey": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/@types/is-hotkey/-/is-hotkey-0.1.10.tgz", @@ -4605,14 +4643,12 @@ "node_modules/@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", - "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", - "dev": true + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==" }, "node_modules/@types/react": { "version": "18.2.39", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz", "integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==", - "dev": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -4661,8 +4697,7 @@ "node_modules/@types/scheduler": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==", - "dev": true + "integrity": "sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==" }, "node_modules/@types/semver": { "version": "7.5.8", @@ -5285,6 +5320,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + }, "node_modules/ast-types-flow": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", @@ -5306,6 +5346,11 @@ "node": ">= 4.0.0" } }, + "node_modules/autosize": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/autosize/-/autosize-4.0.4.tgz", + "integrity": "sha512-5yxLQ22O0fCRGoxGfeLSNt3J8LB1v+umtpMnPW6XjkTWXKoN0AmXAIhelJcDtFT/Y/wYWmfE+oqU10Q0b8FhaQ==" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -5785,6 +5830,11 @@ "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", "license": "MIT" }, + "node_modules/computed-style": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/computed-style/-/computed-style-0.1.4.tgz", + "integrity": "sha512-WpAmaKbMNmS3OProfHIdJiNleNJdgUrJfbKArXua28QF7+0CoZjlLn0lp6vlc+dl5r2/X9GQiQRQQU4BzSa69w==" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5846,6 +5896,14 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/cross-fetch": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", + "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -6961,6 +7019,11 @@ "node": ">=0.8.x" } }, + "node_modules/exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -7032,6 +7095,33 @@ "reusify": "^1.0.4" } }, + "node_modules/fbemitter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fbemitter/-/fbemitter-3.0.0.tgz", + "integrity": "sha512-KWKaceCwKQU0+HPoop6gn4eOHk50bBv/VxjJtGMfwmJt3D29JpN4H4eisCtIPA+a8GVBam+ldMMpMjJUvpDyHw==", + "dependencies": { + "fbjs": "^3.0.0" + } + }, + "node_modules/fbjs": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.5.tgz", + "integrity": "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==", + "dependencies": { + "cross-fetch": "^3.1.5", + "fbjs-css-vars": "^1.0.0", + "loose-envify": "^1.0.0", + "object-assign": "^4.1.0", + "promise": "^7.1.1", + "setimmediate": "^1.0.5", + "ua-parser-js": "^1.0.35" + } + }, + "node_modules/fbjs-css-vars": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz", + "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==" + }, "node_modules/fdir": { "version": "6.4.3", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", @@ -7140,6 +7230,18 @@ "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", "dev": true }, + "node_modules/flux": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/flux/-/flux-4.0.3.tgz", + "integrity": "sha512-yKAbrp7JhZhj6uiT1FTuVMlIAT1J4jqEyBpFApi1kxpGZCvacMVc/t1pMQyotqHhAgvoE3bNvAykhCo2CLjnYw==", + "dependencies": { + "fbemitter": "^3.0.0", + "fbjs": "^3.0.1" + }, + "peerDependencies": { + "react": "^15.0.2 || ^16.0.0 || ^17.0.0" + } + }, "node_modules/focus-trap": { "version": "7.6.4", "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.4.tgz", @@ -7184,6 +7286,38 @@ "is-callable": "^1.1.3" } }, + "node_modules/formik": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/formik/-/formik-2.4.6.tgz", + "integrity": "sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g==", + "funding": [ + { + "type": "individual", + "url": "https://opencollective.com/formik" + } + ], + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.1", + "deepmerge": "^2.1.1", + "hoist-non-react-statics": "^3.3.0", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "react-fast-compare": "^2.0.1", + "tiny-warning": "^1.0.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/formik/node_modules/deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fs-extra": { "version": "11.3.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", @@ -7762,6 +7896,12 @@ "url": "https://opencollective.com/immer" } }, + "node_modules/immutable": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", + "dev": true + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -8496,6 +8636,17 @@ "node": ">=10" } }, + "node_modules/line-height": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/line-height/-/line-height-0.3.1.tgz", + "integrity": "sha512-YExecgqPwnp5gplD2+Y8e8A5+jKpr25+DzMbFdI1/1UAr0FJrTFv4VkHLf8/6B590i1wUPJWMKKldkd/bdQ//w==", + "dependencies": { + "computed-style": "~0.1.3" + }, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/linkify-react": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/linkify-react/-/linkify-react-4.1.3.tgz", @@ -8529,6 +8680,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -9398,6 +9554,14 @@ "node": ">=6" } }, + "node_modules/promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dependencies": { + "asap": "~2.0.3" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -9521,6 +9685,20 @@ "react": ">=16.4.1" } }, + "node_modules/react-autosize-textarea": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/react-autosize-textarea/-/react-autosize-textarea-7.1.0.tgz", + "integrity": "sha512-BHpjCDkuOlllZn3nLazY2F8oYO1tS2jHnWhcjTWQdcKiiMU6gHLNt/fzmqMSyerR0eTdKtfSIqtSeTtghNwS+g==", + "dependencies": { + "autosize": "^4.0.2", + "line-height": "^0.3.1", + "prop-types": "^15.5.6" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0 || ^16.0.0", + "react-dom": "^0.14.0 || ^15.0.0 || ^16.0.0" + } + }, "node_modules/react-blurhash": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/react-blurhash/-/react-blurhash-0.2.0.tgz", @@ -9563,6 +9741,11 @@ "react": ">=16.13.1" } }, + "node_modules/react-fast-compare": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", + "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" + }, "node_modules/react-google-recaptcha": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-2.1.0.tgz", @@ -9601,6 +9784,29 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "node_modules/react-modal": { + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.1.tgz", + "integrity": "sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==", + "dependencies": { + "exenv": "^1.2.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.0", + "warning": "^4.0.3" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18", + "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18" + } + }, "node_modules/react-property": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.0.tgz", @@ -10059,6 +10265,23 @@ "postcss": "^8.3.11" } }, + "node_modules/sass": { + "version": "1.56.2", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.56.2.tgz", + "integrity": "sha512-ciEJhnyCRwzlBCB+h5cCPM6ie/6f8HrhZMQOf5vlU60Y1bI1rx5Zb0vlDZvaycHsg/MqFfF1Eq2eokAa32iw8w==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -10158,6 +10381,11 @@ "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -10754,6 +10982,14 @@ "node": ">=12.0.0" } }, + "node_modules/tippy.js": { + "version": "6.3.7", + "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz", + "integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==", + "dependencies": { + "@popperjs/core": "^2.9.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -11670,6 +11906,14 @@ "node": ">=0.10.0" } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/package.json b/package.json index f0fa46c7..3987dbfb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cinny", - "version": "4.10.0", + "version": "4.9.1", "description": "Yet another matrix client", "main": "index.js", "type": "module", @@ -27,6 +27,7 @@ "@tanstack/react-query": "5.24.1", "@tanstack/react-query-devtools": "5.24.1", "@tanstack/react-virtual": "3.2.0", + "@tippyjs/react": "4.2.6", "@vanilla-extract/css": "1.9.3", "@vanilla-extract/recipes": "0.3.0", "@vanilla-extract/vite-plugin": "3.7.1", @@ -42,8 +43,10 @@ "emojibase": "15.3.1", "emojibase-data": "15.3.2", "file-saver": "2.0.5", + "flux": "4.0.3", "focus-trap-react": "10.0.2", "folds": "2.2.0", + "formik": "2.4.6", "html-dom-parser": "4.0.0", "html-react-parser": "4.2.0", "i18next": "23.12.2", @@ -58,14 +61,17 @@ "millify": "6.1.0", "pdfjs-dist": "4.2.67", "prismjs": "1.30.0", + "prop-types": "15.8.1", "react": "18.2.0", "react-aria": "3.29.1", + "react-autosize-textarea": "7.1.0", "react-blurhash": "0.2.0", "react-colorful": "5.6.1", "react-dom": "18.2.0", "react-error-boundary": "4.0.13", "react-google-recaptcha": "2.1.0", "react-i18next": "15.0.0", + "react-modal": "3.16.1", "react-range": "1.8.14", "react-router-dom": "6.20.0", "sanitize-html": "2.12.1", @@ -73,6 +79,7 @@ "slate-dom": "0.112.2", "slate-history": "0.110.3", "slate-react": "0.112.1", + "tippy.js": "6.3.7", "ua-parser-js": "1.0.35" }, "devDependencies": { @@ -101,6 +108,7 @@ "eslint-plugin-react": "7.31.11", "eslint-plugin-react-hooks": "4.6.0", "prettier": "2.8.1", + "sass": "1.56.2", "typescript": "4.9.4", "vite": "5.4.19", "vite-plugin-pwa": "0.20.5", diff --git a/public/res/ic/filled/category.svg b/public/res/ic/filled/category.svg new file mode 100644 index 00000000..87b2588d --- /dev/null +++ b/public/res/ic/filled/category.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + diff --git a/public/res/ic/filled/pin.svg b/public/res/ic/filled/pin.svg new file mode 100644 index 00000000..6a701474 --- /dev/null +++ b/public/res/ic/filled/pin.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/public/res/ic/filled/star.svg b/public/res/ic/filled/star.svg new file mode 100644 index 00000000..378c891e --- /dev/null +++ b/public/res/ic/filled/star.svg @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/public/res/ic/outlined/add-pin.svg b/public/res/ic/outlined/add-pin.svg new file mode 100644 index 00000000..9634bede --- /dev/null +++ b/public/res/ic/outlined/add-pin.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/add-user.svg b/public/res/ic/outlined/add-user.svg new file mode 100644 index 00000000..c3803d80 --- /dev/null +++ b/public/res/ic/outlined/add-user.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/public/res/ic/outlined/ball.svg b/public/res/ic/outlined/ball.svg new file mode 100644 index 00000000..d4b89ff5 --- /dev/null +++ b/public/res/ic/outlined/ball.svg @@ -0,0 +1,12 @@ + + + + + + diff --git a/public/res/ic/outlined/bell-off.svg b/public/res/ic/outlined/bell-off.svg new file mode 100644 index 00000000..79ce8a33 --- /dev/null +++ b/public/res/ic/outlined/bell-off.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/public/res/ic/outlined/bell-ping.svg b/public/res/ic/outlined/bell-ping.svg new file mode 100644 index 00000000..3431bea1 --- /dev/null +++ b/public/res/ic/outlined/bell-ping.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/public/res/ic/outlined/bell-ring.svg b/public/res/ic/outlined/bell-ring.svg new file mode 100644 index 00000000..57fc2679 --- /dev/null +++ b/public/res/ic/outlined/bell-ring.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/public/res/ic/outlined/bell.svg b/public/res/ic/outlined/bell.svg new file mode 100644 index 00000000..43d470b5 --- /dev/null +++ b/public/res/ic/outlined/bell.svg @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/bin.svg b/public/res/ic/outlined/bin.svg new file mode 100644 index 00000000..984be625 --- /dev/null +++ b/public/res/ic/outlined/bin.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + diff --git a/public/res/ic/outlined/bulb.svg b/public/res/ic/outlined/bulb.svg new file mode 100644 index 00000000..00e80886 --- /dev/null +++ b/public/res/ic/outlined/bulb.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/public/res/ic/outlined/category.svg b/public/res/ic/outlined/category.svg new file mode 100644 index 00000000..c7c33b38 --- /dev/null +++ b/public/res/ic/outlined/category.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/public/res/ic/outlined/check.svg b/public/res/ic/outlined/check.svg new file mode 100644 index 00000000..72a18327 --- /dev/null +++ b/public/res/ic/outlined/check.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/public/res/ic/outlined/chevron-bottom.svg b/public/res/ic/outlined/chevron-bottom.svg new file mode 100644 index 00000000..5562b7aa --- /dev/null +++ b/public/res/ic/outlined/chevron-bottom.svg @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/public/res/ic/outlined/chevron-left.svg b/public/res/ic/outlined/chevron-left.svg new file mode 100644 index 00000000..ba9e12cc --- /dev/null +++ b/public/res/ic/outlined/chevron-left.svg @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/public/res/ic/outlined/chevron-right.svg b/public/res/ic/outlined/chevron-right.svg new file mode 100644 index 00000000..7f6a806e --- /dev/null +++ b/public/res/ic/outlined/chevron-right.svg @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/public/res/ic/outlined/chevron-top.svg b/public/res/ic/outlined/chevron-top.svg new file mode 100644 index 00000000..f5948fe9 --- /dev/null +++ b/public/res/ic/outlined/chevron-top.svg @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/public/res/ic/outlined/circle-plus.svg b/public/res/ic/outlined/circle-plus.svg new file mode 100644 index 00000000..41690a08 --- /dev/null +++ b/public/res/ic/outlined/circle-plus.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/cmd.svg b/public/res/ic/outlined/cmd.svg new file mode 100644 index 00000000..75ae0d98 --- /dev/null +++ b/public/res/ic/outlined/cmd.svg @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/public/res/ic/outlined/coin.svg b/public/res/ic/outlined/coin.svg new file mode 100644 index 00000000..025424e8 --- /dev/null +++ b/public/res/ic/outlined/coin.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/cross.svg b/public/res/ic/outlined/cross.svg new file mode 100644 index 00000000..0acda884 --- /dev/null +++ b/public/res/ic/outlined/cross.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/public/res/ic/outlined/cup.svg b/public/res/ic/outlined/cup.svg new file mode 100644 index 00000000..8921e2c9 --- /dev/null +++ b/public/res/ic/outlined/cup.svg @@ -0,0 +1,9 @@ + + + + + + diff --git a/public/res/ic/outlined/dog.svg b/public/res/ic/outlined/dog.svg new file mode 100644 index 00000000..3b252956 --- /dev/null +++ b/public/res/ic/outlined/dog.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/public/res/ic/outlined/download.svg b/public/res/ic/outlined/download.svg new file mode 100644 index 00000000..677014f3 --- /dev/null +++ b/public/res/ic/outlined/download.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/public/res/ic/outlined/emoji-add.svg b/public/res/ic/outlined/emoji-add.svg new file mode 100644 index 00000000..c4cacef2 --- /dev/null +++ b/public/res/ic/outlined/emoji-add.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/public/res/ic/outlined/emoji.svg b/public/res/ic/outlined/emoji.svg new file mode 100644 index 00000000..0daac879 --- /dev/null +++ b/public/res/ic/outlined/emoji.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/public/res/ic/outlined/explore.svg b/public/res/ic/outlined/explore.svg new file mode 100644 index 00000000..7cc2a479 --- /dev/null +++ b/public/res/ic/outlined/explore.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/external.svg b/public/res/ic/outlined/external.svg new file mode 100644 index 00000000..adade1bd --- /dev/null +++ b/public/res/ic/outlined/external.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/res/ic/outlined/eye-blind.svg b/public/res/ic/outlined/eye-blind.svg new file mode 100644 index 00000000..fbc8e2ae --- /dev/null +++ b/public/res/ic/outlined/eye-blind.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/res/ic/outlined/eye.svg b/public/res/ic/outlined/eye.svg new file mode 100644 index 00000000..1ce868bf --- /dev/null +++ b/public/res/ic/outlined/eye.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/res/ic/outlined/file.svg b/public/res/ic/outlined/file.svg new file mode 100644 index 00000000..d6a2a27a --- /dev/null +++ b/public/res/ic/outlined/file.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/public/res/ic/outlined/flag.svg b/public/res/ic/outlined/flag.svg new file mode 100644 index 00000000..8fce98d6 --- /dev/null +++ b/public/res/ic/outlined/flag.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/public/res/ic/outlined/hash-globe.svg b/public/res/ic/outlined/hash-globe.svg new file mode 100644 index 00000000..ce3df083 --- /dev/null +++ b/public/res/ic/outlined/hash-globe.svg @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/hash-lock.svg b/public/res/ic/outlined/hash-lock.svg new file mode 100644 index 00000000..ae263ced --- /dev/null +++ b/public/res/ic/outlined/hash-lock.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/hash-plus.svg b/public/res/ic/outlined/hash-plus.svg new file mode 100644 index 00000000..69737fd5 --- /dev/null +++ b/public/res/ic/outlined/hash-plus.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/public/res/ic/outlined/hash-search.svg b/public/res/ic/outlined/hash-search.svg new file mode 100644 index 00000000..f135e898 --- /dev/null +++ b/public/res/ic/outlined/hash-search.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/hash-shield.svg b/public/res/ic/outlined/hash-shield.svg new file mode 100644 index 00000000..dfd344b1 --- /dev/null +++ b/public/res/ic/outlined/hash-shield.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/hash.svg b/public/res/ic/outlined/hash.svg new file mode 100644 index 00000000..dcb8b964 --- /dev/null +++ b/public/res/ic/outlined/hash.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/public/res/ic/outlined/heart.svg b/public/res/ic/outlined/heart.svg new file mode 100644 index 00000000..c5b940b6 --- /dev/null +++ b/public/res/ic/outlined/heart.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/public/res/ic/outlined/home.svg b/public/res/ic/outlined/home.svg new file mode 100644 index 00000000..3c7a02df --- /dev/null +++ b/public/res/ic/outlined/home.svg @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/horizontal-menu.svg b/public/res/ic/outlined/horizontal-menu.svg new file mode 100644 index 00000000..a19b3c35 --- /dev/null +++ b/public/res/ic/outlined/horizontal-menu.svg @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/public/res/ic/outlined/inbox.svg b/public/res/ic/outlined/inbox.svg new file mode 100644 index 00000000..65435876 --- /dev/null +++ b/public/res/ic/outlined/inbox.svg @@ -0,0 +1,9 @@ + + + + + + diff --git a/public/res/ic/outlined/info.svg b/public/res/ic/outlined/info.svg new file mode 100644 index 00000000..30a57887 --- /dev/null +++ b/public/res/ic/outlined/info.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/public/res/ic/outlined/invite-arrow.svg b/public/res/ic/outlined/invite-arrow.svg new file mode 100644 index 00000000..370bf8e8 --- /dev/null +++ b/public/res/ic/outlined/invite-arrow.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/invite-cancel-arrow.svg b/public/res/ic/outlined/invite-cancel-arrow.svg new file mode 100644 index 00000000..795a773a --- /dev/null +++ b/public/res/ic/outlined/invite-cancel-arrow.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/invite.svg b/public/res/ic/outlined/invite.svg new file mode 100644 index 00000000..3896e15e --- /dev/null +++ b/public/res/ic/outlined/invite.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/join-arrow.svg b/public/res/ic/outlined/join-arrow.svg new file mode 100644 index 00000000..90cfa651 --- /dev/null +++ b/public/res/ic/outlined/join-arrow.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/public/res/ic/outlined/leave-arrow.svg b/public/res/ic/outlined/leave-arrow.svg new file mode 100644 index 00000000..a51ac1d1 --- /dev/null +++ b/public/res/ic/outlined/leave-arrow.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/public/res/ic/outlined/lock.svg b/public/res/ic/outlined/lock.svg new file mode 100644 index 00000000..77021f0f --- /dev/null +++ b/public/res/ic/outlined/lock.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/markdown.svg b/public/res/ic/outlined/markdown.svg new file mode 100644 index 00000000..775afbfb --- /dev/null +++ b/public/res/ic/outlined/markdown.svg @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/message-unread.svg b/public/res/ic/outlined/message-unread.svg new file mode 100644 index 00000000..fc5e9ff0 --- /dev/null +++ b/public/res/ic/outlined/message-unread.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/public/res/ic/outlined/message.svg b/public/res/ic/outlined/message.svg new file mode 100644 index 00000000..d36e9a30 --- /dev/null +++ b/public/res/ic/outlined/message.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/public/res/ic/outlined/pause.svg b/public/res/ic/outlined/pause.svg new file mode 100644 index 00000000..c312613b --- /dev/null +++ b/public/res/ic/outlined/pause.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + diff --git a/public/res/ic/outlined/peace.svg b/public/res/ic/outlined/peace.svg new file mode 100644 index 00000000..8a7c81a3 --- /dev/null +++ b/public/res/ic/outlined/peace.svg @@ -0,0 +1,9 @@ + + + + + + diff --git a/public/res/ic/outlined/pencil.svg b/public/res/ic/outlined/pencil.svg new file mode 100644 index 00000000..1b8ac24a --- /dev/null +++ b/public/res/ic/outlined/pencil.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/public/res/ic/outlined/photo.svg b/public/res/ic/outlined/photo.svg new file mode 100644 index 00000000..af01a330 --- /dev/null +++ b/public/res/ic/outlined/photo.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/pin.svg b/public/res/ic/outlined/pin.svg new file mode 100644 index 00000000..211242cd --- /dev/null +++ b/public/res/ic/outlined/pin.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/public/res/ic/outlined/play.svg b/public/res/ic/outlined/play.svg new file mode 100644 index 00000000..87b3a8f6 --- /dev/null +++ b/public/res/ic/outlined/play.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/plus.svg b/public/res/ic/outlined/plus.svg new file mode 100644 index 00000000..ce37594e --- /dev/null +++ b/public/res/ic/outlined/plus.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/public/res/ic/outlined/power.svg b/public/res/ic/outlined/power.svg new file mode 100644 index 00000000..8aeb6db8 --- /dev/null +++ b/public/res/ic/outlined/power.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/recent-clock.svg b/public/res/ic/outlined/recent-clock.svg new file mode 100644 index 00000000..30b10d59 --- /dev/null +++ b/public/res/ic/outlined/recent-clock.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + diff --git a/public/res/ic/outlined/reply-arrow.svg b/public/res/ic/outlined/reply-arrow.svg new file mode 100644 index 00000000..3cda01cd --- /dev/null +++ b/public/res/ic/outlined/reply-arrow.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/public/res/ic/outlined/search.svg b/public/res/ic/outlined/search.svg new file mode 100644 index 00000000..75dd6320 --- /dev/null +++ b/public/res/ic/outlined/search.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/public/res/ic/outlined/send.svg b/public/res/ic/outlined/send.svg new file mode 100644 index 00000000..aa487132 --- /dev/null +++ b/public/res/ic/outlined/send.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/public/res/ic/outlined/settings.svg b/public/res/ic/outlined/settings.svg new file mode 100644 index 00000000..ee640b39 --- /dev/null +++ b/public/res/ic/outlined/settings.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + diff --git a/public/res/ic/outlined/shield-empty.svg b/public/res/ic/outlined/shield-empty.svg new file mode 100644 index 00000000..6bc9d304 --- /dev/null +++ b/public/res/ic/outlined/shield-empty.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/public/res/ic/outlined/shield-user.svg b/public/res/ic/outlined/shield-user.svg new file mode 100644 index 00000000..bd5f07c5 --- /dev/null +++ b/public/res/ic/outlined/shield-user.svg @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/public/res/ic/outlined/shield.svg b/public/res/ic/outlined/shield.svg new file mode 100644 index 00000000..9bb46fa1 --- /dev/null +++ b/public/res/ic/outlined/shield.svg @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/space-globe.svg b/public/res/ic/outlined/space-globe.svg new file mode 100644 index 00000000..63d71f1d --- /dev/null +++ b/public/res/ic/outlined/space-globe.svg @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/space-lock.svg b/public/res/ic/outlined/space-lock.svg new file mode 100644 index 00000000..b15705ca --- /dev/null +++ b/public/res/ic/outlined/space-lock.svg @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/space-plus.svg b/public/res/ic/outlined/space-plus.svg new file mode 100644 index 00000000..4d69a1ef --- /dev/null +++ b/public/res/ic/outlined/space-plus.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/space.svg b/public/res/ic/outlined/space.svg new file mode 100644 index 00000000..a4b54b3e --- /dev/null +++ b/public/res/ic/outlined/space.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/public/res/ic/outlined/star.svg b/public/res/ic/outlined/star.svg new file mode 100644 index 00000000..290f159a --- /dev/null +++ b/public/res/ic/outlined/star.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/public/res/ic/outlined/sticker.svg b/public/res/ic/outlined/sticker.svg new file mode 100644 index 00000000..bc486e5e --- /dev/null +++ b/public/res/ic/outlined/sticker.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/res/ic/outlined/sun.svg b/public/res/ic/outlined/sun.svg new file mode 100644 index 00000000..d8ed06fd --- /dev/null +++ b/public/res/ic/outlined/sun.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/res/ic/outlined/tick-mark.svg b/public/res/ic/outlined/tick-mark.svg new file mode 100644 index 00000000..8e76ed55 --- /dev/null +++ b/public/res/ic/outlined/tick-mark.svg @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/public/res/ic/outlined/user.svg b/public/res/ic/outlined/user.svg new file mode 100644 index 00000000..6756a1b2 --- /dev/null +++ b/public/res/ic/outlined/user.svg @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/vertical-menu.svg b/public/res/ic/outlined/vertical-menu.svg new file mode 100644 index 00000000..ec5c544c --- /dev/null +++ b/public/res/ic/outlined/vertical-menu.svg @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/public/res/ic/outlined/vlc.svg b/public/res/ic/outlined/vlc.svg new file mode 100644 index 00000000..8a2b844f --- /dev/null +++ b/public/res/ic/outlined/vlc.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/public/res/ic/outlined/volume-full.svg b/public/res/ic/outlined/volume-full.svg new file mode 100644 index 00000000..20419e72 --- /dev/null +++ b/public/res/ic/outlined/volume-full.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/public/res/ic/outlined/volume-mute.svg b/public/res/ic/outlined/volume-mute.svg new file mode 100644 index 00000000..beb06771 --- /dev/null +++ b/public/res/ic/outlined/volume-mute.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/src/app/atoms/avatar/Avatar.jsx b/src/app/atoms/avatar/Avatar.jsx new file mode 100644 index 00000000..27bf7c90 --- /dev/null +++ b/src/app/atoms/avatar/Avatar.jsx @@ -0,0 +1,69 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import './Avatar.scss'; + +import Text from '../text/Text'; +import RawIcon from '../system-icons/RawIcon'; + +import ImageBrokenSVG from '../../../../public/res/svg/image-broken.svg'; +import { avatarInitials } from '../../../util/common'; + +const Avatar = React.forwardRef(({ text, bgColor, iconSrc, iconColor, imageSrc, size }, ref) => { + let textSize = 's1'; + if (size === 'large') textSize = 'h1'; + if (size === 'small') textSize = 'b1'; + if (size === 'extra-small') textSize = 'b3'; + + return ( +
+ {imageSrc !== null ? ( + { + e.target.style.backgroundColor = 'transparent'; + }} + onError={(e) => { + e.target.src = ImageBrokenSVG; + }} + alt="" + /> + ) : ( + + {iconSrc !== null ? ( + + ) : ( + text !== null && ( + + {avatarInitials(text)} + + ) + )} + + )} +
+ ); +}); + +Avatar.defaultProps = { + text: null, + bgColor: 'transparent', + iconSrc: null, + iconColor: null, + imageSrc: null, + size: 'normal', +}; + +Avatar.propTypes = { + text: PropTypes.string, + bgColor: PropTypes.string, + iconSrc: PropTypes.string, + iconColor: PropTypes.string, + imageSrc: PropTypes.string, + size: PropTypes.oneOf(['large', 'normal', 'small', 'extra-small']), +}; + +export default Avatar; diff --git a/src/app/atoms/avatar/Avatar.scss b/src/app/atoms/avatar/Avatar.scss new file mode 100644 index 00000000..ea69c9e8 --- /dev/null +++ b/src/app/atoms/avatar/Avatar.scss @@ -0,0 +1,56 @@ +@use '../../partials/flex'; + +.avatar-container { + display: inline-flex; + width: 42px; + height: 42px; + border-radius: var(--bo-radius); + position: relative; + + &__large { + width: var(--av-large); + height: var(--av-large); + } + &__normal { + width: var(--av-normal); + height: var(--av-normal); + } + + &__small { + width: var(--av-small); + height: var(--av-small); + } + + &__extra-small { + width: var(--av-extra-small); + height: var(--av-extra-small); + } + + > img { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: inherit; + background-color: var(--bg-surface-hover); + } + + .avatar__border { + @extend .cp-fx__row--c-c; + + position: absolute; + top: 0; + left: 0; + + width: 100%; + height: 100%; + border-radius: inherit; + + .text { + color: white; + } + &--active { + @extend .avatar__border; + box-shadow: var(--bs-surface-border); + } + } +} \ No newline at end of file diff --git a/src/app/atoms/avatar/render.js b/src/app/atoms/avatar/render.js new file mode 100644 index 00000000..e8cf1a66 --- /dev/null +++ b/src/app/atoms/avatar/render.js @@ -0,0 +1,57 @@ +import { avatarInitials, cssVar } from '../../../util/common'; + +// renders the avatar and returns it as an URL +export default async function renderAvatar({ + text, bgColor, imageSrc, size, borderRadius, scale, +}) { + try { + const canvas = document.createElement('canvas'); + canvas.width = size * scale; + canvas.height = size * scale; + + const ctx = canvas.getContext('2d'); + + ctx.scale(scale, scale); + + // rounded corners + ctx.beginPath(); + ctx.moveTo(size, size); + ctx.arcTo(0, size, 0, 0, borderRadius); + ctx.arcTo(0, 0, size, 0, borderRadius); + ctx.arcTo(size, 0, size, size, borderRadius); + ctx.arcTo(size, size, 0, size, borderRadius); + + if (imageSrc) { + // clip corners of image + ctx.closePath(); + ctx.clip(); + + const img = new Image(); + img.crossOrigin = 'anonymous'; + const promise = new Promise((resolve, reject) => { + img.onerror = reject; + img.onload = resolve; + }); + img.src = imageSrc; + await promise; + + ctx.drawImage(img, 0, 0, size, size); + } else { + // colored background + ctx.fillStyle = cssVar(bgColor); + ctx.fill(); + + // centered letter + ctx.fillStyle = '#fff'; + ctx.font = `${cssVar('--fs-s1')} ${cssVar('--font-primary')}`; + ctx.textBaseline = 'middle'; + ctx.textAlign = 'center'; + ctx.fillText(avatarInitials(text), size / 2, size / 2); + } + + return canvas.toDataURL(); + } catch (e) { + console.error(e); + return imageSrc; + } +} diff --git a/src/app/atoms/badge/NotificationBadge.jsx b/src/app/atoms/badge/NotificationBadge.jsx new file mode 100644 index 00000000..12c1bd44 --- /dev/null +++ b/src/app/atoms/badge/NotificationBadge.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import './NotificationBadge.scss'; + +import Text from '../text/Text'; + +function NotificationBadge({ alert, content }) { + const notificationClass = alert ? ' notification-badge--alert' : ''; + return ( +
+ {content !== null && {content}} +
+ ); +} + +NotificationBadge.defaultProps = { + alert: false, + content: null, +}; + +NotificationBadge.propTypes = { + alert: PropTypes.bool, + content: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]), +}; + +export default NotificationBadge; diff --git a/src/app/atoms/badge/NotificationBadge.scss b/src/app/atoms/badge/NotificationBadge.scss new file mode 100644 index 00000000..f5cfa73f --- /dev/null +++ b/src/app/atoms/badge/NotificationBadge.scss @@ -0,0 +1,21 @@ +.notification-badge { + min-width: 16px; + min-height: 8px; + padding: 0 var(--sp-ultra-tight); + background-color: var(--bg-badge); + border-radius: var(--bo-radius); + + .text { + color: var(--tc-badge); + text-align: center; + } + + &--alert { + background-color: var(--bg-positive); + } + + &:empty { + min-width: 8px; + margin: 0 var(--sp-ultra-tight); + } +} \ No newline at end of file diff --git a/src/app/atoms/button/Button.jsx b/src/app/atoms/button/Button.jsx new file mode 100644 index 00000000..1c1c950c --- /dev/null +++ b/src/app/atoms/button/Button.jsx @@ -0,0 +1,53 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import './Button.scss'; + +import Text from '../text/Text'; +import RawIcon from '../system-icons/RawIcon'; +import { blurOnBubbling } from './script'; + +const Button = React.forwardRef(({ + id, className, variant, iconSrc, + type, onClick, children, disabled, +}, ref) => { + const iconClass = (iconSrc === null) ? '' : `btn-${variant}--icon`; + return ( + + ); +}); + +Button.defaultProps = { + id: '', + className: null, + variant: 'surface', + iconSrc: null, + type: 'button', + onClick: null, + disabled: false, +}; + +Button.propTypes = { + id: PropTypes.string, + className: PropTypes.string, + variant: PropTypes.oneOf(['surface', 'primary', 'positive', 'caution', 'danger']), + iconSrc: PropTypes.string, + type: PropTypes.oneOf(['button', 'submit', 'reset']), + onClick: PropTypes.func, + children: PropTypes.node.isRequired, + disabled: PropTypes.bool, +}; + +export default Button; diff --git a/src/app/atoms/button/Button.scss b/src/app/atoms/button/Button.scss new file mode 100644 index 00000000..e1a01bb0 --- /dev/null +++ b/src/app/atoms/button/Button.scss @@ -0,0 +1,81 @@ +@use 'state'; +@use '../../partials/dir'; +@use '../../partials/text'; + +.btn-surface, +.btn-primary, +.btn-positive, +.btn-caution, +.btn-danger { + display: inline-flex; + align-items: center; + justify-content: center; + + min-width: 80px; + padding: var(--sp-extra-tight) var(--sp-normal); + background-color: transparent; + border: none; + border-radius: var(--bo-radius); + cursor: pointer; + @include state.disabled; + + & .text { + @extend .cp-txt__ellipsis; + } + + &--icon { + @include dir.side(padding, var(--sp-tight), var(--sp-loose)); + + } + .ic-raw { + @include dir.side(margin, 0, var(--sp-extra-tight)); + flex-shrink: 0; + } +} + +@mixin color($textColor, $iconColor) { + .text { + color: $textColor; + } + .ic-raw { + background-color: $iconColor; + } +} + + +.btn-surface { + box-shadow: var(--bs-surface-border); + @include color(var(--tc-surface-high), var(--ic-surface-normal)); + @include state.hover(var(--bg-surface-hover)); + @include state.focus(var(--bs-surface-outline)); + @include state.active(var(--bg-surface-active)); +} + +.btn-primary { + background-color: var(--bg-primary); + @include color(var(--tc-primary-high), var(--ic-primary-normal)); + @include state.hover(var(--bg-primary-hover)); + @include state.focus(var(--bs-primary-outline)); + @include state.active(var(--bg-primary-active)); +} +.btn-positive { + box-shadow: var(--bs-positive-border); + @include color(var(--tc-positive-high), var(--ic-positive-normal)); + @include state.hover(var(--bg-positive-hover)); + @include state.focus(var(--bs-positive-outline)); + @include state.active(var(--bg-positive-active)); +} +.btn-caution { + box-shadow: var(--bs-caution-border); + @include color(var(--tc-caution-high), var(--ic-caution-normal)); + @include state.hover(var(--bg-caution-hover)); + @include state.focus(var(--bs-caution-outline)); + @include state.active(var(--bg-caution-active)); +} +.btn-danger { + box-shadow: var(--bs-danger-border); + @include color(var(--tc-danger-high), var(--ic-danger-normal)); + @include state.hover(var(--bg-danger-hover)); + @include state.focus(var(--bs-danger-outline)); + @include state.active(var(--bg-danger-active)); +} \ No newline at end of file diff --git a/src/app/atoms/button/Checkbox.jsx b/src/app/atoms/button/Checkbox.jsx new file mode 100644 index 00000000..7fcea3b5 --- /dev/null +++ b/src/app/atoms/button/Checkbox.jsx @@ -0,0 +1,39 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import './Checkbox.scss'; + +function Checkbox({ + variant, isActive, onToggle, + disabled, tabIndex, +}) { + const className = `checkbox checkbox-${variant}${isActive ? ' checkbox--active' : ''}`; + if (onToggle === null) return ; + return ( + // eslint-disable-next-line jsx-a11y/control-has-associated-label + + ); + if (tooltip === null) return btn; + return ( + {tooltip}} + > + {btn} + + ); +}); + +IconButton.defaultProps = { + variant: 'surface', + size: 'normal', + type: 'button', + tooltip: null, + tooltipPlacement: 'top', + onClick: null, + tabIndex: 0, + disabled: false, + isImage: false, + className: '', +}; + +IconButton.propTypes = { + variant: PropTypes.oneOf(['surface', 'primary', 'positive', 'caution', 'danger']), + size: PropTypes.oneOf(['normal', 'small', 'extra-small']), + type: PropTypes.oneOf(['button', 'submit', 'reset']), + tooltip: PropTypes.string, + tooltipPlacement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']), + src: PropTypes.string.isRequired, + onClick: PropTypes.func, + tabIndex: PropTypes.number, + disabled: PropTypes.bool, + isImage: PropTypes.bool, + className: PropTypes.string, +}; + +export default IconButton; diff --git a/src/app/atoms/button/IconButton.scss b/src/app/atoms/button/IconButton.scss new file mode 100644 index 00000000..aa6480c0 --- /dev/null +++ b/src/app/atoms/button/IconButton.scss @@ -0,0 +1,56 @@ +@use 'state'; + +.ic-btn { + padding: var(--sp-extra-tight); + border: none; + border-radius: var(--bo-radius); + background-color: transparent; + font-size: 0; + line-height: 0; + cursor: pointer; + @include state.disabled; +} + +@mixin color($color) { + .ic-raw { + background-color: $color; + } +} +@mixin focus($color) { + &:focus { + outline: none; + background-color: $color; + } +} + +.ic-btn-surface { + @include color(var(--ic-surface-normal)); + @include state.hover(var(--bg-surface-hover)); + @include focus(var(--bg-surface-hover)); + @include state.active(var(--bg-surface-active)); +} +.ic-btn-primary { + @include color(var(--ic-primary-normal)); + @include state.hover(var(--bg-primary-hover)); + @include focus(var(--bg-primary-hover)); + @include state.active(var(--bg-primary-active)); + background-color: var(--bg-primary); +} +.ic-btn-positive { + @include color(var(--ic-positive-normal)); + @include state.hover(var(--bg-positive-hover)); + @include focus(var(--bg-positive-hover)); + @include state.active(var(--bg-positive-active)); +} +.ic-btn-caution { + @include color(var(--ic-caution-normal)); + @include state.hover(var(--bg-caution-hover)); + @include focus(var(--bg-caution-hover)); + @include state.active(var(--bg-caution-active)); +} +.ic-btn-danger { + @include color(var(--ic-danger-normal)); + @include state.hover(var(--bg-danger-hover)); + @include focus(var(--bg-danger-hover)); + @include state.active(var(--bg-danger-active)); +} \ No newline at end of file diff --git a/src/app/atoms/button/RadioButton.jsx b/src/app/atoms/button/RadioButton.jsx new file mode 100644 index 00000000..35b68baf --- /dev/null +++ b/src/app/atoms/button/RadioButton.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import './RadioButton.scss'; + +function RadioButton({ isActive, onToggle, disabled }) { + if (onToggle === null) return ; + return ( + // eslint-disable-next-line jsx-a11y/control-has-associated-label + + ); +} + +Chip.propTypes = { + iconSrc: PropTypes.string, + iconColor: PropTypes.string, + text: PropTypes.string, + children: PropTypes.element, + onClick: PropTypes.func, +}; + +Chip.defaultProps = { + iconSrc: null, + iconColor: null, + text: null, + children: null, + onClick: null, +}; + +export default Chip; diff --git a/src/app/atoms/chip/Chip.scss b/src/app/atoms/chip/Chip.scss new file mode 100644 index 00000000..7396b0dc --- /dev/null +++ b/src/app/atoms/chip/Chip.scss @@ -0,0 +1,31 @@ +@use '../../partials/dir'; + +.chip { + padding: var(--sp-ultra-tight) var(--sp-extra-tight); + + display: inline-flex; + flex-direction: row; + align-items: center; + + background: var(--bg-surface-low); + border-radius: var(--bo-radius); + box-shadow: var(--bs-surface-border); + cursor: pointer; + + @media (hover: hover) { + &:hover { + background-color: var(--bg-surface-hover); + } + } + + & > .text { + flex: 1; + color: var(--tc-surface-high); + } + + & > .ic-raw { + width: 16px; + height: 16px; + @include dir.side(margin, 0, var(--sp-ultra-tight)); + } +} \ No newline at end of file diff --git a/src/app/atoms/context-menu/ContextMenu.jsx b/src/app/atoms/context-menu/ContextMenu.jsx new file mode 100644 index 00000000..7d1acd44 --- /dev/null +++ b/src/app/atoms/context-menu/ContextMenu.jsx @@ -0,0 +1,115 @@ +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import './ContextMenu.scss'; + +import Tippy from '@tippyjs/react'; +import 'tippy.js/animations/scale-extreme.css'; + +import Text from '../text/Text'; +import Button from '../button/Button'; +import ScrollView from '../scroll/ScrollView'; + +function ContextMenu({ + content, placement, maxWidth, render, afterToggle, +}) { + const [isVisible, setVisibility] = useState(false); + const showMenu = () => setVisibility(true); + const hideMenu = () => setVisibility(false); + + useEffect(() => { + if (afterToggle !== null) afterToggle(isVisible); + }, [isVisible]); + + return ( + {typeof content === 'function' ? content(hideMenu) : content}} + placement={placement} + interactive + arrow={false} + maxWidth={maxWidth} + duration={200} + > + {render(isVisible ? hideMenu : showMenu)} + + ); +} + +ContextMenu.defaultProps = { + maxWidth: 'unset', + placement: 'right', + afterToggle: null, +}; + +ContextMenu.propTypes = { + content: PropTypes.oneOfType([ + PropTypes.node, + PropTypes.func, + ]).isRequired, + placement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']), + maxWidth: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]), + render: PropTypes.func.isRequired, + afterToggle: PropTypes.func, +}; + +function MenuHeader({ children }) { + return ( +
+ { children } +
+ ); +} + +MenuHeader.propTypes = { + children: PropTypes.node.isRequired, +}; + +function MenuItem({ + variant, iconSrc, type, + onClick, children, disabled, +}) { + return ( +
+ +
+ ); +} + +MenuItem.defaultProps = { + variant: 'surface', + iconSrc: null, + type: 'button', + disabled: false, + onClick: null, +}; + +MenuItem.propTypes = { + variant: PropTypes.oneOf(['surface', 'positive', 'caution', 'danger']), + iconSrc: PropTypes.string, + type: PropTypes.oneOf(['button', 'submit']), + onClick: PropTypes.func, + children: PropTypes.node.isRequired, + disabled: PropTypes.bool, +}; + +function MenuBorder() { + return
; +} + +export { + ContextMenu as default, MenuHeader, MenuItem, MenuBorder, +}; diff --git a/src/app/atoms/context-menu/ContextMenu.scss b/src/app/atoms/context-menu/ContextMenu.scss new file mode 100644 index 00000000..2df9f0a4 --- /dev/null +++ b/src/app/atoms/context-menu/ContextMenu.scss @@ -0,0 +1,81 @@ +@use '../../partials/flex'; +@use '../../partials/text'; +@use '../../partials/dir'; + +.context-menu { + background-color: var(--bg-surface); + box-shadow: var(--bs-popup); + border-radius: var(--bo-radius); + overflow: hidden; + + &:focus { + outline: none; + } + & .tippy-content > div > .scrollbar { + max-height: 90vh; + } +} + +.context-menu__click-wrapper { + display: inline-flex; + + &:focus { + outline: none; + } +} + +.context-menu__header { + height: 34px; + padding: 0 var(--sp-normal); + margin-bottom: var(--sp-ultra-tight); + display: flex; + align-items: center; + border-bottom: 1px solid var(--bg-surface-border); + + .text { + @extend .cp-txt__ellipsis; + color: var(--tc-surface-low); + } + + &:not(:first-child) { + margin-top: var(--sp-extra-tight); + border-top: 1px solid var(--bg-surface-border); + } +} + +.context-menu__item { + display: flex; + button[class^="btn"] { + @extend .cp-fx__item-one; + justify-content: flex-start; + border-radius: 0; + box-shadow: none; + white-space: nowrap; + padding: var(--sp-extra-tight) var(--sp-normal); + + & > .ic-raw { + @include dir.side(margin, 0, var(--sp-tight)); + } + + // if item doesn't have icon + .text:first-child { + @include dir.side( + margin, + calc(var(--ic-small) + var(--sp-tight)), + 0 + ); + } + } + .btn-surface:focus { + background-color: var(--bg-surface-hover); + } + .btn-positive:focus { + background-color: var(--bg-positive-hover); + } + .btn-caution:focus { + background-color: var(--bg-caution-hover); + } + .btn-danger:focus { + background-color: var(--bg-danger-hover); + } +} \ No newline at end of file diff --git a/src/app/atoms/context-menu/ReusableContextMenu.jsx b/src/app/atoms/context-menu/ReusableContextMenu.jsx new file mode 100644 index 00000000..59bdb142 --- /dev/null +++ b/src/app/atoms/context-menu/ReusableContextMenu.jsx @@ -0,0 +1,87 @@ +import React, { useState, useEffect, useRef } from 'react'; + +import cons from '../../../client/state/cons'; +import navigation from '../../../client/state/navigation'; + +import ContextMenu from './ContextMenu'; + +let key = null; +function ReusableContextMenu() { + const [data, setData] = useState(null); + const openerRef = useRef(null); + + const closeMenu = () => { + key = null; + if (data) openerRef.current.click(); + }; + + useEffect(() => { + if (data) { + const { cords } = data; + openerRef.current.style.transform = `translate(${cords.x}px, ${cords.y}px)`; + openerRef.current.style.width = `${cords.width}px`; + openerRef.current.style.height = `${cords.height}px`; + openerRef.current.click(); + } + const handleContextMenuOpen = (placement, cords, render, afterClose) => { + if (key) { + closeMenu(); + return; + } + setData({ + placement, cords, render, afterClose, + }); + }; + navigation.on(cons.events.navigation.REUSABLE_CONTEXT_MENU_OPENED, handleContextMenuOpen); + return () => { + navigation.removeListener( + cons.events.navigation.REUSABLE_CONTEXT_MENU_OPENED, + handleContextMenuOpen, + ); + }; + }, [data]); + + const handleAfterToggle = (isVisible) => { + if (isVisible) { + key = Math.random(); + return; + } + data?.afterClose?.(); + if (setData) setData(null); + + if (key === null) return; + const copyKey = key; + setTimeout(() => { + if (key === copyKey) key = null; + }, 200); + }; + + return ( + ( + + )} + /> + ); +} + +export default ReusableContextMenu; diff --git a/src/app/atoms/divider/Divider.jsx b/src/app/atoms/divider/Divider.jsx new file mode 100644 index 00000000..76721241 --- /dev/null +++ b/src/app/atoms/divider/Divider.jsx @@ -0,0 +1,28 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import './Divider.scss'; + +import Text from '../text/Text'; + +function Divider({ text, variant, align }) { + const dividerClass = ` divider--${variant} divider--${align}`; + return ( +
+ {text !== null && {text}} +
+ ); +} + +Divider.defaultProps = { + text: null, + variant: 'surface', + align: 'center', +}; + +Divider.propTypes = { + text: PropTypes.string, + variant: PropTypes.oneOf(['surface', 'primary', 'positive', 'caution', 'danger']), + align: PropTypes.oneOf(['left', 'center', 'right']), +}; + +export default Divider; diff --git a/src/app/atoms/divider/Divider.scss b/src/app/atoms/divider/Divider.scss new file mode 100644 index 00000000..0f013ff0 --- /dev/null +++ b/src/app/atoms/divider/Divider.scss @@ -0,0 +1,68 @@ +.divider-line { + content: ''; + display: inline-block; + flex: 1; + border-bottom: 1px solid var(--local-divider-color); + opacity: var(--local-divider-opacity); +} + +.divider { + display: flex; + align-items: center; + + &--center::before, + &--right::before { + @extend .divider-line; + } + &--center::after, + &--left::after { + @extend .divider-line; + } + + &__text { + padding: 2px var(--sp-extra-tight); + border-radius: calc(var(--bo-radius) / 2); + } +} + +.divider--surface { + --local-divider-color: var(--bg-divider); + --local-divider-opacity: 1; + + .divider__text { + color: var(--tc-surface-low); + border: 1px solid var(--bg-divider); + } +} +.divider--primary { + --local-divider-color: var(--bg-primary); + --local-divider-opacity: .8; + .divider__text { + color: var(--tc-primary-high); + background-color: var(--bg-primary); + } +} +.divider--positive { + --local-divider-color: var(--bg-positive); + --local-divider-opacity: .8; + .divider__text { + color: var(--bg-surface); + background-color: var(--bg-positive); + } +} +.divider--danger { + --local-divider-color: var(--bg-danger); + --local-divider-opacity: .8; + .divider__text { + color: var(--bg-surface); + background-color: var(--bg-danger); + } +} +.divider--caution { + --local-divider-color: var(--bg-caution); + --local-divider-opacity: .8; + .divider__text { + color: var(--bg-surface); + background-color: var(--bg-caution); + } +} diff --git a/src/app/atoms/header/Header.jsx b/src/app/atoms/header/Header.jsx new file mode 100644 index 00000000..3c81e423 --- /dev/null +++ b/src/app/atoms/header/Header.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import './Header.scss'; + +function Header({ children }) { + return ( +
+ {children} +
+ ); +} + +Header.propTypes = { + children: PropTypes.node.isRequired, +}; + +function TitleWrapper({ children }) { + return ( +
+ {children} +
+ ); +} + +TitleWrapper.propTypes = { + children: PropTypes.node.isRequired, +}; + +export { Header as default, TitleWrapper }; diff --git a/src/app/atoms/header/Header.scss b/src/app/atoms/header/Header.scss new file mode 100644 index 00000000..9e45f824 --- /dev/null +++ b/src/app/atoms/header/Header.scss @@ -0,0 +1,43 @@ +@use '../../partials/text'; +@use '../../partials/dir'; + +.header { + @include dir.side(padding, var(--sp-normal), var(--sp-extra-tight)); + width: 100%; + height: var(--header-height); + border-bottom: 1px solid var(--bg-surface-border); + display: flex; + align-items: center; + + &__title-wrapper { + flex: 1; + min-width: 0; + display: flex; + align-items: center; + margin: 0 var(--sp-tight); + + &:first-child { + @include dir.side(margin, 0, var(--sp-tight)); + } + + & > .text:first-child { + @extend .cp-txt__ellipsis; + min-width: 0; + } + & > .text-b3{ + flex: 1; + min-width: 0; + + margin-top: var(--sp-ultra-tight); + @include dir.side(margin, var(--sp-tight), 0); + @include dir.side(padding, var(--sp-tight), 0); + @include dir.side(border, 1px solid var(--bg-surface-border), none); + + max-height: calc(2 * var(--lh-b3)); + overflow: hidden; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + display: -webkit-box; + } + } +} \ No newline at end of file diff --git a/src/app/atoms/input/Input.jsx b/src/app/atoms/input/Input.jsx new file mode 100644 index 00000000..96c94967 --- /dev/null +++ b/src/app/atoms/input/Input.jsx @@ -0,0 +1,97 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import './Input.scss'; + +import TextareaAutosize from 'react-autosize-textarea'; + +function Input({ + id, label, name, value, placeholder, + required, type, onChange, forwardRef, + resizable, minHeight, onResize, state, + onKeyDown, disabled, autoFocus, +}) { + return ( +
+ { label !== '' && } + { resizable + ? ( + + ) : ( + + )} +
+ ); +} + +Input.defaultProps = { + id: null, + name: '', + label: '', + value: '', + placeholder: '', + type: 'text', + required: false, + onChange: null, + forwardRef: null, + resizable: false, + minHeight: 46, + onResize: null, + state: 'normal', + onKeyDown: null, + disabled: false, + autoFocus: false, +}; + +Input.propTypes = { + id: PropTypes.string, + name: PropTypes.string, + label: PropTypes.string, + value: PropTypes.string, + placeholder: PropTypes.string, + required: PropTypes.bool, + type: PropTypes.string, + onChange: PropTypes.func, + forwardRef: PropTypes.shape({}), + resizable: PropTypes.bool, + minHeight: PropTypes.number, + onResize: PropTypes.func, + state: PropTypes.oneOf(['normal', 'success', 'error']), + onKeyDown: PropTypes.func, + disabled: PropTypes.bool, + autoFocus: PropTypes.bool, +}; + +export default Input; diff --git a/src/app/atoms/input/Input.scss b/src/app/atoms/input/Input.scss new file mode 100644 index 00000000..40fe43ec --- /dev/null +++ b/src/app/atoms/input/Input.scss @@ -0,0 +1,52 @@ +@use '../../atoms/scroll/scrollbar'; + +.input { + display: block; + width: 100%; + min-width: 0px; + margin: 0; + padding: var(--sp-tight) var(--sp-normal); + background-color: var(--bg-surface-low); + color: var(--tc-surface-normal); + box-shadow: none; + border-radius: var(--bo-radius); + border: 1px solid var(--bg-surface-border); + font-size: var(--fs-b2); + letter-spacing: var(--ls-b2); + line-height: var(--lh-b2); + + :disabled { + opacity: 0.4; + cursor: no-drop; + } + + &__label { + display: inline-block; + margin-bottom: var(--sp-ultra-tight); + color: var(--tc-surface-low); + } + + &--resizable { + resize: vertical !important; + overflow-y: auto !important; + @include scrollbar.scroll; + @include scrollbar.scroll__v; + @include scrollbar.scroll--auto-hide; + } + &--success { + border: 1px solid var(--bg-positive); + box-shadow: none !important; + } + &--error { + border: 1px solid var(--bg-danger); + box-shadow: none !important; + } + + &:focus { + outline: none; + box-shadow: var(--bs-primary-border); + } + &::placeholder { + color: var(--tc-surface-low) + } +} \ No newline at end of file diff --git a/src/app/atoms/modal/RawModal.jsx b/src/app/atoms/modal/RawModal.jsx new file mode 100644 index 00000000..450be0e0 --- /dev/null +++ b/src/app/atoms/modal/RawModal.jsx @@ -0,0 +1,73 @@ +import React, { useEffect } from 'react'; +import PropTypes from 'prop-types'; +import './RawModal.scss'; + +import Modal from 'react-modal'; + +import navigation from '../../../client/state/navigation'; + +Modal.setAppElement('#root'); + +function RawModal({ + className, overlayClassName, + isOpen, size, onAfterOpen, onAfterClose, + onRequestClose, closeFromOutside, children, +}) { + let modalClass = (className !== null) ? `${className} ` : ''; + switch (size) { + case 'large': + modalClass += 'raw-modal__large '; + break; + case 'medium': + modalClass += 'raw-modal__medium '; + break; + case 'small': + default: + modalClass += 'raw-modal__small '; + } + + useEffect(() => { + navigation.setIsRawModalVisible(isOpen); + }, [isOpen]); + + const modalOverlayClass = (overlayClassName !== null) ? `${overlayClassName} ` : ''; + return ( + + {children} + + ); +} + +RawModal.defaultProps = { + className: null, + overlayClassName: null, + size: 'small', + onAfterOpen: null, + onAfterClose: null, + onRequestClose: null, + closeFromOutside: true, +}; + +RawModal.propTypes = { + className: PropTypes.string, + overlayClassName: PropTypes.string, + isOpen: PropTypes.bool.isRequired, + size: PropTypes.oneOf(['large', 'medium', 'small']), + onAfterOpen: PropTypes.func, + onAfterClose: PropTypes.func, + onRequestClose: PropTypes.func, + closeFromOutside: PropTypes.bool, + children: PropTypes.node.isRequired, +}; + +export default RawModal; diff --git a/src/app/atoms/modal/RawModal.scss b/src/app/atoms/modal/RawModal.scss new file mode 100644 index 00000000..d612a4bc --- /dev/null +++ b/src/app/atoms/modal/RawModal.scss @@ -0,0 +1,66 @@ +.raw-modal { + --small-modal-width: 525px; + --medium-modal-width: 712px; + --large-modal-width: 1024px; + + position: relative; + width: 100%; + max-height: 100%; + border-radius: var(--bo-radius); + box-shadow: var(--bs-popup); + outline: none; + overflow: hidden; + + &__small { + max-width: var(--small-modal-width); + } + &__medium { + max-width: var(--medium-modal-width); + } + &__large { + max-width: var(--large-modal-width); + } + + &__overlay { + position: fixed; + top: 0; + left: 0; + z-index: 999; + + display: flex; + justify-content: center; + align-items: center; + + padding: var(--sp-normal); + width: 100%; + height: 100%; + background-color: var(--bg-overlay); + } +} + +.ReactModal__Overlay { + animation: raw-modal--overlay 150ms; +} + +.ReactModal__Content { + animation: raw-modal--content 150ms; +} + +@keyframes raw-modal--content { + 0% { + transform: translateY(100px); + opacity: .5; + } + 100% { + transform: translateY(0); + opacity: 1; + } +} +@keyframes raw-modal--overlay { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} diff --git a/src/app/atoms/scroll/ScrollView.jsx b/src/app/atoms/scroll/ScrollView.jsx new file mode 100644 index 00000000..26c0c83a --- /dev/null +++ b/src/app/atoms/scroll/ScrollView.jsx @@ -0,0 +1,37 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import './ScrollView.scss'; + +const ScrollView = React.forwardRef(({ + horizontal, vertical, autoHide, invisible, onScroll, children, +}, ref) => { + let scrollbarClasses = ''; + if (horizontal) scrollbarClasses += ' scrollbar__h'; + if (vertical) scrollbarClasses += ' scrollbar__v'; + if (autoHide) scrollbarClasses += ' scrollbar--auto-hide'; + if (invisible) scrollbarClasses += ' scrollbar--invisible'; + return ( +
+ {children} +
+ ); +}); + +ScrollView.defaultProps = { + horizontal: false, + vertical: true, + autoHide: false, + invisible: false, + onScroll: null, +}; + +ScrollView.propTypes = { + horizontal: PropTypes.bool, + vertical: PropTypes.bool, + autoHide: PropTypes.bool, + invisible: PropTypes.bool, + onScroll: PropTypes.func, + children: PropTypes.node.isRequired, +}; + +export default ScrollView; diff --git a/src/app/atoms/scroll/ScrollView.scss b/src/app/atoms/scroll/ScrollView.scss new file mode 100644 index 00000000..251037e1 --- /dev/null +++ b/src/app/atoms/scroll/ScrollView.scss @@ -0,0 +1,31 @@ +@use '../../partials/dir'; +@use '_scrollbar'; + +@mixin paddingForSafari($padding) { + @media not all and (min-resolution:.001dpcm) { + @include dir.side(padding, 0, $padding); + } +} + +.scrollbar { + width: 100%; + height: 100%; + @include scrollbar.scroll; + @include paddingForSafari(var(--sp-extra-tight)); + + &__h { + @include scrollbar.scroll__h; + } + + &__v { + @include scrollbar.scroll__v; + } + + &--auto-hide { + @include scrollbar.scroll--auto-hide; + } + &--invisible { + @include scrollbar.scroll--invisible; + @include paddingForSafari(0); + } +} \ No newline at end of file diff --git a/src/app/atoms/scroll/_scrollbar.scss b/src/app/atoms/scroll/_scrollbar.scss new file mode 100644 index 00000000..78ec75ad --- /dev/null +++ b/src/app/atoms/scroll/_scrollbar.scss @@ -0,0 +1,65 @@ +.firefox-scrollbar { + scrollbar-width: thin; + scrollbar-color: var(--bg-surface-hover) transparent; + &--transparent { + scrollbar-color: transparent transparent; + } +} +.webkit-scrollbar { + &::-webkit-scrollbar { + width: 8px; + height: 8px; + } +} +.webkit-scrollbar-track { + &::-webkit-scrollbar-track { + background-color: transparent; + } +} +.webkit-scrollbar-thumb { + &::-webkit-scrollbar-thumb { + background-color: var(--bg-surface-hover); + } + &::-webkit-scrollbar-thumb:hover { + background-color: var(--bg-surface-active); + } + &--transparent { + &::-webkit-scrollbar-thumb { + background-color: transparent; + } + } +} + +@mixin scroll { + overflow: hidden; + // Below code stop scroll when x-scrollable content come in timeline + // overscroll-behavior: none; + @extend .firefox-scrollbar; + @extend .webkit-scrollbar; + @extend .webkit-scrollbar-track; + @extend .webkit-scrollbar-thumb; +} + +@mixin scroll__h { + overflow-x: scroll; +} +@mixin scroll__v { + overflow-y: scroll; +} +@mixin scroll--auto-hide { + @extend .firefox-scrollbar--transparent; + @extend .webkit-scrollbar-thumb--transparent; + + &:hover { + @extend .firefox-scrollbar; + @extend .webkit-scrollbar-thumb; + } +} +@mixin scroll--invisible { + -ms-overflow-style: none; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } +} \ No newline at end of file diff --git a/src/app/atoms/segmented-controls/SegmentedControls.jsx b/src/app/atoms/segmented-controls/SegmentedControls.jsx new file mode 100644 index 00000000..1d54dd06 --- /dev/null +++ b/src/app/atoms/segmented-controls/SegmentedControls.jsx @@ -0,0 +1,55 @@ +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import './SegmentedControls.scss'; + +import { blurOnBubbling } from '../button/script'; + +import Text from '../text/Text'; +import RawIcon from '../system-icons/RawIcon'; + +function SegmentedControls({ + selected, segments, onSelect, +}) { + const [select, setSelect] = useState(selected); + + function selectSegment(segmentIndex) { + setSelect(segmentIndex); + onSelect(segmentIndex); + } + + useEffect(() => { + setSelect(selected); + }, [selected]); + + return ( +
+ { + segments.map((segment, index) => ( + + )) + } +
+ ); +} + +SegmentedControls.propTypes = { + selected: PropTypes.number.isRequired, + segments: PropTypes.arrayOf(PropTypes.shape({ + iconSrc: PropTypes.string, + text: PropTypes.string, + })).isRequired, + onSelect: PropTypes.func.isRequired, +}; + +export default SegmentedControls; diff --git a/src/app/atoms/segmented-controls/SegmentedControls.scss b/src/app/atoms/segmented-controls/SegmentedControls.scss new file mode 100644 index 00000000..fb1fd987 --- /dev/null +++ b/src/app/atoms/segmented-controls/SegmentedControls.scss @@ -0,0 +1,57 @@ +@use '../button/state'; +@use '../../partials/dir'; + +.segmented-controls { + background-color: var(--bg-surface-low); + border-radius: var(--bo-radius); + border: 1px solid var(--bg-surface-border); + + display: inline-flex; + overflow: hidden; +} + +.segment-btn { + padding: var(--sp-extra-tight) 0; + cursor: pointer; + @include state.hover(var(--bg-surface-hover)); + @include state.active(var(--bg-surface-active)); + + &__base { + padding: 0 var(--sp-normal); + display: flex; + align-items: center; + justify-content: center; + @include dir.side(border, 1px solid var(--bg-surface-border), none); + + & .text:nth-child(2) { + margin: 0 var(--sp-extra-tight); + } + } + &:first-child &__base { + border: none; + } + + &--active { + background-color: var(--bg-surface); + border: 1px solid var(--bg-surface-border); + border-width: 0 1px 0 1px; + + & .segment-btn__base, + & + .segment-btn .segment-btn__base { + border: none; + } + &:first-child{ + border-left: none; + } + &:last-child { + border-right: none; + } + [dir=rtl] & { + border-left: 1px solid var(--bg-surface-border); + border-right: 1px solid var(--bg-surface-border); + + &:first-child { border-right: none;} + &:last-child { border-left: none;} + } + } +} \ No newline at end of file diff --git a/src/app/atoms/spinner/Spinner.jsx b/src/app/atoms/spinner/Spinner.jsx new file mode 100644 index 00000000..61c9747e --- /dev/null +++ b/src/app/atoms/spinner/Spinner.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import './Spinner.scss'; + +function Spinner({ size }) { + return ( +
+ ); +} + +Spinner.defaultProps = { + size: 'normal', +}; + +Spinner.propTypes = { + size: PropTypes.oneOf(['normal', 'small']), +}; + +export default Spinner; diff --git a/src/app/atoms/spinner/Spinner.scss b/src/app/atoms/spinner/Spinner.scss new file mode 100644 index 00000000..73fbf676 --- /dev/null +++ b/src/app/atoms/spinner/Spinner.scss @@ -0,0 +1,22 @@ +.donut-spinner { + display: inline-block; + border: 4px solid var(--bg-surface-border); + border-left-color: var(--tc-surface-normal); + border-radius: 50%; + animation: donut-spin 1.2s cubic-bezier(0.73, 0.32, 0.67, 0.86) infinite; + + &--normal { + width: 40px; + height: 40px; + } + &--small { + width: 28px; + height: 28px; + } +} + +@keyframes donut-spin { + to { + transform: rotate(1turn); + } +} \ No newline at end of file diff --git a/src/app/atoms/system-icons/RawIcon.jsx b/src/app/atoms/system-icons/RawIcon.jsx new file mode 100644 index 00000000..a6697f4f --- /dev/null +++ b/src/app/atoms/system-icons/RawIcon.jsx @@ -0,0 +1,32 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import './RawIcon.scss'; + +function RawIcon({ color, size, src, isImage }) { + const style = {}; + if (color !== null) style.backgroundColor = color; + if (isImage) { + style.backgroundColor = 'transparent'; + style.backgroundImage = `url("${src}")`; + } else { + style.WebkitMaskImage = `url("${src}")`; + style.maskImage = `url("${src}")`; + } + + return ; +} + +RawIcon.defaultProps = { + color: null, + size: 'normal', + isImage: false, +}; + +RawIcon.propTypes = { + color: PropTypes.string, + size: PropTypes.oneOf(['large', 'normal', 'small', 'extra-small']), + src: PropTypes.string.isRequired, + isImage: PropTypes.bool, +}; + +export default RawIcon; diff --git a/src/app/atoms/system-icons/RawIcon.scss b/src/app/atoms/system-icons/RawIcon.scss new file mode 100644 index 00000000..56fc9b3c --- /dev/null +++ b/src/app/atoms/system-icons/RawIcon.scss @@ -0,0 +1,28 @@ +@mixin icSize($size) { + width: $size; + height: $size; +} + +.ic-raw { + display: inline-block; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: cover; + mask-size: cover; + background-color: var(--ic-surface-normal); + + background-size: cover; + background-repeat: no-repeat; +} +.ic-raw-large { + @include icSize(var(--ic-large)); +} +.ic-raw-normal { + @include icSize(var(--ic-normal)); +} +.ic-raw-small { + @include icSize(var(--ic-small)); +} +.ic-raw-extra-small { + @include icSize(var(--ic-extra-small)); +} \ No newline at end of file diff --git a/src/app/atoms/tabs/Tabs.jsx b/src/app/atoms/tabs/Tabs.jsx new file mode 100644 index 00000000..bcdc8ef7 --- /dev/null +++ b/src/app/atoms/tabs/Tabs.jsx @@ -0,0 +1,88 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import './Tabs.scss'; + +import Button from '../button/Button'; +import ScrollView from '../scroll/ScrollView'; + +function TabItem({ + selected, iconSrc, + onClick, children, disabled, +}) { + const isSelected = selected ? 'tab-item--selected' : ''; + + return ( + + ); +} + +TabItem.defaultProps = { + selected: false, + iconSrc: null, + onClick: null, + disabled: false, +}; + +TabItem.propTypes = { + selected: PropTypes.bool, + iconSrc: PropTypes.string, + onClick: PropTypes.func, + children: PropTypes.node.isRequired, + disabled: PropTypes.bool, +}; + +function Tabs({ items, defaultSelected, onSelect }) { + const [selectedItem, setSelectedItem] = useState(items[defaultSelected]); + + const handleTabSelection = (item, index, target) => { + if (selectedItem === item) return; + target.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); + setSelectedItem(item); + onSelect(item, index); + }; + + return ( +
+ +
+ {items.map((item, index) => ( + handleTabSelection(item, index, e.currentTarget)} + > + {item.text} + + ))} +
+
+
+ ); +} + +Tabs.defaultProps = { + defaultSelected: 0, +}; + +Tabs.propTypes = { + items: PropTypes.arrayOf( + PropTypes.shape({ + iconSrc: PropTypes.string, + text: PropTypes.string, + disabled: PropTypes.bool, + }), + ).isRequired, + defaultSelected: PropTypes.number, + onSelect: PropTypes.func.isRequired, +}; + +export default Tabs; diff --git a/src/app/atoms/tabs/Tabs.scss b/src/app/atoms/tabs/Tabs.scss new file mode 100644 index 00000000..39ddddec --- /dev/null +++ b/src/app/atoms/tabs/Tabs.scss @@ -0,0 +1,45 @@ +@use '../../partials/dir'; + +.tabs { + height: var(--header-height); + box-shadow: inset 0 -1px 0 var(--bg-surface-border); + + &__content { + min-width: 100%; + height: 100%; + display: flex; + } +} + +.tab-item { + flex-shrink: 0; + + @include dir.side(padding, var(--sp-normal), 24px); + border-radius: 0; + height: 100%; + box-shadow: none; + border-radius: var(--bo-radius) var(--bo-radius) 0 0; + + &:focus, + &:active { + background-color: var(--bg-surface-active); + box-shadow: none; + } + + &--selected { + --bs-tab-selected: inset 0 -2px 0 var(--tc-surface-high); + box-shadow: var(--bs-tab-selected); + + & .ic-raw { + background-color: var(--ic-surface-high); + } + & .text { + font-weight: var(--fw-medium); + } + &:focus, + &:active { + background-color: var(--bg-surface-active); + box-shadow: var(--bs-tab-selected); + } + } +} \ No newline at end of file diff --git a/src/app/atoms/text/Text.jsx b/src/app/atoms/text/Text.jsx new file mode 100644 index 00000000..3f507ee3 --- /dev/null +++ b/src/app/atoms/text/Text.jsx @@ -0,0 +1,42 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import './Text.scss'; + +function Text({ + className, style, variant, weight, + primary, span, children, +}) { + const classes = []; + if (className) classes.push(className); + + classes.push(`text text-${variant} text-${weight}`); + if (primary) classes.push('font-primary'); + + const textClass = classes.join(' '); + if (span) return { children }; + if (variant === 'h1') return

{ children }

; + if (variant === 'h2') return

{ children }

; + if (variant === 's1') return

{ children }

; + return

{ children }

; +} + +Text.defaultProps = { + className: null, + style: null, + variant: 'b1', + weight: 'normal', + primary: false, + span: false, +}; + +Text.propTypes = { + className: PropTypes.string, + style: PropTypes.shape({}), + variant: PropTypes.oneOf(['h1', 'h2', 's1', 'b1', 'b2', 'b3']), + weight: PropTypes.oneOf(['light', 'normal', 'medium', 'bold']), + primary: PropTypes.bool, + span: PropTypes.bool, + children: PropTypes.node.isRequired, +}; + +export default Text; diff --git a/src/app/atoms/text/Text.scss b/src/app/atoms/text/Text.scss new file mode 100644 index 00000000..256bf6ea --- /dev/null +++ b/src/app/atoms/text/Text.scss @@ -0,0 +1,61 @@ +@mixin font($type) { + font-size: var(--fs-#{$type}); + letter-spacing: var(--ls-#{$type}); + line-height: var(--lh-#{$type}); + + & img.emoji, + & img[data-mx-emoticon] { + height: calc(var(--lh-#{$type}) - .25rem); + } +} + +.text { + margin: 0; + padding: 0; + color: var(--tc-surface-high); + + & img.emoji, + & img[data-mx-emoticon] { + margin: 0 !important; + margin-right: 2px !important; + padding: 0 !important; + position: relative; + top: -.1rem; + vertical-align: middle; + } +} + +.text-light { + font-weight: var(--fw-light); +} +.text-normal { + font-weight: var(--fw-normal); +} +.text-medium { + font-weight: var(--fw-medium); +} +.text-bold { + font-weight: var(--fw-bold); +} + +.text-h1 { + @include font(h1); +} +.text-h2 { + @include font(h2); +} +.text-s1 { + @include font(s1); +} +.text-b1 { + @include font(b1); + color: var(--tc-surface-normal); +} +.text-b2 { + @include font(b2); + color: var(--tc-surface-normal); +} +.text-b3 { + @include font(b3); + color: var(--tc-surface-low); +} \ No newline at end of file diff --git a/src/app/atoms/time/Time.jsx b/src/app/atoms/time/Time.jsx new file mode 100644 index 00000000..d7bbe439 --- /dev/null +++ b/src/app/atoms/time/Time.jsx @@ -0,0 +1,63 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import dateFormat from 'dateformat'; +import { isInSameDay } from '../../../util/common'; + +/** + * Renders a formatted timestamp. + * + * Displays the time in hour:minute format if the message is from today or yesterday, unless `fullTime` is true. + * For older messages, it shows the date and time. + * + * @param {number} timestamp - The timestamp to display. + * @param {boolean} [fullTime=false] - If true, always show the full date and time. + * @param {boolean} hour24Clock - Whether to use 24-hour time format. + * @param {string} dateFormatString - Format string for the date part. + * @returns {JSX.Element} A