@cryptotaxi247 / CoPilot / commits / fc8d63d4

Template update (#271)

* comment * update: assets scss * update: auth components * update: notifications list * update: common components * update: stores and composables * update: navbar * update: toolbar * update: layout components * update: auth page * update: entry point component * update: tailwind configuration * update: dependencies * improve: components scss * chore: remove unnecessary code for now --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed Jul 10, 2024 at 12:35 UTC fc8d63d4f7b31df7ef994b96b0708b0047a26ab1
66 files changed +1600 -1301
frontend/package-lock.json
+89 -131
@@ -79,7 +79,7 @@
79 "picocolors": "^1.0.1",
80 "postcss": "^8.4.39",
81 "prettier": "^3.3.2",
82 - "sass": "^1.77.6",
82 + "sass": "^1.77.7",
83 "start-server-and-test": "^2.0.4",
84 "tailwind-config-viewer": "^2.0.4",
85 "tailwindcss": "^3.4.4",
@@ -91,7 +91,7 @@
91 "vite-bundle-visualizer": "^1.2.1",
92 "vite-plugin-vue-devtools": "^7.3.5",
93 "vite-svg-loader": "^5.1.0",
94 - "vitest": "^2.0.1",
94 + "vitest": "^2.0.2",
95 "vue-tsc": "^2.0.26"
96 },
97 "engines": {
@@ -1647,18 +1647,6 @@
1647 "url": "https://github.com/chalk/strip-ansi?sponsor=1"
1648 }
1649 },
1650 - "node_modules/@jest/schemas": {
1651 - "version": "29.6.3",
1652 - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
1653 - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
1654 - "dev": true,
1655 - "dependencies": {
1656 - "@sinclair/typebox": "^0.27.8"
1657 - },
1658 - "engines": {
1659 - "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
1660 - }
1661 - },
1650 "node_modules/@jridgewell/gen-mapping": {
1651 "version": "0.3.5",
1652 "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
@@ -1692,9 +1680,9 @@
1680 }
1681 },
1682 "node_modules/@jridgewell/sourcemap-codec": {
1695 - "version": "1.4.15",
1696 - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
1697 - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
1683 + "version": "1.5.0",
1684 + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
1685 + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
1686 },
1687 "node_modules/@jridgewell/trace-mapping": {
1688 "version": "0.3.25",
@@ -2092,12 +2080,6 @@
2080 "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
2081 "dev": true
2082 },
2095 - "node_modules/@sinclair/typebox": {
2096 - "version": "0.27.8",
2097 - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
2098 - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
2099 - "dev": true
2100 - },
2083 "node_modules/@trysound/sax": {
2084 "version": "0.2.0",
2085 "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
@@ -2853,26 +2835,39 @@
2835 }
2836 },
2837 "node_modules/@vitest/expect": {
2856 - "version": "2.0.1",
2857 - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.1.tgz",
2858 - "integrity": "sha512-yw70WL3ZwzbI2O3MOXYP2Shf4vqVkS3q5FckLJ6lhT9VMMtDyWdofD53COZcoeuHwsBymdOZp99r5bOr5g+oeA==",
2838 + "version": "2.0.2",
2839 + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.2.tgz",
2840 + "integrity": "sha512-nKAvxBYqcDugYZ4nJvnm5OR8eDJdgWjk4XM9owQKUjzW70q0icGV2HVnQOyYsp906xJaBDUXw0+9EHw2T8e0mQ==",
2841 + "dev": true,
2842 + "dependencies": {
2843 + "@vitest/spy": "2.0.2",
2844 + "@vitest/utils": "2.0.2",
2845 + "chai": "^5.1.1",
2846 + "tinyrainbow": "^1.2.0"
2847 + },
2848 + "funding": {
2849 + "url": "https://opencollective.com/vitest"
2850 + }
2851 + },
2852 + "node_modules/@vitest/pretty-format": {
2853 + "version": "2.0.2",
2854 + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.2.tgz",
2855 + "integrity": "sha512-SBCyOXfGVvddRd9r2PwoVR0fonQjh9BMIcBMlSzbcNwFfGr6ZhOhvBzurjvi2F4ryut2HcqiFhNeDVGwru8tLg==",
2856 "dev": true,
2857 "dependencies": {
2861 - "@vitest/spy": "2.0.1",
2862 - "@vitest/utils": "2.0.1",
2863 - "chai": "^5.1.1"
2858 + "tinyrainbow": "^1.2.0"
2859 },
2860 "funding": {
2861 "url": "https://opencollective.com/vitest"
2862 }
2863 },
2864 "node_modules/@vitest/runner": {
2870 - "version": "2.0.1",
2871 - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.1.tgz",
2872 - "integrity": "sha512-XfcSXOGGxgR2dQ466ZYqf0ZtDLLDx9mZeQcKjQDLQ9y6Cmk2Wl7wxMuhiYK4Fo1VxCtLcFEGW2XpcfMuiD1Maw==",
2865 + "version": "2.0.2",
2866 + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.2.tgz",
2867 + "integrity": "sha512-OCh437Vi8Wdbif1e0OvQcbfM3sW4s2lpmOjAE7qfLrpzJX2M7J1IQlNvEcb/fu6kaIB9n9n35wS0G2Q3en5kHg==",
2868 "dev": true,
2869 "dependencies": {
2875 - "@vitest/utils": "2.0.1",
2870 + "@vitest/utils": "2.0.2",
2871 "pathe": "^1.1.2"
2872 },
2873 "funding": {
@@ -2880,23 +2875,23 @@
2875 }
2876 },
2877 "node_modules/@vitest/snapshot": {
2883 - "version": "2.0.1",
2884 - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.1.tgz",
2885 - "integrity": "sha512-rst79a4Q+J5vrvHRapdfK4BdqpMH0eF58jVY1vYeBo/1be+nkyenGI5SCSohmjf6MkCkI20/yo5oG+0R8qrAnA==",
2878 + "version": "2.0.2",
2879 + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.2.tgz",
2880 + "integrity": "sha512-Yc2ewhhZhx+0f9cSUdfzPRcsM6PhIb+S43wxE7OG0kTxqgqzo8tHkXFuFlndXeDMp09G3sY/X5OAo/RfYydf1g==",
2881 "dev": true,
2882 "dependencies": {
2883 + "@vitest/pretty-format": "2.0.2",
2884 "magic-string": "^0.30.10",
2889 - "pathe": "^1.1.2",
2890 - "pretty-format": "^29.7.0"
2885 + "pathe": "^1.1.2"
2886 },
2887 "funding": {
2888 "url": "https://opencollective.com/vitest"
2889 }
2890 },
2891 "node_modules/@vitest/spy": {
2897 - "version": "2.0.1",
2898 - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.1.tgz",
2899 - "integrity": "sha512-NLkdxbSefAtJN56GtCNcB4GiHFb5i9q1uh4V229lrlTZt2fnwsTyjLuWIli1xwK2fQspJJmHXHyWx0Of3KTXWA==",
2892 + "version": "2.0.2",
2893 + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.2.tgz",
2894 + "integrity": "sha512-MgwJ4AZtCgqyp2d7WcQVE8aNG5vQ9zu9qMPYQHjsld/QVsrvg78beNrXdO4HYkP0lDahCO3P4F27aagIag+SGQ==",
2895 "dev": true,
2896 "dependencies": {
2897 "tinyspy": "^3.0.0"
@@ -2906,15 +2901,15 @@
2901 }
2902 },
2903 "node_modules/@vitest/utils": {
2909 - "version": "2.0.1",
2910 - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.1.tgz",
2911 - "integrity": "sha512-STH+2fHZxlveh1mpU4tKzNgRk7RZJyr6kFGJYCI5vocdfqfPsQrgVC6k7dBWHfin5QNB4TLvRS0Ckly3Dt1uWw==",
2904 + "version": "2.0.2",
2905 + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.2.tgz",
2906 + "integrity": "sha512-pxCY1v7kmOCWYWjzc0zfjGTA3Wmn8PKnlPvSrsA643P1NHl1fOyXj2Q9SaNlrlFE+ivCsxM80Ov3AR82RmHCWQ==",
2907 "dev": true,
2908 "dependencies": {
2914 - "diff-sequences": "^29.6.3",
2909 + "@vitest/pretty-format": "2.0.2",
2910 "estree-walker": "^3.0.3",
2911 "loupe": "^3.1.1",
2917 - "pretty-format": "^29.7.0"
2912 + "tinyrainbow": "^1.2.0"
2913 },
2914 "funding": {
2915 "url": "https://opencollective.com/vitest"
@@ -3812,9 +3807,9 @@
3807 }
3808 },
3809 "node_modules/browserslist": {
3815 - "version": "4.23.1",
3816 - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz",
3817 - "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==",
3810 + "version": "4.23.2",
3811 + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz",
3812 + "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==",
3813 "dev": true,
3814 "funding": [
3815 {
@@ -3831,10 +3826,10 @@
3826 }
3827 ],
3828 "dependencies": {
3834 - "caniuse-lite": "^1.0.30001629",
3835 - "electron-to-chromium": "^1.4.796",
3829 + "caniuse-lite": "^1.0.30001640",
3830 + "electron-to-chromium": "^1.4.820",
3831 "node-releases": "^2.0.14",
3837 - "update-browserslist-db": "^1.0.16"
3832 + "update-browserslist-db": "^1.1.0"
3833 },
3834 "bin": {
3835 "browserslist": "cli.js"
@@ -4000,9 +3995,9 @@
3995 }
3996 },
3997 "node_modules/caniuse-lite": {
4003 - "version": "1.0.30001640",
4004 - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001640.tgz",
4005 - "integrity": "sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==",
3998 + "version": "1.0.30001641",
3999 + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001641.tgz",
4000 + "integrity": "sha512-Phv5thgl67bHYo1TtMY/MurjkHhV4EDaCosezRXgZ8jzA/Ub+wjxAvbGvjoFENStinwi5kCyOYV3mi5tOGykwA==",
4001 "dev": true,
4002 "funding": [
4003 {
@@ -4918,15 +4913,6 @@
4913 "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
4914 "dev": true
4915 },
4921 - "node_modules/diff-sequences": {
4922 - "version": "29.6.3",
4923 - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
4924 - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==",
4925 - "dev": true,
4926 - "engines": {
4927 - "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
4928 - }
4929 - },
4916 "node_modules/dir-glob": {
4917 "version": "3.0.1",
4918 "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -5084,9 +5070,9 @@
5070 "dev": true
5071 },
5072 "node_modules/electron-to-chromium": {
5087 - "version": "1.4.820",
5088 - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.820.tgz",
5089 - "integrity": "sha512-kK/4O/YunacfboFEk/BDf7VO1HoPmDudLTJAU9NmXIOSjsV7qVIX3OrI4REZo0VmdqhcpUcncQc6N8Q3aEXlHg==",
5073 + "version": "1.4.823",
5074 + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.823.tgz",
5075 + "integrity": "sha512-4h+oPeAiGQOHFyUJOqpoEcPj/xxlicxBzOErVeYVMMmAiXUXsGpsFd0QXBMaUUbnD8hhSfLf9uw+MlsoIA7j5w==",
5076 "dev": true
5077 },
5078 "node_modules/emoji-regex": {
@@ -7270,16 +7256,13 @@
7256 "dev": true
7257 },
7258 "node_modules/jackspeak": {
7273 - "version": "3.4.2",
7274 - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.2.tgz",
7275 - "integrity": "sha512-qH3nOSj8q/8+Eg8LUPOq3C+6HWkpUioIjDsq1+D4zY91oZvpPttw8GwtF1nReRYKXl+1AORyFqtm2f5Q1SB6/Q==",
7259 + "version": "3.4.3",
7260 + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
7261 + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
7262 "dev": true,
7263 "dependencies": {
7264 "@isaacs/cliui": "^8.0.2"
7265 },
7280 - "engines": {
7281 - "node": "14 >=14.21 || 16 >=16.20 || >=18"
7282 - },
7266 "funding": {
7267 "url": "https://github.com/sponsors/isaacs"
7268 },
@@ -8742,13 +8725,10 @@
8725 }
8726 },
8727 "node_modules/path-scurry/node_modules/lru-cache": {
8745 - "version": "10.4.2",
8746 - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.2.tgz",
8747 - "integrity": "sha512-voV4dDrdVZVNz84n39LFKDaRzfwhdzJ7akpyXfTMxCgRUp07U3lcJUXRlhTKP17rgt09sUzLi5iCitpEAr+6ug==",
8748 - "dev": true,
8749 - "engines": {
8750 - "node": "14 || 16 || 18 || 20 || >=22"
8751 - }
8728 + "version": "10.4.3",
8729 + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
8730 + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
8731 + "dev": true
8732 },
8733 "node_modules/path-to-regexp": {
8734 "version": "6.2.2",
@@ -9145,32 +9125,6 @@
9125 "url": "https://github.com/sponsors/sindresorhus"
9126 }
9127 },
9148 - "node_modules/pretty-format": {
9149 - "version": "29.7.0",
9150 - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
9151 - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
9152 - "dev": true,
9153 - "dependencies": {
9154 - "@jest/schemas": "^29.6.3",
9155 - "ansi-styles": "^5.0.0",
9156 - "react-is": "^18.0.0"
9157 - },
9158 - "engines": {
9159 - "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
9160 - }
9161 - },
9162 - "node_modules/pretty-format/node_modules/ansi-styles": {
9163 - "version": "5.2.0",
9164 - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
9165 - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
9166 - "dev": true,
9167 - "engines": {
9168 - "node": ">=10"
9169 - },
9170 - "funding": {
9171 - "url": "https://github.com/chalk/ansi-styles?sponsor=1"
9172 - }
9173 - },
9128 "node_modules/process": {
9129 "version": "0.11.10",
9130 "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
@@ -9279,12 +9233,6 @@
9233 }
9234 ]
9235 },
9282 - "node_modules/react-is": {
9283 - "version": "18.3.1",
9284 - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
9285 - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
9286 - "dev": true
9287 - },
9236 "node_modules/read-cache": {
9237 "version": "1.0.0",
9238 "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -9752,9 +9700,9 @@
9700 "dev": true
9701 },
9702 "node_modules/sass": {
9755 - "version": "1.77.6",
9756 - "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.6.tgz",
9757 - "integrity": "sha512-ByXE1oLD79GVq9Ht1PeHWCPMPB8XHpBuz1r85oByKHjZY6qV6rWnQovQzXJXuQ/XyE1Oj3iPk3lo28uzaRA2/Q==",
9703 + "version": "1.77.7",
9704 + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.7.tgz",
9705 + "integrity": "sha512-9ywH75cO+rLjbrZ6en3Gp8qAMwPGBapFtlsMJoDTkcMU/bSe5a6cjKVUn5Jr4Gzg5GbP3HE8cm+02pLCgcoMow==",
9706 "dev": true,
9707 "dependencies": {
9708 "chokidar": ">=3.0.0 <4.0.0",
@@ -10604,6 +10552,15 @@
10552 "node": "^18.0.0 || >=20.0.0"
10553 }
10554 },
10555 + "node_modules/tinyrainbow": {
10556 + "version": "1.2.0",
10557 + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz",
10558 + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==",
10559 + "dev": true,
10560 + "engines": {
10561 + "node": ">=14.0.0"
10562 + }
10563 + },
10564 "node_modules/tinyspy": {
10565 "version": "3.0.0",
10566 "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.0.tgz",
@@ -11141,15 +11098,15 @@
11098 }
11099 },
11100 "node_modules/vite-node": {
11144 - "version": "2.0.1",
11145 - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.1.tgz",
11146 - "integrity": "sha512-nVd6kyhPAql0s+xIVJzuF+RSRH8ZimNrm6U8ZvTA4MXv8CHI17TFaQwRaFiK75YX6XeFqZD4IoAaAfi9OR1XvQ==",
11101 + "version": "2.0.2",
11102 + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.2.tgz",
11103 + "integrity": "sha512-w4vkSz1Wo+NIQg8pjlEn0jQbcM/0D+xVaYjhw3cvarTanLLBh54oNiRbsT8PNK5GfuST0IlVXjsNRoNlqvY/fw==",
11104 "dev": true,
11105 "dependencies": {
11106 "cac": "^6.7.14",
11107 "debug": "^4.3.5",
11108 "pathe": "^1.1.2",
11152 - "picocolors": "^1.0.1",
11109 + "tinyrainbow": "^1.2.0",
11110 "vite": "^5.0.0"
11111 },
11112 "bin": {
@@ -11426,28 +11383,29 @@
11383 }
11384 },
11385 "node_modules/vitest": {
11429 - "version": "2.0.1",
11430 - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.1.tgz",
11431 - "integrity": "sha512-PBPvNXRJiywtI9NmbnEqHIhcXlk8mB0aKf6REQIaYGY4JtWF1Pg8Am+N0vAuxdg/wUSlxPSVJr8QdjwcVxc2Hg==",
11386 + "version": "2.0.2",
11387 + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.2.tgz",
11388 + "integrity": "sha512-WlpZ9neRIjNBIOQwBYfBSr0+of5ZCbxT2TVGKW4Lv0c8+srCFIiRdsP7U009t8mMn821HQ4XKgkx5dVWpyoyLw==",
11389 "dev": true,
11390 "dependencies": {
11391 "@ampproject/remapping": "^2.3.0",
11435 - "@vitest/expect": "2.0.1",
11436 - "@vitest/runner": "2.0.1",
11437 - "@vitest/snapshot": "2.0.1",
11438 - "@vitest/spy": "2.0.1",
11439 - "@vitest/utils": "2.0.1",
11392 + "@vitest/expect": "2.0.2",
11393 + "@vitest/pretty-format": "^2.0.2",
11394 + "@vitest/runner": "2.0.2",
11395 + "@vitest/snapshot": "2.0.2",
11396 + "@vitest/spy": "2.0.2",
11397 + "@vitest/utils": "2.0.2",
11398 "chai": "^5.1.1",
11399 "debug": "^4.3.5",
11400 "execa": "^8.0.1",
11401 "magic-string": "^0.30.10",
11402 "pathe": "^1.1.2",
11445 - "picocolors": "^1.0.1",
11403 "std-env": "^3.7.0",
11404 "tinybench": "^2.8.0",
11405 "tinypool": "^1.0.0",
11406 + "tinyrainbow": "^1.2.0",
11407 "vite": "^5.0.0",
11450 - "vite-node": "2.0.1",
11408 + "vite-node": "2.0.2",
11409 "why-is-node-running": "^2.2.2"
11410 },
11411 "bin": {
@@ -11462,8 +11420,8 @@
11420 "peerDependencies": {
11421 "@edge-runtime/vm": "*",
11422 "@types/node": "^18.0.0 || >=20.0.0",
11465 - "@vitest/browser": "2.0.1",
11466 - "@vitest/ui": "2.0.1",
11423 + "@vitest/browser": "2.0.2",
11424 + "@vitest/ui": "2.0.2",
11425 "happy-dom": "*",
11426 "jsdom": "*"
11427 },
frontend/package.json
+3 -3
@@ -101,7 +101,7 @@
101 "picocolors": "^1.0.1",
102 "postcss": "^8.4.39",
103 "prettier": "^3.3.2",
104 - "sass": "^1.77.6",
104 + "sass": "^1.77.7",
105 "start-server-and-test": "^2.0.4",
106 "tailwind-config-viewer": "^2.0.4",
107 "tailwindcss": "^3.4.4",
@@ -113,7 +113,7 @@
113 "vite-bundle-visualizer": "^1.2.1",
114 "vite-plugin-vue-devtools": "^7.3.5",
115 "vite-svg-loader": "^5.1.0",
116 - "vitest": "^2.0.1",
116 + "vitest": "^2.0.2",
117 "vue-tsc": "^2.0.26"
118 },
119 "optionalDependencies": {
@@ -137,4 +137,4 @@
137 "engines": {
138 "node": ">=18.0.0"
139 }
140 -}
140 +}
\ No newline at end of file
frontend/src/App.vue
+17 -15
@@ -1,58 +1,56 @@
1 <template>
2 <Provider>
3 - <component :is="layoutComponent" :class="[`theme-${themeName}`, `layout-${layout}`, themeName]">
3 + <component
4 + :is="layoutComponent"
5 + :class="[`theme-${themeName}`, `layout-${layoutComponentName}`, themeName, { 'opacity-0': loading }]"
6 + >
7 <RouterView v-slot="{ Component }">
8 <transition :name="`router-${routerTransition}`" mode="out-in" appear>
9 <component
10 :is="Component"
11 :key="forceRefresh"
9 - :class="[`theme-${themeName}`, `layout-${layout}`, themeName]"
12 + :class="[`theme-${themeName}`, `layout-${layoutComponentName}`, themeName]"
13 />
14 </transition>
15 </RouterView>
16 </component>
17
15 - <SplashScreen :loading="loading" />
18 + <SplashScreen :show="loading" />
19 <SearchDialog v-if="isLogged" />
17 - <!--
18 - <LayoutSettings />
19 - -->
20 </Provider>
21 </template>
22
23 <script lang="ts" setup>
24 -import { computed, onBeforeMount, ref, type Component } from "vue"
24 +import { computed, onBeforeMount, ref, watch, type Component } from "vue"
25 import { useMainStore } from "@/stores/main"
26 import { useAuthStore } from "@/stores/auth"
27 import { useThemeStore } from "@/stores/theme"
28 -import VerticalNav from "@/layouts/VerticalNav/index.vue"
28 import HorizontalNav from "@/layouts/HorizontalNav/index.vue"
29 import Blank from "@/layouts/Blank/index.vue"
30 import Provider from "@/layouts/common/Provider.vue"
31 import SplashScreen from "@/layouts/common/SplashScreen.vue"
33 -// import LayoutSettings from "@/components/common/LayoutSettings.vue"
32 import SearchDialog from "@/components/common/SearchDialog.vue"
33 import { Layout, RouterTransition, type ThemeName } from "@/types/theme.d"
36 -import { type RouteLocationNormalized, useRouter, useRoute } from "vue-router"
34 +import { type RouteLocationNormalized, useRoute, useRouter } from "vue-router"
35 import "@/assets/scss/index.scss"
36
37 const router = useRouter()
38 const loading = ref(true)
39
40 const layoutComponents = {
43 - VerticalNav,
41 HorizontalNav,
42 Blank
43 }
44
48 -const mainStore = useMainStore()
45 const themeStore = useThemeStore()
46 +const mainStore = useMainStore()
47 const authStore = useAuthStore()
48
52 -const forceLayout = ref<Layout | null>(null)
49 const forceRefresh = computed<number>(() => mainStore.forceRefresh)
50 +const forceLayout = ref<Layout | null>(null)
51 const layout = computed<Layout>(() => themeStore.layout)
55 -const layoutComponent = computed<Component>(() => layoutComponents[forceLayout.value || layout.value])
52 +const layoutComponentName = computed<Layout>(() => forceLayout.value || layout.value)
53 +const layoutComponent = computed<Component>(() => layoutComponents[layoutComponentName.value])
54 const routerTransition = computed<RouterTransition>(() => themeStore.routerTransition)
55 const themeName = computed<ThemeName>(() => themeStore.themeName)
56 const isLogged = computed(() => authStore.isLogged)
@@ -65,7 +63,11 @@ function checkForcedLayout(route: RouteLocationNormalized) {
63 }
64 }
65
68 -router.beforeEach(route => {
66 +watch(layoutComponentName, () => {
67 + loading.value = false
68 +})
69 +
70 +router.afterEach(route => {
71 checkForcedLayout(route)
72 })
73
frontend/src/assets/scss/apexchart-override.scss
+21 -1
@@ -9,5 +9,25 @@
9 }
10
11 .apexcharts-tooltip {
12 - backdrop-filter: blur(6px);
12 + backdrop-filter: blur(3px);
13 + box-shadow:
14 + 0px 5px 10px -5px rgba(0, 0, 0, 0.2),
15 + 0px 5px 20px 0px rgba(0, 0, 0, 0.2) !important;
16 +
17 + &.apexcharts-theme-dark {
18 + background: rgba(0, 0, 0, 0.5) !important;
19 + }
20 +
21 + &.apexcharts-theme-light {
22 + background: rgba(255, 255, 255, 0.5) !important;
23 + }
24 +}
25 +
26 +.direction-rtl {
27 + .apexcharts-legend {
28 + direction: ltr;
29 + }
30 + .apexcharts-title-text {
31 + direction: ltr;
32 + }
33 }
frontend/src/assets/scss/common.scss
+16 -6
@@ -1,6 +1,7 @@
1 +@import "@/assets/scss/mixin.scss";
2 +
3 .custom-label {
4 color: var(--label-color);
3 - margin-top: 12px;
5 font-size: 10px;
6 padding: 2px 6px;
7 border-radius: var(--border-radius-small);
@@ -8,10 +9,6 @@
9 display: inline-block;
10 font-weight: bold;
11
11 - &:not(:last-child) {
12 - margin-right: 10px;
13 - }
14 -
12 &::before {
13 content: "";
14 width: 100%;
@@ -21,7 +18,7 @@
18 position: absolute;
19 top: 0;
20 left: 0;
24 - z-index: -1;
21 + z-index: 0;
22 opacity: 0.15;
23 }
24 }
@@ -32,6 +29,19 @@
29 .page-min-wrapped {
30 min-height: calc(100svh - var(--toolbar-height) - var(--view-padding) - (var(--view-padding) / 2));
31 }
32 +.page-mobile-full {
33 + @media (max-width: 700px) {
34 + @include page-full-view;
35 + }
36 +}
37 +.direction-rtl {
38 + .page-mobile-full {
39 + @media (max-width: 700px) {
40 + right: calc(var(--view-padding) * -1);
41 + left: unset;
42 + }
43 + }
44 +}
45
46 .page {
47 .page-header {
frontend/src/assets/scss/functions.scss new
+35
@@ -0,0 +1,35 @@
1 +/*
2 +A simple little SCSS mixin for creating scrim gradients
3 +Inspired by Andreas Larson - https://github.com/larsenwork
4 +https://css-tricks.com/easing-linear-gradients/
5 +*/
6 +
7 +@function scrimGradient($startColor: $color-black, $direction: "to bottom") {
8 + $scrimCoordinates: (
9 + 0: 1,
10 + 19: 0.738,
11 + 34: 0.541,
12 + 47: 0.382,
13 + 56.5: 0.278,
14 + 65: 0.194,
15 + 73: 0.126,
16 + 80.2: 0.075,
17 + 86.1: 0.042,
18 + 91: 0.021,
19 + 95.2: 0.008,
20 + 98.2: 0.002,
21 + 100: 0
22 + );
23 +
24 + $hue: hue($startColor);
25 + $saturation: saturation($startColor);
26 + $lightness: lightness($startColor);
27 + $stops: ();
28 +
29 + @each $colorStop, $alphaValue in $scrimCoordinates {
30 + $stop: hsla($hue, $saturation, $lightness, $alphaValue) percentage($colorStop/100);
31 + $stops: append($stops, $stop, comma);
32 + }
33 +
34 + @return linear-gradient(unquote($direction), $stops);
35 +}
frontend/src/assets/scss/helpers.scss
+3 -3
@@ -2,7 +2,7 @@
2 /* Works on Firefox */
3
4 scrollbar-width: thin;
5 - scrollbar-color: var(--hover-010-color) var(--bg-sidebar);
5 + scrollbar-color: var(--hover-010-color) var(--bg-secondary-color);
6
7 /* Works on Chrome, Edge, and Safari */
8 &::-webkit-scrollbar,
@@ -12,14 +12,14 @@
12
13 &::-webkit-scrollbar-track,
14 ::-webkit-scrollbar-track {
15 - background: var(--bg-sidebar);
15 + background: var(--bg-secondary-color);
16 }
17
18 &::-webkit-scrollbar-thumb,
19 ::-webkit-scrollbar-thumb {
20 background-color: var(--hover-010-color);
21 border-radius: 20px;
22 - border: 2px solid var(--bg-sidebar);
22 + border: 2px solid var(--bg-secondary-color);
23 }
24 }
25
frontend/src/assets/scss/naive-override.scss
+257
@@ -64,3 +64,260 @@
64 .v-binder-follower-content {
65 max-width: calc(100vw - (var(--view-padding) * 2));
66 }
67 +
68 +.n-data-table {
69 + .n-data-table-resize-button {
70 + --n-merged-border-color: var(--divider-010-color) !important;
71 + }
72 +}
73 +
74 +.n-space.n-space--rtl {
75 + .n-layout {
76 + aside.n-layout-sider.n-layout-sider--left-placement.n-layout-sider--bordered {
77 + .n-layout-toggle-button {
78 + right: unset;
79 + left: 0;
80 + transform: translateX(-50%) translateY(-50%) rotate(180deg);
81 + }
82 + .n-layout-sider__border {
83 + right: unset;
84 + left: 0;
85 + }
86 + }
87 + }
88 +}
89 +
90 +.direction-rtl {
91 + .v-binder-follower-container {
92 + direction: ltr;
93 +
94 + .v-binder-follower-content {
95 + & > * {
96 + direction: rtl;
97 + }
98 + }
99 + }
100 +
101 + .n-menu {
102 + direction: rtl;
103 +
104 + .n-menu-item {
105 + .n-menu-item-content {
106 + padding-left: unset;
107 + padding-right: 32px;
108 + gap: 8px;
109 +
110 + .n-menu-item-content__icon {
111 + margin-right: unset !important;
112 + }
113 + }
114 + }
115 +
116 + .n-menu-item-group {
117 + .n-menu-item-group-title {
118 + padding-right: 32px;
119 + padding-left: 0 !important;
120 + }
121 + }
122 +
123 + &.n-menu--collapsed {
124 + .n-menu-item {
125 + .n-menu-item-content {
126 + padding-right: 0px;
127 + direction: ltr;
128 + }
129 + }
130 + }
131 + }
132 +
133 + .n-base-select-menu {
134 + .n-base-select-option {
135 + .n-base-select-option__content {
136 + .n-icon {
137 + margin-right: unset !important;
138 + margin-left: 8px;
139 + }
140 + }
141 + }
142 + }
143 +
144 + .n-base-selection {
145 + .n-base-selection-label {
146 + .n-base-selection-input {
147 + .n-base-selection-input__content {
148 + .n-icon {
149 + margin-right: unset !important;
150 + margin-left: 8px;
151 + }
152 + }
153 + }
154 + }
155 + }
156 +
157 + .n-color-picker-panel {
158 + .n-color-picker-action {
159 + gap: 8px;
160 +
161 + & > * {
162 + margin-left: 0;
163 + }
164 + }
165 + }
166 +
167 + .n-cascader-menu {
168 + direction: ltr !important;
169 + }
170 +
171 + .n-rate {
172 + direction: ltr;
173 + }
174 +
175 + .n-carousel {
176 + direction: ltr;
177 + }
178 +
179 + .n-tabs {
180 + .n-tabs-nav {
181 + direction: ltr;
182 + }
183 + }
184 +
185 + .n-timeline {
186 + &:not(.n-timeline--horizontal) {
187 + .n-timeline-item .n-timeline-item-timeline {
188 + direction: ltr;
189 + }
190 + }
191 +
192 + &.n-timeline--horizontal {
193 + .n-timeline-item .n-timeline-item-timeline {
194 + .n-timeline-item-timeline__line {
195 + right: var(--n-icon-size);
196 + left: 0;
197 + }
198 + }
199 + }
200 + }
201 +
202 + .n-data-table-filter-menu__action {
203 + gap: 8px;
204 +
205 + .n-button--rtl {
206 + margin: 0 !important;
207 + }
208 + }
209 +
210 + .n-data-table-check-extra {
211 + right: unset !important;
212 + left: -3px;
213 + }
214 +
215 + .n-page-header {
216 + .n-page-header__main {
217 + gap: 16px;
218 +
219 + & > * {
220 + margin-right: 0;
221 + margin-left: 0;
222 + }
223 + }
224 + }
225 +
226 + .n-date-panel-calendar {
227 + direction: ltr;
228 + }
229 +
230 + .n-date-panel {
231 + .n-date-panel-header {
232 + gap: 10px;
233 +
234 + & > *:not(:last-child) {
235 + margin-right: 0;
236 + }
237 + }
238 +
239 + .n-date-panel-actions {
240 + .n-date-panel-actions__suffix {
241 + margin-bottom: 0;
242 + gap: 8px;
243 + flex-wrap: wrap;
244 +
245 + .n-button {
246 + margin-bottom: 0;
247 +
248 + &:not(:last-child) {
249 + margin-right: 0;
250 + }
251 + }
252 + }
253 + }
254 +
255 + .n-date-panel-month-calendar {
256 + direction: ltr;
257 + }
258 + }
259 +
260 + .n-time-picker-panel {
261 + direction: ltr !important;
262 + }
263 +
264 + .n-form {
265 + &.n-form--inline {
266 + gap: 18px;
267 +
268 + .n-form-item {
269 + margin-right: 0;
270 + }
271 + }
272 +
273 + .n-form-item.n-form-item--left-labelled {
274 + .n-form-item-label {
275 + padding-right: 0;
276 + padding-left: 12px;
277 + }
278 + }
279 + }
280 +
281 + .n-input-group {
282 + direction: ltr;
283 + justify-content: flex-end;
284 + }
285 +
286 + .n-slider {
287 + direction: ltr;
288 + }
289 +
290 + .n-transfer {
291 + direction: ltr;
292 +
293 + .n-transfer-list-item {
294 + &.n-transfer-list-item--source {
295 + gap: 8px;
296 +
297 + & > * {
298 + margin-right: 0 !important;
299 + }
300 + }
301 + }
302 + }
303 +
304 + .n-popover {
305 + .n-popover__content {
306 + .n-popconfirm__panel {
307 + .n-popconfirm__body {
308 + .n-popconfirm__icon {
309 + margin-right: 0;
310 + margin-left: 8px;
311 + }
312 + }
313 + .n-popconfirm__action {
314 + gap: 8px;
315 +
316 + .n-button:not(:last-child) {
317 + margin-right: 0px;
318 + }
319 + }
320 + }
321 + }
322 + }
323 +}
frontend/src/components/auth/AuthForm.vue
+8 -7
@@ -37,24 +37,23 @@ import Logo from "@/layouts/common/Logo.vue"
37 import { NButton } from "naive-ui"
38 import { ref, onBeforeMount, computed } from "vue"
39 import { useRouter } from "vue-router"
40 -import type { FormType } from "./types"
40 +import type { FormType } from "./types.d"
41
42 const props = defineProps<{
43 type?: FormType
44 useOnlyRouter?: boolean
45 }>()
46
47 -const router = useRouter()
48 -
47 const typeRef = ref<FormType>("signin")
50 -
51 -const isDark = computed<boolean>(() => useThemeStore().isThemeDark)
48 +const router = useRouter()
49 +const themeStore = useThemeStore()
50 +const isDark = computed<boolean>(() => themeStore.isThemeDark)
51 const title = computed<string>(() =>
52 typeRef.value === "signin"
53 ? "SOCFortress CoPilot"
54 : typeRef.value === "signup"
56 - ? "SOCFortress CoPilot"
57 - : "Forgot Password"
55 + ? "SOCFortress CoPilot"
56 + : "Forgot Password"
57 )
58
59 function gotoSignIn() {
@@ -63,12 +62,14 @@ function gotoSignIn() {
62 }
63 router.replace({ name: "Login" })
64 }
65 +
66 function gotoSignUp() {
67 if (!props.useOnlyRouter) {
68 typeRef.value = "signup"
69 }
70 router.replace({ name: "Register" })
71 }
72 +
73 function gotoForgotPassword() {
74 if (!props.useOnlyRouter) {
75 typeRef.value = "forgotpassword"
frontend/src/components/auth/ForgotPassword.vue
+1 -2
@@ -18,12 +18,11 @@
18
19 <script lang="ts" setup>
20 import { ref } from "vue"
21 -
21 import {
22 type FormInst,
23 type FormValidationError,
25 - useMessage,
24 type FormRules,
25 + useMessage,
26 NForm,
27 NFormItem,
28 NInput,
frontend/src/components/auth/SignIn.vue
+3 -3
@@ -37,12 +37,11 @@
37
38 <script lang="ts" setup>
39 import { ref } from "vue"
40 -
40 import {
41 type FormInst,
42 type FormValidationError,
44 - useMessage,
43 type FormRules,
44 + useMessage,
45 NForm,
46 NFormItem,
47 NInput,
@@ -71,6 +70,7 @@ const model = ref<ModelType>({
70 username: "",
71 password: ""
72 })
73 +const authStore = useAuthStore()
74
75 const rules: FormRules = {
76 username: [
@@ -100,7 +100,7 @@ function signIn(e: Event) {
100 password: model.value.password
101 }
102
103 - useAuthStore()
103 + authStore
104 .login(payload)
105 .then(() => {
106 router.push({ path: "/", replace: true })
frontend/src/components/auth/SignUp.vue
+5 -5
@@ -146,8 +146,8 @@ import { computed, ref } from "vue"
146 import {
147 type FormInst,
148 type FormValidationError,
149 - useMessage,
149 type FormRules,
150 + useMessage,
151 NForm,
152 NFormItem,
153 NInput,
@@ -177,16 +177,16 @@ interface ModelType {
177 */
178 }
179
180 +const emit = defineEmits<{
181 + (e: "goto-signin"): void
182 +}>()
183 +
184 const ArrowRightIcon = "carbon:arrow-right"
185 const ArrowLeftIcon = "carbon:arrow-left"
186 // const ImageIcon = "carbon:image"
187 // const RemoveImageIcon = "carbon:no-image"
188 const UserAddIcon = "carbon:user-admin"
189
186 -const emit = defineEmits<{
187 - (e: "goto-signin"): void
188 -}>()
189 -
190 const wizardCurrent = ref(1)
191 const loading = ref(false)
192 const formRef = ref<FormInst | null>(null)
frontend/src/components/cards/CardActions.vue
+5 -5
@@ -8,13 +8,13 @@
8 <div class="subtitle" v-if="!hideSubtitle && subtitle">{{ subtitle }}</div>
9 </template>
10 <template #header-extra>
11 - <n-dropdown :options="menuOptions" placement="bottom-end" @select="menuSelect" v-if="!hideMenu">
12 - <Icon :size="20" :name="MenuIcon" class="ml-3" />
11 + <n-dropdown :options="menuOptions" placement="bottom-end" to="body" @select="menuSelect" v-if="!hideMenu">
12 + <Icon :size="20" :name="MenuIcon" />
13 </n-dropdown>
14 </template>
15 <template #default>
16 <div class="overflow-hidden w-full">
17 - <n-scrollbar x-scrollable style="width: 100%">
17 + <n-scrollbar x-scrollable class="!w-full" trigger="none">
18 <slot></slot>
19 </n-scrollbar>
20 </div>
@@ -71,7 +71,7 @@ const menuOptions = computed(() =>
71 key: "reload",
72 icon: renderIcon(ReloadIcon)
73 }
74 - ]
74 + ]
75 : [
76 {
77 label: "Collapse",
@@ -83,7 +83,7 @@ const menuOptions = computed(() =>
83 key: "reload",
84 icon: renderIcon(ReloadIcon)
85 }
86 - ]
86 + ]
87 )
88
89 function menuSelect(key: string) {
frontend/src/components/common/FileDrop.vue
-4
@@ -10,10 +10,6 @@
10 <script lang="ts" setup>
11 import { toRefs } from "vue"
12
13 -defineOptions({
14 - name: "FileDrop"
15 -})
16 -
13 const props = withDefaults(
14 defineProps<{
15 accept: string
frontend/src/components/common/ImageCropper.vue
+1 -1
@@ -3,7 +3,7 @@
3 <slot :openCropper="openCropper"></slot>
4
5 <n-modal v-model:show="showCropper">
6 - <n-card class="image-cropper-modal flex flex-col" content-style="padding:20px">
6 + <n-card class="image-cropper-modal flex flex-col" content-class="!p-5">
7 <div class="aspect-square">
8 <div class="upload-box" v-if="!img">
9 <n-upload
frontend/src/components/common/ImageLoader.vue
+2 -6
@@ -18,6 +18,8 @@
18 </template>
19
20 <script setup lang="ts">
21 +import { ref, watch } from "vue"
22 +
23 export interface ImageEvent {
24 height: number
25 }
@@ -25,12 +27,6 @@ export interface ImageLoadEvent extends Event {
27 path?: HTMLElement[]
28 }
29
28 -import { ref, watch } from "vue"
29 -
30 -defineOptions({
31 - name: "ImageLoader"
32 -})
33 -
30 const emit = defineEmits(["image-error", "image-loading", "image-loaded", "image-fallback"])
31
32 const props = defineProps({
frontend/src/components/common/LayoutSettings.vue
+91 -58
@@ -24,7 +24,7 @@
24 />
25 <n-color-picker v-model:value="lightColor" :modes="['hex']" :show-alpha="false" v-else />
26 </div>
27 - <div class="palette flex justify-between">
27 + <div class="palette flex justify-around mt-3">
28 <n-button text v-for="color of palette" :key="color.light" @click="setPrimary(color)">
29 <template #icon>
30 <Icon
@@ -76,14 +76,16 @@
76 </div>
77 <div class="flex items-center gap-2">
78 <div class="basis-1/2">
79 - <n-button
79 + <!--
80 + <n-button
81 class="!w-full"
82 @click="layout = Layout.VerticalNav"
83 :type="layout === Layout.VerticalNav ? 'primary' : 'default'"
84 :disabled="isMobileView"
84 - >
85 - Vertical
86 - </n-button>
85 + >
86 + Vertical
87 + </n-button>
88 + -->
89 </div>
90 <div class="basis-1/2">
91 <n-button
@@ -102,14 +104,14 @@
104 <div class="flex justify-between items-center">
105 <div class="switch-label">
106 View boxed
105 - <span v-if="isMobileView" class="text-secondary-color">(desktop only)</span>
107 + <span v-if="isMobileView" class="text-secondary-color px-1">desktop only</span>
108 </div>
109 <n-switch v-model:value="boxed" :disabled="isMobileView" size="small" />
110 </div>
111 <div class="flex justify-between items-center">
112 <div class="switch-label">
113 Toolbar boxed
112 - <span v-if="isMobileView" class="text-secondary-color">(desktop only)</span>
114 + <span v-if="isMobileView" class="text-secondary-color px-1">desktop only</span>
115 </div>
116 <n-switch
117 v-model:value="toolbarBoxed"
@@ -121,6 +123,13 @@
123 <div class="switch-label">Footer visible</div>
124 <n-switch v-model:value="footerShown" size="small" />
125 </div>
126 + <div class="flex justify-between items-center">
127 + <div class="switch-label">
128 + RTL
129 + <span class="text-secondary-color px-1">beta</span>
130 + </div>
131 + <n-switch v-model:value="rtl" size="small" />
132 + </div>
133 </div>
134 </div>
135
@@ -154,18 +163,9 @@ import { computed, ref } from "vue"
163 import { NColorPicker, NButton, NSelect, useOsTheme, NScrollbar, NSwitch } from "naive-ui"
164 import { useThemeStore } from "@/stores/theme"
165 import Icon from "@/components/common/Icon.vue"
157 -
166 import { Layout, RouterTransition, ThemeEnum } from "@/types/theme.d"
167 import { useWindowSize } from "@vueuse/core"
168
161 -const SettingsIcon = "carbon:settings-adjust"
162 -const CloseIcon = "carbon:close"
163 -const LightIcon = "ion:sunny"
164 -const DarkIcon = "ion:moon"
165 -const LightOutlineIcon = "ion:sunny-outline"
166 -const DarkOutlineIcon = "ion:moon-outline"
167 -const ColorIcon = "carbon:circle-solid"
168 -
169 interface ColorPalette {
170 light: string
171 dark: string
@@ -173,12 +173,17 @@ interface ColorPalette {
173
174 type Palette = ColorPalette[]
175
176 -const store = useThemeStore()
176 +const SettingsIcon = "carbon:settings-adjust"
177 +const CloseIcon = "carbon:close"
178 +const LightIcon = "ion:sunny"
179 +const DarkIcon = "ion:moon"
180 +const LightOutlineIcon = "ion:sunny-outline"
181 +const DarkOutlineIcon = "ion:moon-outline"
182 +const ColorIcon = "carbon:circle-solid"
183
184 +const themeStore = useThemeStore()
185 const { width: winWidth } = useWindowSize()
179 -
186 const isMobileView = computed<boolean>(() => winWidth.value < 700)
181 -
187 const open = ref(false)
188 const transitionOptions = [
189 {
@@ -204,43 +209,48 @@ const transitionOptions = [
209 ]
210
211 const layout = computed({
207 - get: () => store.layout,
208 - set: val => store.setLayout(val)
212 + get: () => themeStore.layout,
213 + set: val => themeStore.setLayout(val)
214 })
215
216 const routerTransition = computed({
212 - get: () => store.routerTransition,
213 - set: val => store.setRouterTransition(val)
217 + get: () => themeStore.routerTransition,
218 + set: val => themeStore.setRouterTransition(val)
219 })
220
221 const theme = computed({
217 - get: () => store.themeName,
218 - set: val => store.setTheme(val)
222 + get: () => themeStore.themeName,
223 + set: val => themeStore.setTheme(val)
224 })
225
226 const darkColor = computed({
222 - get: () => store.darkPrimaryColor,
223 - set: val => store.setColor(ThemeEnum.Dark, "primary", val)
227 + get: () => themeStore.darkPrimaryColor,
228 + set: val => themeStore.setColor(ThemeEnum.Dark, "primary", val)
229 })
230
231 const lightColor = computed({
227 - get: () => store.lightPrimaryColor,
228 - set: val => store.setColor(ThemeEnum.Light, "primary", val)
232 + get: () => themeStore.lightPrimaryColor,
233 + set: val => themeStore.setColor(ThemeEnum.Light, "primary", val)
234 +})
235 +
236 +const rtl = computed({
237 + get: () => themeStore.isRTL,
238 + set: val => themeStore.setRTL(val)
239 })
240
241 const boxed = computed({
232 - get: () => store.isBoxed,
233 - set: val => store.setBoxed(val)
242 + get: () => themeStore.isBoxed,
243 + set: val => themeStore.setBoxed(val)
244 })
245
246 const toolbarBoxed = computed({
237 - get: () => store.isToolbarBoxed,
238 - set: val => store.setToolbarBoxed(val)
247 + get: () => themeStore.isToolbarBoxed,
248 + set: val => themeStore.setToolbarBoxed(val)
249 })
250
251 const footerShown = computed({
242 - get: () => store.isFooterShown,
243 - set: val => store.setFooterShow(val)
252 + get: () => themeStore.isFooterShown,
253 + set: val => themeStore.setFooterShow(val)
254 })
255
256 const palette = ref<Palette>([
@@ -252,25 +262,24 @@ const palette = ref<Palette>([
262 ])
263
264 function setPrimary(color: ColorPalette) {
255 - store.setColor(ThemeEnum.Dark, "primary", color.dark)
256 - store.setColor(ThemeEnum.Light, "primary", color.light)
265 + themeStore.setColor(ThemeEnum.Dark, "primary", color.dark)
266 + themeStore.setColor(ThemeEnum.Light, "primary", color.light)
267 }
268
269 function reset() {
260 - store.setColor(ThemeEnum.Dark, "primary", "#00E19B")
261 - store.setColor(ThemeEnum.Light, "primary", "#00B27B")
262 - store.setTheme(useOsTheme().value || ThemeEnum.Light)
263 - store.setLayout(Layout.VerticalNav)
264 - store.setRouterTransition(RouterTransition.FadeUp)
265 - store.setBoxed(true)
266 - store.setToolbarBoxed(true)
267 - store.setFooterShow(true)
270 + themeStore.setColor(ThemeEnum.Dark, "primary", "#00E19B")
271 + themeStore.setColor(ThemeEnum.Light, "primary", "#00B27B")
272 + themeStore.setTheme(useOsTheme().value || ThemeEnum.Light)
273 + themeStore.setLayout(Layout.HorizontalNav)
274 + themeStore.setRouterTransition(RouterTransition.FadeUp)
275 + themeStore.setRTL(false)
276 + themeStore.setBoxed(true)
277 + themeStore.setToolbarBoxed(true)
278 + themeStore.setFooterShow(true)
279 }
280 </script>
281
282 <style scoped lang="scss">
272 -@import "@/assets/scss/common.scss";
273 -
283 .layout-settings {
284 position: fixed;
285 right: 10px;
@@ -290,11 +299,13 @@ function reset() {
299 width: 100%;
300 height: 100%;
301 position: absolute;
302 + will-change: opacity;
303 }
304 .ls-form {
305 position: absolute;
306 height: 100%;
307 width: 100%;
308 + will-change: opacity;
309
310 .ls-header {
311 border-bottom: var(--border-small-050);
@@ -303,7 +314,6 @@ function reset() {
314 font-weight: 700;
315 padding: 10px 14px;
316 line-height: 1;
306 - padding-right: 8px;
317 transition: all 0.3s;
318
319 .ls-icon {
@@ -375,14 +385,6 @@ function reset() {
385 }
386 }
387 }
378 -
379 - .palette {
380 - margin: 8px 0;
381 - margin-bottom: 6px;
382 - padding: 0 4px;
383 - width: 94%;
384 - margin-left: 3%;
385 - }
388 }
389
390 &.ls-boxed-selection {
@@ -405,7 +407,7 @@ function reset() {
407
408 &.open {
409 width: 230px;
408 - height: 615px;
410 + height: 645px;
411 right: 16px;
412 border-radius: var(--border-radius);
413 max-height: 90vh;
@@ -425,7 +427,38 @@ function reset() {
427 .anim-enter-from,
428 .anim-leave-to {
429 opacity: 0;
428 - transform: translateY(1%);
430 + // transform: translateY(1%);
431 + }
432 +}
433 +
434 +.direction-rtl {
435 + .layout-settings {
436 + right: unset;
437 + left: 10px;
438 +
439 + .ls-form {
440 + .ls-header {
441 + direction: rtl;
442 + }
443 +
444 + :deep() {
445 + .n-color-picker {
446 + .n-color-picker-trigger {
447 + .n-color-picker-trigger__fill {
448 + .n-color-picker-trigger__value {
449 + margin-left: unset !important;
450 + margin-right: 44px;
451 + }
452 + }
453 + }
454 + }
455 + }
456 + }
457 +
458 + &.open {
459 + right: unset;
460 + left: 16px;
461 + }
462 }
463 }
464 </style>
frontend/src/components/common/LtrContext.vue new
+15
@@ -0,0 +1,15 @@
1 +<template>
2 + <n-config-provider class="ltr-context" :rtl="[]">
3 + <slot />
4 + </n-config-provider>
5 +</template>
6 +
7 +<script setup lang="ts">
8 +import { NConfigProvider } from "naive-ui"
9 +</script>
10 +
11 +<style scoped lang="scss">
12 +.ltr-context {
13 + direction: ltr;
14 +}
15 +</style>
frontend/src/components/common/Notifications/List.vue
+19 -7
@@ -9,11 +9,11 @@
9 >
10 <div class="icon-box">
11 <Icon :name="AlertIcon" :size="21" v-if="item.category === 'alert'"></Icon>
12 - <n-tooltip trigger="hover" style="padding: 0" placement="right" v-if="!item.read">
12 + <n-tooltip trigger="hover" class="!p-0" content-class="!p-0" placement="right" v-if="!item.read">
13 <template #trigger>
14 <div class="read-badge" @click.stop="setRead(item.id)"></div>
15 </template>
16 - Set as read
16 + <div class="px-3 py-2">Set as read</div>
17 </n-tooltip>
18 </div>
19 <div class="content grow">
@@ -78,11 +78,12 @@ function formatDatetime(date: Date | string) {
78 .notifications-list {
79 .item {
80 position: relative;
81 - padding: 14px 0;
81 + padding: 14px 12px;
82 + gap: 4px;
83
84 .icon-box {
84 - width: 70px;
85 - min-width: 70px;
85 + width: 45px;
86 + min-width: 45px;
87 display: flex;
88 justify-content: center;
89 position: relative;
@@ -100,7 +101,7 @@ function formatDatetime(date: Date | string) {
101 .read-badge {
102 position: absolute;
103 top: 5px;
103 - left: 14px;
104 + left: 0px;
105 width: 10px;
106 height: 10px;
107 border-radius: 50%;
@@ -110,7 +111,7 @@ function formatDatetime(date: Date | string) {
111 }
112
113 .content {
113 - padding-right: 20px;
114 + padding: 0 8px;
115 font-size: 14px;
116
117 .title {
@@ -204,4 +205,15 @@ function formatDatetime(date: Date | string) {
205 }
206 }
207 }
208 +
209 +.direction-rtl {
210 + .notifications-list {
211 + .item {
212 + .delete-btn {
213 + right: unset;
214 + left: 8px;
215 + }
216 + }
217 + }
218 +}
219 </style>
frontend/src/components/common/PageSplitted.vue deleted
-249
@@ -1,249 +0,0 @@
1 -<template>
2 - <div class="wrapper flex grow" :class="[{ 'sidebar-open': sidebarOpen }, `sidebar-position-${sidebarPosition}`]">
3 - <div class="sidebar flex flex-col" ref="sidebar">
4 - <div class="sidebar-header flex items-center">
5 - <slot name="sidebar-header"></slot>
6 - </div>
7 - <div class="sidebar-main grow">
8 - <n-scrollbar style="max-height: 100%">
9 - <div class="sidebar-main-content">
10 - <slot name="sidebar-content"></slot>
11 - </div>
12 - </n-scrollbar>
13 - </div>
14 - <div class="sidebar-footer flex items-center">
15 - <slot name="sidebar-footer"></slot>
16 - </div>
17 - </div>
18 -
19 - <div class="main flex-grow flex flex-col">
20 - <div class="main-toolbar flex items-center">
21 - <div class="menu-btn flex justify-center opacity-50">
22 - <n-button text @click="sidebarOpen = true">
23 - <Icon :size="24" :name="MenuIcon"></Icon>
24 - </n-button>
25 - </div>
26 -
27 - <div>
28 - <slot name="main-toolbar"></slot>
29 - </div>
30 - </div>
31 - <div class="main-view grow">
32 - <n-scrollbar style="max-height: 100%">
33 - <div class="main-content">
34 - <slot name="main-content"></slot>
35 - </div>
36 - </n-scrollbar>
37 - </div>
38 - <div class="main-footer flex items-center">
39 - <slot name="main-footer"></slot>
40 - </div>
41 - </div>
42 - </div>
43 -</template>
44 -
45 -<script setup lang="ts">
46 -import { NScrollbar, NButton } from "naive-ui"
47 -import Icon from "@/components/common/Icon.vue"
48 -
49 -const MenuIcon = "ion:menu-sharp"
50 -import { ref, toRefs } from "vue"
51 -import { onClickOutside } from "@vueuse/core"
52 -
53 -type SidebarPosition = "left" | "right"
54 -
55 -const props = withDefaults(
56 - defineProps<{
57 - sidebarPosition?: SidebarPosition
58 - }>(),
59 - { sidebarPosition: "left" }
60 -)
61 -const { sidebarPosition } = toRefs(props)
62 -
63 -const sidebarOpen = ref(false)
64 -const sidebar = ref(null)
65 -
66 -onClickOutside(sidebar, () => (sidebarOpen.value = false))
67 -</script>
68 -
69 -<style lang="scss" scoped>
70 -.wrapper {
71 - --mb-toolbar-height: 70px;
72 - position: relative;
73 - height: 100%;
74 - overflow: hidden;
75 - border-radius: var(--border-radius);
76 - border: 1px solid var(--border-color);
77 - background-color: var(--bg-color);
78 -
79 - .sidebar {
80 - background-color: var(--bg-secondary-color);
81 - min-width: 250px;
82 - width: 40%;
83 - max-width: 350px;
84 - overflow: hidden;
85 -
86 - .sidebar-header {
87 - border-block-end: var(--border-small-050);
88 - min-height: var(--mb-toolbar-height);
89 - padding: 0 30px;
90 - }
91 -
92 - .sidebar-main {
93 - overflow: hidden;
94 -
95 - .sidebar-main-content {
96 - padding: 30px;
97 - }
98 - }
99 -
100 - .sidebar-footer {
101 - border-block-start: var(--border-small-050);
102 - min-height: var(--mb-toolbar-height);
103 - padding: 0 30px;
104 - }
105 - }
106 -
107 - .main {
108 - background-color: var(--bg-color);
109 - position: relative;
110 - container-type: inline-size;
111 -
112 - .main-toolbar {
113 - border-block-end: var(--border-small-050);
114 - min-height: var(--mb-toolbar-height);
115 - padding: 0 30px;
116 - gap: 18px;
117 - line-height: 1.3;
118 -
119 - .menu-btn {
120 - display: none;
121 - }
122 - }
123 -
124 - .main-view {
125 - overflow: hidden;
126 -
127 - .main-content {
128 - padding: 30px;
129 - }
130 - }
131 -
132 - .main-footer {
133 - border-block-start: var(--border-small-050);
134 - min-height: var(--mb-toolbar-height);
135 - padding: 0 30px;
136 - }
137 - }
138 -
139 - &.sidebar-position-right {
140 - flex-direction: row-reverse;
141 - }
142 -
143 - @media (max-width: 700px) {
144 - --mb-toolbar-height: 62px;
145 - height: 100%;
146 - overflow: hidden;
147 - border-radius: 0;
148 - border: none;
149 -
150 - &::before {
151 - content: "";
152 - width: 100vw;
153 - display: block;
154 - background-color: var(--bg-body);
155 - position: absolute;
156 - top: 0;
157 - left: 0;
158 - bottom: 0;
159 - transform: translateX(-100%);
160 - opacity: 0;
161 - transition:
162 - opacity 0.25s ease-in-out,
163 - transform 0s linear 0.3s;
164 - z-index: 1;
165 - }
166 -
167 - .sidebar {
168 - position: absolute;
169 - top: 0;
170 - left: 0;
171 - bottom: 0;
172 - transform: translateX(-100%);
173 - transition: transform 0.25s ease-in-out;
174 - z-index: 1;
175 -
176 - &::before {
177 - content: "";
178 - width: 100%;
179 - height: 100%;
180 - display: block;
181 - background-color: var(--bg-color);
182 - z-index: -1;
183 - position: absolute;
184 - }
185 -
186 - .sidebar-header,
187 - .sidebar-footer {
188 - padding: 0 20px;
189 - }
190 -
191 - .sidebar-main {
192 - .sidebar-main-content {
193 - padding: 20px;
194 - }
195 - }
196 - }
197 - .main {
198 - .main-toolbar {
199 - padding: 0 20px;
200 - gap: 14px;
201 -
202 - .menu-btn {
203 - display: flex;
204 - }
205 - }
206 -
207 - .main-view {
208 - .main-content {
209 - padding: 20px;
210 - }
211 - }
212 - .main-footer {
213 - padding: 0 20px;
214 - }
215 - }
216 -
217 - &.sidebar-position-right {
218 - &::before,
219 - .sidebar {
220 - left: initial;
221 - right: 0;
222 - transform: translateX(100%);
223 - }
224 -
225 - .main {
226 - .main-toolbar {
227 - flex-direction: row-reverse;
228 - justify-content: space-between;
229 - }
230 - }
231 - }
232 -
233 - &.sidebar-open {
234 - &::before {
235 - transform: translateX(0);
236 - opacity: 0.4;
237 - transition:
238 - opacity 0.25s ease-in-out,
239 - transform 0s linear 0s;
240 - }
241 -
242 - .sidebar {
243 - transform: translateX(0);
244 - box-shadow: 0px 0px 80px 0px rgba(0, 0, 0, 0.1);
245 - }
246 - }
247 - }
248 -}
249 -</style>
frontend/src/components/common/Percentage.vue
+8 -8
@@ -1,6 +1,6 @@
1 <template>
2 <div
3 - class="percentage flex items-center"
3 + class="percentage flex items-center gap-2"
4 :class="[
5 {
6 color: useColor,
@@ -10,14 +10,14 @@
10 direction
11 ]"
12 >
13 - <span class="progress mr-3" v-if="progress && progress === 'line'">
13 + <span class="progress" v-if="progress && progress === 'line'">
14 <n-progress
15 type="line"
16 :status="direction === 'up' ? 'success' : 'error'"
17 :percentage="value"
18 :show-indicator="false"
19 :stroke-width="18"
20 - style="width: 50px"
20 + class="!w-12"
21 />
22 </span>
23 <span v-if="icon && icon === 'arrow'" class="flex items-center percentage-icon">
@@ -28,14 +28,14 @@
28 {{ direction === "up" ? "+" : "-" }}
29 </span>
30 <span>{{ value }}%</span>
31 - <span class="progress ml-3 flex items-center" v-if="progress && progress === 'circle'">
31 + <span class="progress flex items-center" v-if="progress && progress === 'circle'">
32 <n-progress
33 type="circle"
34 :status="direction === 'up' ? 'success' : 'error'"
35 :percentage="value"
36 :show-indicator="false"
37 :stroke-width="18"
38 - style="width: 22px"
38 + class="!w-5"
39 />
40 </span>
41 </div>
@@ -46,9 +46,6 @@ import { toRefs } from "vue"
46 import { NProgress } from "naive-ui"
47 import Icon from "@/components/common/Icon.vue"
48
49 -const ChevronUp = "tabler:chevron-up"
50 -const ChevronDown = "tabler:chevron-down"
51 -
49 export interface PercentageProps {
50 value: number
51 useColor?: boolean
@@ -67,6 +64,9 @@ const props = withDefaults(defineProps<PercentageProps>(), {
64 progress: false
65 })
66 const { value, useColor, useBackground, useOpacity, icon, progress, direction } = toRefs(props)
67 +
68 +const ChevronUp = "tabler:chevron-up"
69 +const ChevronDown = "tabler:chevron-down"
70 </script>
71
72 <style scoped lang="scss">
frontend/src/components/common/SearchDialog.vue
+18 -25
@@ -1,13 +1,6 @@
1 <template>
2 <n-modal v-model:show="showSearchBox" class="search-box-modal">
3 - <n-card
4 - style="width: 600px"
5 - content-style="padding: 0;"
6 - :bordered="false"
7 - size="huge"
8 - role="dialog"
9 - aria-modal="true"
10 - >
3 + <n-card content-class="!p-0" class="!w-150" :bordered="false" size="huge" role="dialog" aria-modal="true">
4 <div class="search-box" @keydown.up="prevItem()" @keydown.down="nextItem()">
5 <div class="search-input flex items-center">
6 <Icon :name="SearchIcon" :size="16"></Icon>
@@ -16,7 +9,7 @@
9 <Icon :name="CloseIcon" :size="20" @click="closeBox()" class="cursor-pointer"></Icon>
10 </div>
11 <n-divider />
19 - <n-scrollbar style="height: 400px" ref="scrollContent">
12 + <n-scrollbar class="!h-96" ref="scrollContent">
13 <div class="conten-wrap">
14 <div class="group" v-for="group of filteredGroups" :key="group.name">
15 <div class="group-title">{{ group.name }}</div>
@@ -31,7 +24,7 @@
24 >
25 <div class="icon">
26 <n-avatar v-if="item.iconImage" round :size="28" :src="item.iconImage" />
34 - <Icon :name="item.iconName" v-if="item.iconName" :size="18"></Icon>
27 + <Icon :name="item.iconName" v-if="item.iconName" :size="16"></Icon>
28 </div>
29 <div class="title grow">
30 <Highlighter
@@ -52,13 +45,13 @@
45 </n-scrollbar>
46 <n-divider />
47 <div class="hint-bar flex items-center justify-center">
55 - <div class="hint flex items-center justify-center">
48 + <div class="hint flex items-center justify-center gap-1">
49 <div class="icon">
50 <Icon :name="ArrowEnterIcon" :size="12"></Icon>
51 </div>
52 <span class="label">to select</span>
53 </div>
61 - <div class="hint flex items-center justify-center">
54 + <div class="hint flex items-center justify-center gap-1">
55 <div class="icon">
56 <Icon :name="ArrowSortIcon" :size="12"></Icon>
57 </div>
@@ -83,18 +76,6 @@ import Icon from "@/components/common/Icon.vue"
76 import { emitter } from "@/emitter"
77 import { useGoto } from "@/composables/useGoto"
78
86 -const SearchIcon = "ion:search-outline"
87 -const ArrowEnterIcon = "fluent:arrow-enter-left-24-regular"
88 -const ArrowSortIcon = "fluent:arrow-sort-24-regular"
89 -const FullScreenIcon = "fluent:full-screen-maximize-24-regular"
90 -const DarkModeIcon = "ion:moon-outline"
91 -const CloseIcon = "carbon:close"
92 -
93 -const ConnectorsIcon = "carbon:hybrid-networking"
94 -const AlertsIcon = "carbon:warning-hex"
95 -const SocAlertsIcon = "carbon:security"
96 -const CustomerIcon = "carbon:user-follow"
97 -
79 interface GroupItem {
80 iconName: string | null
81 iconImage: string | null
@@ -109,8 +90,20 @@ interface Group {
90 name: string
91 items: GroupItem[]
92 }
93 +
94 type Groups = Group[]
95
96 +const SearchIcon = "ion:search-outline"
97 +const ArrowEnterIcon = "fluent:arrow-enter-left-24-regular"
98 +const ArrowSortIcon = "fluent:arrow-sort-24-regular"
99 +const FullScreenIcon = "fluent:full-screen-maximize-24-regular"
100 +const DarkModeIcon = "ion:moon-outline"
101 +const CloseIcon = "carbon:close"
102 +const ConnectorsIcon = "carbon:hybrid-networking"
103 +const AlertsIcon = "carbon:warning-hex"
104 +const SocAlertsIcon = "carbon:security"
105 +const CustomerIcon = "carbon:user-follow"
106 +
107 const showSearchBox = ref(false)
108 const search = ref("")
109 const activeItem = ref<null | string | number>(null)
@@ -393,6 +386,7 @@ onMounted(() => {
386 font-size: 12px;
387 gap: 20px;
388 padding: 10px 0;
389 +
390 .icon {
391 background-color: var(--code-color);
392 width: 18px;
@@ -400,7 +394,6 @@ onMounted(() => {
394 padding-top: 1px;
395 text-align: center;
396 border-radius: 4px;
403 - margin-right: 5px;
397 display: flex;
398 align-items: center;
399 justify-content: center;
frontend/src/components/common/SegmentedPage.vue new
+442
@@ -0,0 +1,442 @@
1 +<template>
2 + <n-split
3 + direction="horizontal"
4 + :default-size="splitDefault"
5 + :min="0"
6 + :max="1"
7 + :resize-trigger-size="0"
8 + :disabled="!enableResize || splitDisabled"
9 + ref="splitPane"
10 + class="wrapper flex grow"
11 + :class="[{ 'sidebar-open': sidebarOpen }, `sidebar-position-${sidebarPosition}`]"
12 + >
13 + <template #1>
14 + <div class="sidebar flex flex-col" ref="sidebar" v-if="sidebarAvailable">
15 + <div class="sidebar-header flex items-center" v-if="$slots['sidebar-header']">
16 + <slot name="sidebar-header"></slot>
17 + </div>
18 + <div class="sidebar-main grow" v-if="$slots['sidebar-content']">
19 + <n-scrollbar class="max-h-full">
20 + <div class="sidebar-main-content" :style="sidebarContentStyle" :class="sidebarContentClass">
21 + <slot name="sidebar-content"></slot>
22 + </div>
23 + </n-scrollbar>
24 + </div>
25 + <div class="sidebar-footer flex items-center" v-if="$slots['sidebar-footer']">
26 + <slot name="sidebar-footer"></slot>
27 + </div>
28 + </div>
29 + </template>
30 +
31 + <template #resize-trigger>
32 + <div class="split-trigger">
33 + <div class="split-trigger-icon">
34 + <Icon :name="SplitIcon" :size="12"></Icon>
35 + </div>
36 + </div>
37 + </template>
38 +
39 + <template #2>
40 + <div class="main flex-grow flex flex-col">
41 + <div class="main-toolbar flex items-center" v-if="$slots['main-toolbar']">
42 + <div class="menu-btn flex justify-center opacity-50" v-if="sidebarAvailable && !hideMenuBtn">
43 + <n-button text @click="sidebarOpen = true">
44 + <Icon :size="24" :name="MenuIcon"></Icon>
45 + </n-button>
46 + </div>
47 +
48 + <div class="grow">
49 + <slot name="main-toolbar"></slot>
50 + </div>
51 + </div>
52 + <div class="main-view grow" :class="{ 'no-container-query': disableContainerQuery }">
53 + <n-scrollbar class="max-h-full" v-if="useMainScroll" ref="mainScrollbar">
54 + <div class="main-content" :style="mainContentStyle" :class="mainContentClass">
55 + <slot name="main-content"></slot>
56 + </div>
57 + </n-scrollbar>
58 + <div class="main-content" :style="mainContentStyle" :class="mainContentClass" v-else>
59 + <slot name="main-content"></slot>
60 + </div>
61 + </div>
62 + <div class="main-footer flex items-center" v-if="$slots['main-footer']">
63 + <div class="wrap">
64 + <slot name="main-footer"></slot>
65 + </div>
66 + </div>
67 +
68 + <div class="main-overlay" v-if="sidebarOpen"></div>
69 + </div>
70 + </template>
71 + </n-split>
72 +</template>
73 +
74 +<script setup lang="ts">
75 +import { computed, onMounted, ref, toRefs, useSlots, watch } from "vue"
76 +import { NScrollbar, NButton, NSplit } from "naive-ui"
77 +import { onClickOutside, useWindowSize } from "@vueuse/core"
78 +import Icon from "@/components/common/Icon.vue"
79 +
80 +type SidebarPosition = "left" | "right"
81 +
82 +export interface CtxSegmentedPage {
83 + mainScrollbar: typeof NScrollbar | null
84 + closeSidebar: () => void
85 + openSidebar: () => void
86 +}
87 +
88 +const emit = defineEmits<{
89 + (e: "mounted", value: CtxSegmentedPage): void
90 + (e: "sidebar", value: boolean): void
91 +}>()
92 +
93 +const props = withDefaults(
94 + defineProps<{
95 + sidebarPosition?: SidebarPosition
96 + hideMenuBtn?: boolean
97 + useMainScroll?: boolean
98 + mainContentStyle?: string
99 + mainContentClass?: string
100 + sidebarContentStyle?: string
101 + sidebarContentClass?: string
102 + enableResize?: boolean
103 + disableContainerQuery?: boolean
104 + }>(),
105 + { sidebarPosition: "left", useMainScroll: true }
106 +)
107 +const {
108 + sidebarPosition,
109 + hideMenuBtn,
110 + useMainScroll,
111 + mainContentStyle,
112 + mainContentClass,
113 + sidebarContentStyle,
114 + sidebarContentClass
115 +} = toRefs(props)
116 +
117 +const MenuIcon = "ph:list-light"
118 +const SplitIcon = "carbon:draggable"
119 +
120 +const splitPane = ref()
121 +const splitDefault = ref(0.3)
122 +const splitDisabled = ref(false)
123 +
124 +const slots = useSlots()
125 +const sidebarOpen = ref(false)
126 +const sidebar = ref(null)
127 +const mainScrollbar = ref<typeof NScrollbar | null>(null)
128 +const sidebarAvailable = computed(
129 + () => !!slots["sidebar-header"] || !!slots["sidebar-content"] || !!slots["sidebar-footer"]
130 +)
131 +
132 +onClickOutside(sidebar, () => closeSidebar())
133 +
134 +function closeSidebar() {
135 + sidebarOpen.value = false
136 +}
137 +
138 +function openSidebar() {
139 + sidebarOpen.value = true
140 +}
141 +
142 +watch(
143 + sidebarOpen,
144 + val => {
145 + emit("sidebar", val)
146 + },
147 + { immediate: true }
148 +)
149 +
150 +const { width } = useWindowSize()
151 +
152 +watch(
153 + width,
154 + val => {
155 + splitDefault.value = val <= 700 ? 0 : 0.3
156 + splitDisabled.value = val <= 700
157 + },
158 + { immediate: true }
159 +)
160 +
161 +onMounted(() => {
162 + emit("mounted", {
163 + mainScrollbar: mainScrollbar.value,
164 + closeSidebar,
165 + openSidebar
166 + })
167 +})
168 +</script>
169 +
170 +<style lang="scss" scoped>
171 +.wrapper {
172 + --mb-toolbar-height: 70px;
173 + position: relative;
174 + height: 100%;
175 + overflow: hidden;
176 + border-radius: var(--border-radius);
177 + border: 1px solid var(--border-color);
178 + background-color: var(--bg-color);
179 + direction: ltr;
180 +
181 + :deep() {
182 + .n-split-pane-1,
183 + .n-split-pane-2 {
184 + overflow: hidden;
185 + height: 100%;
186 + }
187 + .n-split-pane-1 {
188 + min-width: 250px;
189 + max-width: min(450px, calc(100% - 400px));
190 + }
191 + }
192 +
193 + .split-trigger {
194 + height: 100%;
195 + width: 3px;
196 + display: flex;
197 + align-items: center;
198 + position: relative;
199 + z-index: 1;
200 + left: -2px;
201 + transition: background-color 0.3s var(--bezier-ease);
202 +
203 + .split-trigger-icon {
204 + background-color: var(--border-color);
205 + border-radius: var(--border-radius-small);
206 + height: 18px;
207 + display: flex;
208 + justify-content: center;
209 + align-items: center;
210 + position: relative;
211 + margin-left: -5px;
212 + z-index: 1;
213 + transition: background-color 0.3s var(--bezier-ease);
214 + }
215 +
216 + &:hover {
217 + background-color: var(--primary-010-color);
218 +
219 + .split-trigger-icon {
220 + background-color: var(--primary-010-color);
221 + }
222 + }
223 + }
224 +
225 + .sidebar {
226 + background-color: var(--bg-secondary-color);
227 + min-width: 250px;
228 + //max-width: calc(100% - 400px);
229 + //width: 35%;
230 + height: 100%;
231 + overflow: hidden;
232 + border-right: 1px solid var(--border-color);
233 +
234 + .sidebar-header {
235 + border-block-end: var(--border-small-050);
236 + min-height: var(--mb-toolbar-height);
237 + height: var(--mb-toolbar-height);
238 + padding: 0 30px;
239 + }
240 +
241 + .sidebar-main {
242 + overflow: hidden;
243 +
244 + .sidebar-main-content {
245 + padding: 30px;
246 + }
247 + }
248 +
249 + .sidebar-footer {
250 + border-block-start: var(--border-small-050);
251 + min-height: var(--mb-toolbar-height);
252 + padding: 0 30px;
253 + }
254 + }
255 +
256 + .main {
257 + background-color: var(--bg-color);
258 + position: relative;
259 + height: 100%;
260 +
261 + .main-overlay {
262 + position: absolute;
263 + width: 100%;
264 + height: 100%;
265 + z-index: 2;
266 + top: 0;
267 + left: 0;
268 + }
269 +
270 + .main-toolbar {
271 + border-block-end: var(--border-small-050);
272 + min-height: var(--mb-toolbar-height);
273 + height: var(--mb-toolbar-height);
274 + padding: 0 30px;
275 + gap: 18px;
276 + line-height: 1.3;
277 + container-type: inline-size;
278 +
279 + .menu-btn {
280 + display: none;
281 + }
282 + }
283 +
284 + .main-view {
285 + overflow: hidden;
286 + &:not(.no-container-query) {
287 + container-type: inline-size;
288 + }
289 +
290 + .main-content {
291 + padding: 30px;
292 + }
293 + }
294 +
295 + .main-footer {
296 + container-type: inline-size;
297 + border-block-start: var(--border-small-050);
298 + padding: 0 30px;
299 +
300 + .wrap {
301 + min-height: calc(var(--mb-toolbar-height) - 1px);
302 + width: 100%;
303 + display: flex;
304 + align-items: center;
305 + }
306 + }
307 + }
308 +
309 + &.sidebar-position-right {
310 + flex-direction: row-reverse;
311 +
312 + .sidebar {
313 + border-right: none;
314 + border-left: 1px solid var(--border-color);
315 + }
316 + }
317 +
318 + @media (max-width: 700px) {
319 + --mb-toolbar-height: 62px;
320 + height: 100%;
321 + overflow: hidden;
322 + border-radius: 0;
323 + border: none;
324 +
325 + :deep() {
326 + .n-split-pane-1 {
327 + min-width: 0;
328 + max-width: 0;
329 + }
330 + }
331 +
332 + &::before {
333 + content: "";
334 + width: 100vw;
335 + display: block;
336 + background-color: var(--bg-body);
337 + position: absolute;
338 + top: 0;
339 + left: 0;
340 + bottom: 0;
341 + transform: translateX(-100%);
342 + opacity: 0;
343 + transition:
344 + opacity 0.25s ease-in-out,
345 + transform 0s linear 0.3s;
346 + z-index: 1;
347 + }
348 +
349 + .sidebar {
350 + position: absolute;
351 + top: 0;
352 + left: 0;
353 + bottom: 0;
354 + transform: translateX(-100%);
355 + transition: transform 0.25s ease-in-out;
356 + z-index: 3;
357 + min-width: 300px;
358 + max-width: min(450px, 80vw);
359 +
360 + &::before {
361 + content: "";
362 + width: 100%;
363 + height: 100%;
364 + display: block;
365 + background-color: var(--bg-color);
366 + z-index: -1;
367 + position: absolute;
368 + }
369 +
370 + .sidebar-header,
371 + .sidebar-footer {
372 + padding: 0 20px;
373 + }
374 +
375 + .sidebar-main {
376 + .sidebar-main-content {
377 + padding: 20px;
378 + }
379 + }
380 + }
381 + .main {
382 + .main-toolbar {
383 + padding: 0 20px;
384 + gap: 14px;
385 +
386 + .menu-btn {
387 + display: flex;
388 + }
389 + }
390 +
391 + .main-view {
392 + .main-content {
393 + padding: 20px;
394 + }
395 + }
396 + .main-footer {
397 + padding: 0 20px;
398 + }
399 + }
400 +
401 + &.sidebar-position-right {
402 + &::before,
403 + .sidebar {
404 + left: initial;
405 + right: 0;
406 + transform: translateX(100%);
407 + }
408 +
409 + .main {
410 + .main-toolbar {
411 + flex-direction: row-reverse;
412 + justify-content: space-between;
413 + }
414 + }
415 + }
416 +
417 + &.sidebar-open {
418 + &::before {
419 + transform: translateX(0);
420 + opacity: 0.4;
421 + transition:
422 + opacity 0.25s ease-in-out,
423 + transform 0s linear 0s;
424 + }
425 +
426 + .sidebar {
427 + transform: translateX(0);
428 + box-shadow: 0px 0px 80px 0px rgba(0, 0, 0, 0.1);
429 + }
430 + }
431 + }
432 +}
433 +
434 +.direction-rtl {
435 + .wrapper {
436 + .sidebar,
437 + .main {
438 + direction: rtl;
439 + }
440 + }
441 +}
442 +</style>
frontend/src/components/monitoringAlerts/Item.vue
+4 -5
@@ -92,11 +92,6 @@ const loading = computed(() => loadingDelete.value || loadingInvoke.value)
92
93 <style lang="scss" scoped>
94 .monitoring-alerts-item {
95 - &:not(.embedded) {
96 - border-radius: var(--border-radius);
97 - background-color: var(--bg-color);
98 - border: var(--border-small-050);
99 - }
95 border-top: var(--border-small-050);
96 transition: all 0.2s var(--bezier-ease);
97 min-height: 100px;
@@ -122,6 +117,10 @@ const loading = computed(() => loadingDelete.value || loadingInvoke.value)
117 }
118
119 &:not(.embedded) {
120 + border-radius: var(--border-radius);
121 + background-color: var(--bg-color);
122 + border: var(--border-small-050);
123 +
124 &:hover {
125 border-color: var(--primary-color);
126 }
frontend/src/components/soc/SocAlerts/SocAlertItem/SocAlertItem.vue
+4 -5
@@ -253,11 +253,6 @@ onBeforeMount(() => {
253
254 <style lang="scss" scoped>
255 .soc-alert-item {
256 - &:not(.embedded) {
257 - border-radius: var(--border-radius);
258 - background-color: var(--bg-color);
259 - border: var(--border-small-050);
260 - }
256 transition: all 0.2s var(--bezier-ease);
257
258 .soc-alert-info {
@@ -311,6 +306,10 @@ onBeforeMount(() => {
306 }
307
308 &:not(.embedded) {
309 + border-radius: var(--border-radius);
310 + background-color: var(--bg-color);
311 + border: var(--border-small-050);
312 +
313 &:hover,
314 &.highlight {
315 border-color: var(--primary-color);
frontend/src/components/soc/SocCases/SocCaseItem.vue
+4 -5
@@ -445,11 +445,6 @@ onBeforeMount(() => {
445
446 <style lang="scss" scoped>
447 .soc-case-item {
448 - &:not(.embedded) {
449 - border-radius: var(--border-radius);
450 - background-color: var(--bg-color);
451 - border: var(--border-small-050);
452 - }
448 border-top: var(--border-small-050);
449 transition: all 0.2s var(--bezier-ease);
450 min-height: 100px;
@@ -506,6 +501,10 @@ onBeforeMount(() => {
501 }
502
503 &:not(.embedded) {
504 + border-radius: var(--border-radius);
505 + background-color: var(--bg-color);
506 + border: var(--border-small-050);
507 +
508 &:hover {
509 border-color: var(--primary-color);
510 }
frontend/src/composables/useHideLayoutFooter.ts
+4 -4
@@ -6,14 +6,14 @@ import { onBeforeMount, onBeforeUnmount } from "vue"
6 // at the moment this worker around permit to hide Layout Footer
7
8 export function useHideLayoutFooter() {
9 - const store = useThemeStore()
9 + const themeStore = useThemeStore()
10
11 - if (store.isFooterShown) {
11 + if (themeStore.isFooterShown) {
12 onBeforeMount(() => {
13 - store.setFooterShow(false)
13 + themeStore.setFooterShow(false)
14 })
15 onBeforeUnmount(() => {
16 - store.setFooterShow(true)
16 + themeStore.setFooterShow(true)
17 })
18 }
19 }
frontend/src/composables/useStoreI18n.ts
+5 -4
@@ -3,19 +3,20 @@ import { useLocalesStore } from "@/stores/i18n"
3
4 export function useStoreI18n() {
5 const { t } = useI18n()
6 + const localesStore = useLocalesStore()
7
8 return {
9 initLocale: (): string => {
9 - return useLocalesStore().locale
10 + return localesStore.locale
11 },
12 getAvailableLocales: (): string[] => {
12 - return useLocalesStore().available
13 + return localesStore.available
14 },
15 getLocale: (): string => {
15 - return useLocalesStore().locale
16 + return localesStore.locale
17 },
18 setLocale: (newLocale: string): string => {
18 - useLocalesStore().setLocale(newLocale)
19 + localesStore.setLocale(newLocale)
20 return newLocale
21 },
22 t
frontend/src/composables/useThemeSwitch.ts
+3 -1
@@ -1,9 +1,11 @@
1 import { useThemeStore } from "@/stores/theme"
2
3 export function useThemeSwitch() {
4 + const themeStore = useThemeStore()
5 +
6 return {
7 toggle: () => {
6 - useThemeStore().toggleTheme()
8 + themeStore.toggleTheme()
9 }
10 }
11 }
frontend/src/design-tokens.json
+1 -1
@@ -164,4 +164,4 @@
164 "hover050": "rgba(255, 255, 255, 0.5)"
165 }
166 }
167 -}
167 +}
\ No newline at end of file
frontend/src/layouts/Blank/MainContainer.vue
+2 -5
@@ -14,14 +14,11 @@ import { NScrollbar } from "naive-ui"
14 import { useRoute, useRouter } from "vue-router"
15 import { useThemeStore } from "@/stores/theme"
16
17 -defineOptions({
18 - name: "MainContainer"
19 -})
20 -
17 +const themeStore = useThemeStore()
18 const router = useRouter()
19 const route = useRoute()
20 const routeName = computed<string>(() => route.name?.toString() || "")
24 -const boxed = computed(() => useThemeStore().isBoxed)
21 +const boxed = computed(() => themeStore.isBoxed)
22 const scrollbar = ref()
23
24 onMounted(() => {
frontend/src/layouts/Blank/index.vue
-4
@@ -8,10 +8,6 @@
8
9 <script lang="ts" setup>
10 import MainContainer from "./MainContainer.vue"
11 -
12 -defineOptions({
13 - name: "VerticalNav"
14 -})
11 </script>
12
13 <style lang="scss" scoped>
frontend/src/layouts/HorizontalNav/HeaderBar.vue
+15 -5
@@ -14,11 +14,8 @@ import Navbar from "@/layouts/common/Navbar/index.vue"
14 import Logo from "@/layouts/common/Logo.vue"
15 import { useThemeStore } from "@/stores/theme"
16
17 -defineOptions({
18 - name: "HeaderBar"
19 -})
20 -
21 -const isDark = computed<boolean>(() => useThemeStore().isThemeDark)
17 +const themeStore = useThemeStore()
18 +const isDark = computed<boolean>(() => themeStore.isThemeDark)
19 </script>
20
21 <style lang="scss" scoped>
@@ -44,4 +41,17 @@ const isDark = computed<boolean>(() => useThemeStore().isThemeDark)
41 display: none;
42 }
43 }
44 +
45 +.direction-rtl {
46 + .header-bar {
47 + direction: rtl;
48 + padding-left: 0;
49 + padding-right: var(--view-padding);
50 +
51 + .nav {
52 + margin-right: 0;
53 + margin-left: var(--view-padding);
54 + }
55 + }
56 +}
57 </style>
frontend/src/layouts/HorizontalNav/MainContainer.vue
+11 -20
@@ -5,7 +5,7 @@
5 <div class="view" :class="[{ boxed }, `route-${routeName}`]">
6 <slot></slot>
7 </div>
8 - <FooterEL :boxed="boxed" v-if="footerShown" />
8 + <MainFooter :boxed="boxed" v-if="footerShown" />
9 </n-scrollbar>
10 </div>
11 </template>
@@ -15,20 +15,17 @@ import { computed, ref, onMounted } from "vue"
15 import { NScrollbar } from "naive-ui"
16 import { useRoute, useRouter } from "vue-router"
17 import Toolbar from "@/layouts/common/Toolbar/index.vue"
18 -import FooterEL from "@/layouts/common/FooterEL.vue"
18 +import MainFooter from "@/layouts/common/MainFooter.vue"
19 import { useThemeStore } from "@/stores/theme"
20
21 -defineOptions({
22 - name: "MainContainer"
23 -})
24 -
21 +const themeStore = useThemeStore()
22 const router = useRouter()
23 const route = useRoute()
24 const routeName = computed<string>(() => route.name?.toString() || "")
28 -const sidebarCollapsed = computed<boolean>(() => useThemeStore().sidebar.collapsed)
29 -const footerShown = computed(() => useThemeStore().isFooterShown)
30 -const boxed = computed<boolean>(() => useThemeStore().isBoxed)
31 -const toolbarBoxed = computed(() => useThemeStore().isToolbarBoxed)
25 +const sidebarCollapsed = computed<boolean>(() => themeStore.sidebar.collapsed)
26 +const footerShown = computed(() => themeStore.isFooterShown)
27 +const boxed = computed<boolean>(() => themeStore.isBoxed)
28 +const toolbarBoxed = computed(() => themeStore.isToolbarBoxed)
29 const scrollbar = ref()
30
31 onMounted(() => {
@@ -82,22 +79,16 @@ onMounted(() => {
79 }
80
81 @media (max-width: $sidebar-bp) {
85 - padding-left: 0px;
82 + transition: all var(--sidebar-anim-ease) var(--sidebar-anim-duration);
83
84 .view {
85 padding-top: calc(var(--view-padding) / 2);
86 }
87
91 - &.sidebar-collapsed {
92 - padding-left: 0px;
93 - }
94 -
95 - transition: all var(--sidebar-anim-ease) var(--sidebar-anim-duration);
96 -
88 &.sidebar-opened {
89 //transform: scale(0.8) translateX(100%) rotateY(35deg);
99 - transform-origin: center left;
100 - border-radius: 16px;
90 + //transform-origin: center left;
91 + //border-radius: 16px;
92 overflow: hidden;
93 opacity: 0.5;
94 }
@@ -106,7 +97,7 @@ onMounted(() => {
97 @media (min-width: ($sidebar-bp + 1px)) {
98 :deep() {
99 header.toolbar {
109 - .logo {
100 + .logo-box {
101 display: none;
102 }
103 }
frontend/src/layouts/HorizontalNav/Sidebar.vue
+22 -11
@@ -24,13 +24,10 @@ import SidebarHeader from "./SidebarHeader.vue"
24 import SidebarFooter from "./SidebarFooter.vue"
25 import { useThemeStore } from "@/stores/theme"
26
27 -defineOptions({
28 - name: "Sidebar"
29 -})
30 -
27 +const themeStore = useThemeStore()
28 const sidebar = ref(null)
29 const sidebarHovered = useElementHover(sidebar)
33 -const sidebarCollapsed = computed<boolean>(() => useThemeStore().sidebar.collapsed)
30 +const sidebarCollapsed = computed<boolean>(() => themeStore.sidebar.collapsed)
31 const sidebarClosed = computed<boolean>(() => !sidebarHovered.value && sidebarCollapsed.value)
32
33 function clickListener() {
@@ -38,7 +35,7 @@ function clickListener() {
35 onClickOutside(sidebar, e => {
36 if (!sidebarCollapsed.value) {
37 e.stopPropagation()
41 - useThemeStore().closeSidebar()
38 + themeStore.closeSidebar()
39 }
40 })
41 }
@@ -74,7 +71,7 @@ onMounted(() => {
71 left: 0;
72 //bottom: 0;
73 //border-right:var(--border-small-100);
77 - padding-right: 1px;
74 + //padding-right: 1px;
75 width: var(--sidebar-open-width);
76 height: 100vh;
77 height: 100svh;
@@ -86,15 +83,14 @@ onMounted(() => {
83 box-shadow var(--sidebar-anim-ease) var(--sidebar-anim-duration),
84 color 0.3s var(--bezier-ease) 0s,
85 background-color 0.3s var(--bezier-ease) 0s;
86 + z-index: -1;
87 + transition: all 0.3s var(--bezier-ease) 0s;
88 + transform: translateX(-100%);
89
90 .sidebar-wrap {
91 overflow: hidden;
92 }
93
94 - z-index: -1;
95 - transition: all 0.3s var(--bezier-ease) 0s;
96 - transform: translateX(-100%);
97 -
94 @media (max-width: $sidebar-bp) {
95 &.opened {
96 z-index: 4;
@@ -107,4 +103,19 @@ onMounted(() => {
103 opacity: 0.15;
104 }
105 }
106 +
107 +.direction-rtl {
108 + .sidebar {
109 + left: unset;
110 + right: 0;
111 +
112 + @media (max-width: $sidebar-bp) {
113 + transform: translateX(100%);
114 +
115 + &.opened {
116 + transform: translateX(0%);
117 + }
118 + }
119 + }
120 +}
121 </style>
frontend/src/layouts/HorizontalNav/SidebarFooter.vue
+3 -8
@@ -10,12 +10,6 @@ import { NMenu } from "naive-ui"
10 import { useThemeStore } from "@/stores/theme"
11 import { renderIcon } from "@/utils"
12
13 -const ContactIcon = "ic:outline-alternate-email"
14 -
15 -defineOptions({
16 - name: "SidebarFooter"
17 -})
18 -
13 const props = withDefaults(
14 defineProps<{
15 collapsed?: boolean
@@ -24,6 +18,7 @@ const props = withDefaults(
18 )
19 const { collapsed } = toRefs(props)
20
21 +const ContactIcon = "ic:outline-alternate-email"
22 const menuOptions = ref([
23 {
24 label: () =>
@@ -40,8 +35,8 @@ const menuOptions = ref([
35 icon: renderIcon(ContactIcon)
36 }
37 ])
43 -
44 -const collapsedWidth = computed<number>(() => useThemeStore().sidebar.closeWidth - 16)
38 +const themeStore = useThemeStore()
39 +const collapsedWidth = computed<number>(() => themeStore.sidebar.closeWidth - 16)
40 </script>
41
42 <style lang="scss" scoped>
frontend/src/layouts/HorizontalNav/SidebarHeader.vue
+29 -10
@@ -31,26 +31,26 @@ import Icon from "@/components/common/Icon.vue"
31 import { Icon as Iconify } from "@iconify/vue"
32 import Logo from "@/layouts/common/Logo.vue"
33
34 -const CircleRegular = "fa6-regular:circle"
35 -const DotCircleRegular = "fa6-regular:circle-dot"
36 -const CloseOutline = "fa6-regular:circle-xmark"
37 -
34 const props = defineProps<{
35 logoMini?: boolean
36 }>()
37 const { logoMini } = toRefs(props)
38
39 +const CircleRegular = "fa6-regular:circle"
40 +const DotCircleRegular = "fa6-regular:circle-dot"
41 +const CloseOutline = "carbon:chevron-left"
42 +const themeStore = useThemeStore()
43 const showPin = computed<boolean>(() => !logoMini.value)
44 const sidebarCollapsed = computed({
45 get(): boolean {
46 - return useThemeStore().sidebar.collapsed
46 + return themeStore.sidebar.collapsed
47 },
48 set() {
49 - useThemeStore().toggleSidebar()
49 + themeStore.toggleSidebar()
50 }
51 })
52 -const isDark = computed<boolean>(() => useThemeStore().isThemeDark)
53 -const isLight = computed(() => useThemeStore().isThemeLight)
52 +const isDark = computed<boolean>(() => themeStore.isThemeDark)
53 +const isLight = computed(() => themeStore.isThemeLight)
54 </script>
55
56 <style lang="scss" scoped>
@@ -113,11 +113,18 @@ const isLight = computed(() => useThemeStore().isThemeLight)
113 :deep() {
114 .n-icon {
115 cursor: pointer;
116 - opacity: 0.3;
117 - transition: opacity var(--sidebar-anim-ease) var(--sidebar-anim-duration);
116 +
117 + .i-large {
118 + opacity: 0.3;
119 + transition: opacity var(--sidebar-anim-ease) var(--sidebar-anim-duration);
120 + }
121
122 &:hover {
123 opacity: 1;
124 +
125 + .i-large {
126 + opacity: 1;
127 + }
128 }
129 }
130 }
@@ -143,4 +150,16 @@ const isLight = computed(() => useThemeStore().isThemeLight)
150 }
151 }
152 }
153 +
154 +.direction-rtl {
155 + .sidebar-header {
156 + .sidebar-pin {
157 + .i-small {
158 + svg {
159 + transform: rotateY(180deg);
160 + }
161 + }
162 + }
163 + }
164 +}
165 </style>
frontend/src/layouts/HorizontalNav/index.vue
-4
@@ -13,10 +13,6 @@ import HeaderBar from "./HeaderBar.vue"
13 import Sidebar from "./Sidebar.vue"
14 import MainContainer from "./MainContainer.vue"
15 import "./main.scss"
16 -
17 -defineOptions({
18 - name: "VerticalNav"
19 -})
16 </script>
17
18 <style lang="scss" scoped>
frontend/src/layouts/VerticalNav/MainContainer.vue deleted
-103
@@ -1,103 +0,0 @@
1 -<template>
2 - <div id="main" class="main" :class="{ 'sidebar-collapsed': sidebarCollapsed, 'sidebar-opened': !sidebarCollapsed }">
3 - <n-scrollbar ref="scrollbar">
4 - <Toolbar :boxed="toolbarBoxed" />
5 - <div class="view" :class="[{ boxed }, `route-${routeName}`]">
6 - <slot></slot>
7 - </div>
8 - <FooterEL :boxed="boxed" v-if="footerShown" />
9 - </n-scrollbar>
10 - </div>
11 -</template>
12 -
13 -<script lang="ts" setup>
14 -import { computed, ref, onMounted } from "vue"
15 -import { NScrollbar } from "naive-ui"
16 -import { useRoute, useRouter } from "vue-router"
17 -import Toolbar from "@/layouts/common/Toolbar/index.vue"
18 -import FooterEL from "@/layouts/common/FooterEL.vue"
19 -import { useThemeStore } from "@/stores/theme"
20 -
21 -defineOptions({
22 - name: "MainContainer"
23 -})
24 -
25 -const router = useRouter()
26 -const route = useRoute()
27 -const routeName = computed<string>(() => route.name?.toString() || "")
28 -const sidebarCollapsed = computed(() => useThemeStore().sidebar.collapsed)
29 -const boxed = computed(() => useThemeStore().isBoxed)
30 -const footerShown = computed(() => useThemeStore().isFooterShown)
31 -const toolbarBoxed = computed(() => useThemeStore().isToolbarBoxed)
32 -const scrollbar = ref()
33 -
34 -onMounted(() => {
35 - router.afterEach(() => {
36 - if (scrollbar?.value?.scrollTo) {
37 - scrollbar?.value.scrollTo({ top: 0 })
38 - }
39 - })
40 -})
41 -</script>
42 -
43 -<style lang="scss" scoped>
44 -@import "./variables";
45 -
46 -.main {
47 - width: 100%;
48 - padding-left: var(--sidebar-open-width);
49 - position: relative;
50 - transition: padding var(--sidebar-anim-ease) var(--sidebar-anim-duration);
51 - background-color: var(--bg-body);
52 -
53 - :deep() {
54 - & > .n-scrollbar {
55 - & > .n-scrollbar-rail {
56 - top: calc(var(--toolbar-height) + 2px);
57 - }
58 - }
59 - }
60 - &.sidebar-collapsed {
61 - padding-left: var(--sidebar-close-width);
62 - }
63 -
64 - .view {
65 - padding: var(--view-padding);
66 - padding-top: calc(var(--view-padding) / 2);
67 - width: 100%;
68 -
69 - &.boxed {
70 - max-width: var(--boxed-width);
71 - margin: 0 auto;
72 - }
73 - }
74 -
75 - @media (max-width: $sidebar-bp) {
76 - padding-left: 0px;
77 - &.sidebar-collapsed {
78 - padding-left: 0px;
79 - }
80 -
81 - transition: all var(--sidebar-anim-ease) var(--sidebar-anim-duration);
82 -
83 - &.sidebar-opened {
84 - //transform: scale(0.8) translateX(100%) rotateY(35deg);
85 - //transform: scale(0.8) translateX(100%);
86 - transform-origin: center left;
87 - border-radius: 16px;
88 - overflow: hidden;
89 - opacity: 0.5;
90 - }
91 - }
92 -
93 - @media (min-width: ($sidebar-bp + 1px)) {
94 - :deep() {
95 - header.toolbar {
96 - .logo {
97 - display: none;
98 - }
99 - }
100 - }
101 - }
102 -}
103 -</style>
frontend/src/layouts/VerticalNav/Sidebar.vue deleted
-121
@@ -1,121 +0,0 @@
1 -<template>
2 - <aside
3 - id="sidebar"
4 - class="sidebar flex flex-col"
5 - :class="{ collapsed: sidebarCollapsed, opened: !sidebarCollapsed }"
6 - >
7 - <div class="sidebar-wrap grow flex flex-col" ref="sidebar">
8 - <SidebarHeader :logo-mini="sidebarClosed" />
9 - <n-scrollbar>
10 - <Navbar :collapsed="sidebarClosed"></Navbar>
11 - </n-scrollbar>
12 - <SidebarFooter :collapsed="sidebarClosed" />
13 - </div>
14 - </aside>
15 -</template>
16 -
17 -<script lang="ts" setup>
18 -import { computed, onMounted, ref, watch } from "vue"
19 -import { NScrollbar } from "naive-ui"
20 -import { isMobile } from "@/utils"
21 -import { onClickOutside, useElementHover } from "@vueuse/core"
22 -import Navbar from "@/layouts/common/Navbar/index.vue"
23 -import SidebarHeader from "./SidebarHeader.vue"
24 -import SidebarFooter from "./SidebarFooter.vue"
25 -import { useThemeStore } from "@/stores/theme"
26 -
27 -defineOptions({
28 - name: "Sidebar"
29 -})
30 -
31 -const sidebar = ref(null)
32 -const sidebarHovered = useElementHover(sidebar)
33 -
34 -const sidebarCollapsed = computed(() => useThemeStore().sidebar.collapsed)
35 -const sidebarClosed = computed(() => !sidebarHovered.value && sidebarCollapsed.value)
36 -
37 -function clickListener() {
38 - if (sidebar.value) {
39 - onClickOutside(sidebar, e => {
40 - if (!sidebarCollapsed.value) {
41 - e.stopPropagation()
42 - useThemeStore().closeSidebar()
43 - }
44 - })
45 - }
46 -}
47 -
48 -onMounted(() => {
49 - watch(
50 - sidebarCollapsed,
51 - val => {
52 - if (val) {
53 - if (isMobile()) {
54 - sidebarHovered.value = false
55 - }
56 - }
57 - },
58 - {
59 - immediate: true
60 - }
61 - )
62 -
63 - if (window.innerWidth <= 700) {
64 - clickListener()
65 - }
66 -})
67 -</script>
68 -
69 -<style lang="scss" scoped>
70 -@import "./variables";
71 -
72 -.sidebar {
73 - position: fixed;
74 - z-index: 4;
75 - top: 0;
76 - left: 0;
77 - //bottom: 0;
78 - //border-right:var(--border-small-100);
79 - padding-right: 1px;
80 - width: var(--sidebar-open-width);
81 - height: 100vh;
82 - height: 100svh;
83 - overflow-x: hidden;
84 - overflow-y: auto;
85 - background-color: var(--bg-sidebar);
86 - transition:
87 - width var(--sidebar-anim-ease) var(--sidebar-anim-duration),
88 - box-shadow var(--sidebar-anim-ease) var(--sidebar-anim-duration),
89 - color 0.3s var(--bezier-ease) 0s,
90 - background-color 0.3s var(--bezier-ease) 0s;
91 -
92 - .sidebar-wrap {
93 - overflow: hidden;
94 - }
95 -
96 - :deep(.n-scrollbar-rail) {
97 - opacity: 0.15;
98 - }
99 -
100 - &.collapsed {
101 - width: var(--sidebar-close-width);
102 -
103 - &:hover {
104 - width: var(--sidebar-open-width);
105 - box-shadow: 0px 0px 80px 0px rgba(0, 0, 0, 0.2);
106 - }
107 - }
108 -
109 - @media (max-width: $sidebar-bp) {
110 - z-index: -1;
111 - transition: all 0.3s var(--bezier-ease) 0s;
112 - transform: translateX(-100%);
113 -
114 - &.opened {
115 - z-index: 4;
116 - transform: translateX(0);
117 - box-shadow: 0px 0px 80px 0px rgba(0, 0, 0, 0.2);
118 - }
119 - }
120 -}
121 -</style>
frontend/src/layouts/VerticalNav/SidebarFooter.vue deleted
-63
@@ -1,63 +0,0 @@
1 -<template>
2 - <div class="sidebar-footer" :class="{ collapsed }">
3 - <n-menu :options="menuOptions" :collapsed="collapsed" :collapsed-width="collapsedWidth" />
4 - </div>
5 -</template>
6 -
7 -<script lang="ts" setup>
8 -import { computed, h, ref, toRefs } from "vue"
9 -import { NMenu } from "naive-ui"
10 -import { useThemeStore } from "@/stores/theme"
11 -import { renderIcon } from "@/utils"
12 -
13 -defineOptions({
14 - name: "SidebarFooter"
15 -})
16 -
17 -const ContactIcon = "ic:outline-alternate-email"
18 -
19 -const props = withDefaults(
20 - defineProps<{
21 - collapsed?: boolean
22 - }>(),
23 - { collapsed: false }
24 -)
25 -const { collapsed } = toRefs(props)
26 -
27 -const menuOptions = ref([
28 - {
29 - label: () =>
30 - h(
31 - "a",
32 - {
33 - href: "https://www.socfortress.co/contact_form.html",
34 - target: "_blank",
35 - rel: "noopenner noreferrer"
36 - },
37 - "Contact SOCFortress"
38 - ),
39 - key: "contact-socfortress",
40 - icon: renderIcon(ContactIcon)
41 - }
42 -])
43 -
44 -const collapsedWidth = computed<number>(() => useThemeStore().sidebar.closeWidth - 16)
45 -</script>
46 -
47 -<style lang="scss" scoped>
48 -.sidebar-footer {
49 - margin: 8px;
50 - background-color: var(--bg-body);
51 - border-radius: var(--border-radius);
52 - padding: 3px 0;
53 - transition: all 0.3s;
54 -
55 - :deep() {
56 - .n-menu {
57 - .n-menu-item-content.n-menu-item-content--selected::before {
58 - background-color: transparent !important;
59 - }
60 - }
61 - }
62 -}
63 -</style>
frontend/src/layouts/VerticalNav/SidebarHeader.vue deleted
-150
@@ -1,150 +0,0 @@
1 -<template>
2 - <div class="sidebar-header flex items-center justify-between">
3 - <div class="logo-box grow" :class="{ mini: logoMini }">
4 - <Transition name="fade" mode="out-in">
5 - <Logo :mini="false" :dark="true" class="anim-wrap" v-if="isDark && !logoMini" />
6 - <Logo :mini="false" :dark="false" class="anim-wrap" v-else-if="isLight && !logoMini" />
7 - <Logo :mini="true" :dark="true" class="anim-wrap" v-else-if="isDark && logoMini" />
8 - <Logo :mini="true" :dark="false" class="anim-wrap" v-else-if="isLight && logoMini" />
9 - </Transition>
10 - </div>
11 - <Transition name="fade" mode="out-in">
12 - <div class="sidebar-pin flex items-center" v-if="showPin">
13 - <Icon :size="20" @click="sidebarCollapsed = !sidebarCollapsed">
14 - <span class="i-large">
15 - <Iconify :icon="CircleRegular" v-if="sidebarCollapsed" />
16 - <Iconify :icon="DotCircleRegular" v-if="!sidebarCollapsed" />
17 - </span>
18 - <span class="i-small">
19 - <Iconify :icon="CloseOutline" v-if="!sidebarCollapsed" />
20 - </span>
21 - </Icon>
22 - </div>
23 - </Transition>
24 - </div>
25 -</template>
26 -
27 -<script lang="ts" setup>
28 -import { computed, toRefs } from "vue"
29 -import { useThemeStore } from "@/stores/theme"
30 -import Icon from "@/components/common/Icon.vue"
31 -import { Icon as Iconify } from "@iconify/vue"
32 -import Logo from "@/layouts/common/Logo.vue"
33 -
34 -defineOptions({
35 - name: "SidebarHeader"
36 -})
37 -
38 -const CircleRegular = "fa6-regular:circle"
39 -const DotCircleRegular = "fa6-regular:circle-dot"
40 -const CloseOutline = "fa6-regular:circle-xmark"
41 -
42 -const props = defineProps<{
43 - logoMini?: boolean
44 -}>()
45 -const { logoMini } = toRefs(props)
46 -
47 -const showPin = computed<boolean>(() => !logoMini.value)
48 -const sidebarCollapsed = computed({
49 - get(): boolean {
50 - return useThemeStore().sidebar.collapsed
51 - },
52 - set() {
53 - useThemeStore().toggleSidebar()
54 - }
55 -})
56 -const isDark = computed<boolean>(() => useThemeStore().isThemeDark)
57 -const isLight = computed(() => useThemeStore().isThemeLight)
58 -</script>
59 -
60 -<style lang="scss" scoped>
61 -@import "./variables";
62 -
63 -.sidebar-header {
64 - height: var(--toolbar-height);
65 - min-height: var(--toolbar-height);
66 -
67 - :deep() {
68 - .logo-box {
69 - height: 100%;
70 - width: 100%;
71 - position: relative;
72 -
73 - .anim-wrap {
74 - padding: 16px 0px;
75 - position: absolute;
76 - top: 0;
77 - left: 0;
78 - right: 0;
79 - bottom: 0;
80 - display: flex;
81 - align-items: center;
82 -
83 - img {
84 - max-height: 32px;
85 - display: block;
86 - height: calc(var(--toolbar-height) - 32px);
87 - transform: translateX(32px);
88 - transition: transform var(--sidebar-anim-ease) var(--sidebar-anim-duration);
89 - }
90 - }
91 -
92 - &.fade-enter-active,
93 - &.fade-leave-active {
94 - transition: opacity var(--sidebar-anim-ease) var(--sidebar-anim-duration);
95 - }
96 -
97 - &.fade-enter-from,
98 - &.fade-leave-to {
99 - opacity: 0;
100 - }
101 - }
102 -
103 - &.mini {
104 - width: 100%;
105 - .anim-wrap {
106 - img {
107 - transform: translateX(18px);
108 - }
109 - }
110 - }
111 - }
112 -
113 - .sidebar-pin {
114 - padding-right: 16px;
115 - height: 100%;
116 -
117 - :deep() {
118 - .n-icon {
119 - cursor: pointer;
120 - opacity: 0.3;
121 - transition: opacity var(--sidebar-anim-ease) var(--sidebar-anim-duration);
122 -
123 - &:hover {
124 - opacity: 1;
125 - }
126 - }
127 - }
128 -
129 - .i-large {
130 - display: block;
131 - }
132 - .i-small {
133 - display: none;
134 - }
135 - @media (max-width: $sidebar-bp) {
136 - .i-large {
137 - display: none;
138 - }
139 - .i-small {
140 - display: block;
141 - }
142 - }
143 -
144 - &.fade-enter-from,
145 - &.fade-leave-to {
146 - opacity: 0;
147 - }
148 - }
149 -}
150 -</style>
frontend/src/layouts/VerticalNav/_variables.scss deleted
-1
@@ -1 +0,0 @@
1 -$sidebar-bp: 700px;
frontend/src/layouts/VerticalNav/index.vue deleted
-27
@@ -1,27 +0,0 @@
1 -<template>
2 - <div class="layout flex">
3 - <Sidebar />
4 - <MainContainer class="grow">
5 - <slot></slot>
6 - </MainContainer>
7 - </div>
8 -</template>
9 -
10 -<script lang="ts" setup>
11 -import Sidebar from "./Sidebar.vue"
12 -import MainContainer from "./MainContainer.vue"
13 -
14 -defineOptions({
15 - name: "VerticalNav"
16 -})
17 -</script>
18 -
19 -<style lang="scss" scoped>
20 -.layout {
21 - width: 100vw;
22 - height: 100vh;
23 - height: 100svh;
24 - overflow: hidden;
25 - perspective: 1000px;
26 -}
27 -</style>
frontend/src/layouts/common/Logo.vue
+3 -6
@@ -17,10 +17,6 @@
17 import { useThemeStore } from "@/stores/theme"
18 import { computed, toRefs } from "vue"
19
20 -defineOptions({
21 - name: "Logo"
22 -})
23 -
20 const props = withDefaults(
21 defineProps<{
22 mini: boolean
@@ -30,8 +26,9 @@ const props = withDefaults(
26 )
27 const { mini, dark } = toRefs(props)
28
33 -const isDark = computed<boolean>(() => dark.value || useThemeStore().isThemeDark)
34 -const isLight = computed<boolean>(() => !dark.value || useThemeStore().isThemeLight)
29 +const themeStore = useThemeStore()
30 +const isDark = computed<boolean>(() => dark.value ?? themeStore.isThemeDark)
31 +const isLight = computed<boolean>(() => !dark.value || themeStore.isThemeLight)
32 </script>
33
34 <style lang="scss" scoped>
frontend/src/layouts/common/MainFooter.vue renamed
+2 -5
@@ -28,16 +28,13 @@ import Icon from "@/components/common/Icon.vue"
28 import BrainIcon from "@/assets/icons/brain-icon.svg"
29 import { useThemeStore } from "@/stores/theme"
30
31 -defineOptions({
32 - name: "FooterEL"
33 -})
34 -
31 const props = defineProps<{
32 boxed: boolean
33 }>()
34 const { boxed } = toRefs(props)
39 -const style = computed(() => useThemeStore().style)
35
36 +const themeStore = useThemeStore()
37 +const style = computed(() => themeStore.style)
38 const year = ref(new Date().getFullYear())
39 </script>
40
frontend/src/layouts/common/Navbar/index.vue
+73 -19
@@ -29,13 +29,6 @@ import { computed, onBeforeMount, ref, toRefs } from "vue"
29 import { useRouter, useRoute, type RouteRecordNormalized } from "vue-router"
30 import _uniq from "lodash/uniq"
31
32 -defineOptions({
33 - name: "Navbar"
34 -})
35 -
36 -const route = useRoute()
37 -const router = useRouter()
38 -
32 const props = withDefaults(
33 defineProps<{
34 mode?: "vertical" | "horizontal"
@@ -45,13 +38,17 @@ const props = withDefaults(
38 )
39 const { mode, collapsed } = toRefs(props)
40
41 +const route = useRoute()
42 +const router = useRouter()
43 const selectedKey = ref<string | null>(null)
44 const menu = ref<MenuInst | null>(null)
45 const expandedKeys = ref<string[] | undefined>(undefined)
46
52 -const menuOptions = computed<MenuMixedOption[]>(() => getItems(mode.value, collapsed.value))
53 -const collapsedWidth = computed<number>(() => useThemeStore().sidebar.closeWidth)
54 -const sidebarCollapsed = computed<boolean>(() => useThemeStore().sidebar.collapsed)
47 +const themeStore = useThemeStore()
48 +
49 +const menuOptions = computed<MenuMixedOption[]>(() => getItems())
50 +const collapsedWidth = computed<number>(() => themeStore.sidebar.closeWidth)
51 +const sidebarCollapsed = computed<boolean>(() => themeStore.sidebar.collapsed)
52
53 function setMenuKey(matched: RouteRecordNormalized[]) {
54 for (const match of matched) {
@@ -72,7 +69,7 @@ onBeforeMount(() => {
69 setMenuKey(route.matched)
70
71 if (window.innerWidth <= 700 && !sidebarCollapsed.value) {
75 - useThemeStore().closeSidebar()
72 + themeStore.closeSidebar()
73 }
74 }
75 })
@@ -100,6 +97,15 @@ function handleUpdateExpandedKeys(value: string[]) {
97 }
98
99 :deep() {
100 + .n-menu-item {
101 + .n-menu-item-content {
102 + gap: 8px;
103 +
104 + .n-menu-item-content__icon {
105 + margin-right: 0 !important;
106 + }
107 + }
108 + }
109 .n-menu-item-content,
110 .n-menu-item-group {
111 .item-badge {
@@ -117,7 +123,6 @@ function handleUpdateExpandedKeys(value: string[]) {
123 :nth-child(2) {
124 color: var(--fg-color);
125 background: var(--hover-005-color);
120 - margin-right: 10px;
126 height: 22px;
127 line-height: 24px;
128 border-radius: 15px;
@@ -158,18 +163,57 @@ function handleUpdateExpandedKeys(value: string[]) {
163 }
164
165 .n-submenu-children {
166 + --dash-width: 12px;
167 + --dash-height: 2px;
168 + --dash-offset: 43px;
169 +
170 + position: relative;
171 +
172 + &::before {
173 + content: "";
174 + display: block;
175 + background-color: var(--divider-010-color);
176 + width: var(--dash-height);
177 + position: absolute;
178 + top: 0px;
179 + bottom: 20px;
180 + left: var(--dash-offset);
181 + }
182 +
183 .n-menu-item-content {
184 &::after {
185 content: "";
186 display: block;
165 - opacity: 0.1;
166 - background-color: var(--fg-color);
167 - width: 12px;
168 - height: 2px;
187 + background-color: var(--divider-010-color);
188 + width: var(--dash-width);
189 + height: var(--dash-height);
190 position: absolute;
170 - top: 50%;
171 - left: 38px;
172 - margin-top: -1px;
191 + top: calc(50% - calc(ar(--dash-height) / 2));
192 + left: calc(var(--dash-offset) + var(--dash-height));
193 + }
194 + }
195 +
196 + .n-menu-item-group {
197 + .n-menu-item-group-title {
198 + padding-left: 64px !important;
199 + }
200 + }
201 +
202 + .n-submenu-children {
203 + &::before {
204 + display: none;
205 + }
206 + .n-menu-item-content {
207 + &::after {
208 + width: calc(var(--dash-width) * 3);
209 + background: repeating-linear-gradient(
210 + 90deg,
211 + var(--divider-010-color) 0px,
212 + var(--divider-010-color) 5px,
213 + transparent 5px,
214 + transparent 8px
215 + );
216 + }
217 }
218 }
219 }
@@ -183,6 +227,16 @@ function handleUpdateExpandedKeys(value: string[]) {
227 }
228 }
229 }
230 +
231 +.direction-rtl {
232 + .nav {
233 + :deep() {
234 + .n-submenu-children {
235 + --dash-offset: 39px;
236 + }
237 + }
238 + }
239 +}
240 </style>
241
242 <style lang="scss">
frontend/src/layouts/common/Navbar/items.tsx
+1 -2
@@ -18,8 +18,7 @@ const ExternalServicesIcon = "carbon:ibm-cloud-direct-link-2-dedicated"
18 const ReportCreationIcon = "carbon:report-data"
19 const SchedulerIcon = "material-symbols:autoplay"
20
21 -/*eslint @typescript-eslint/no-unused-vars: "off"*/
22 -export default function getItems(mode: "vertical" | "horizontal", collapsed: boolean): MenuMixedOption[] {
21 +export default function getItems(): MenuMixedOption[] {
22 return [
23 {
24 label: () =>
frontend/src/layouts/common/Provider.vue
+21 -7
@@ -1,6 +1,6 @@
1 <template>
2 - <n-config-provider :theme="theme" :theme-overrides="themeOverrides" preflight-style-disabled>
3 - <n-loading-bar-provider :container-style="{ height: '3px' }">
2 + <n-config-provider :rtl="rtlOptions" :theme="theme" :theme-overrides="themeOverrides" preflight-style-disabled>
3 + <n-loading-bar-provider container-class="h-0.75">
4 <n-message-provider>
5 <n-notification-provider>
6 <n-dialog-provider>
@@ -16,7 +16,6 @@
16 </template>
17
18 <script lang="ts" setup>
19 -import { computed, watch } from "vue"
19 import {
20 NGlobalStyle,
21 NConfigProvider,
@@ -28,25 +27,29 @@ import {
27 } from "naive-ui"
28 import { useThemeStore } from "@/stores/theme"
29 import { useWindowSize } from "@vueuse/core"
30 +import { computed, watch } from "vue"
31 import GlobalListener from "@/layouts/common/GlobalListener.vue"
32 +import type { RtlItem } from "naive-ui/es/config-provider/src/internal-interface"
33 +import { rtlStyles } from "./rtlProvider"
34 import type { ThemeName } from "@/types/theme.d"
35
36 const { width } = useWindowSize()
35 -
37 const themeStore = useThemeStore()
37 -
38 const theme = computed(() => themeStore.naiveTheme)
39 +const themeName = computed<ThemeName>(() => themeStore.themeName)
40 const themeOverrides = computed<GlobalThemeOverrides>(() => themeStore.themeOverrides)
41 const style = computed(() => themeStore.style)
41 -const themeName = computed<ThemeName>(() => themeStore.themeName)
42 +const isRTL = computed(() => themeStore.isRTL)
43 +const rtlOptions = computed<RtlItem[] | undefined>(() => (isRTL.value ? rtlStyles : undefined))
44
45 watch(
44 - style,
46 + [isRTL, style],
47 () => {
48 setGlobalVars()
49 },
50 { immediate: true }
51 )
52 +
53 watch(
54 themeName,
55 (val, old) => {
@@ -54,6 +57,7 @@ watch(
57 },
58 { immediate: true }
59 )
60 +
61 watch(
62 width,
63 () => {
@@ -62,8 +66,18 @@ watch(
66 { immediate: true }
67 )
68
69 +// This function allows you to utilize the values in the style object as variables within your CSS/SCSS code like: var(-–bg-color)
70 function setGlobalVars() {
71 const html = document.children[0] as HTMLElement
72 + const body = document.getElementsByTagName("body")?.[0]
73 + if (isRTL.value && body) {
74 + body.classList.add("direction-rtl")
75 + body.classList.remove("direction-ltr")
76 + } else {
77 + body.classList.remove("direction-rtl")
78 + body.classList.add("direction-ltr")
79 + }
80 + //html.dir = isRTL.value ? "rtl" : "ltr"
81 const { style: htmlStyle } = html
82 for (const key in style.value) {
83 htmlStyle.setProperty("--" + key, style.value[key])
frontend/src/layouts/common/SplashScreen.vue
+3 -3
@@ -1,6 +1,6 @@
1 <template>
2 <transition name="loading-fade">
3 - <div class="splash-screen-loading-box" v-if="loading">
3 + <div class="splash-screen-loading-box" v-if="show">
4 <n-spin :size="80" />
5 </div>
6 </transition>
@@ -11,9 +11,9 @@ import { NSpin } from "naive-ui"
11 import { toRefs } from "vue"
12
13 const props = defineProps<{
14 - loading: boolean
14 + show: boolean
15 }>()
16 -const { loading } = toRefs(props)
16 +const { show } = toRefs(props)
17 </script>
18
19 <style scoped lang="scss">
frontend/src/layouts/common/Toolbar/Avatar.vue
+2 -5
@@ -17,13 +17,10 @@ const LogoutIcon = "ion:log-out-outline"
17 const LogsIcon = "carbon:cloud-logging"
18 const ContactIcon = "ic:outline-alternate-email"
19
20 -defineOptions({
21 - name: "Avatar"
22 -})
23 -
20 const router = useRouter()
21 +const authStore = useAuthStore()
22
26 -const userPic = useAuthStore().userPic
23 +const userPic = authStore.userPic
24
25 const options = ref([
26 {
frontend/src/layouts/common/Toolbar/Breadcrumb.vue
+1 -7
@@ -26,21 +26,15 @@ import { onBeforeMount, ref } from "vue"
26 import { useRouter, useRoute } from "vue-router"
27 import Icon from "@/components/common/Icon.vue"
28
29 -const HomeIcon = "fluent:home-24-regular"
30 -
29 interface Page {
30 name: string
31 path: string
32 key: string
33 }
34
37 -defineOptions({
38 - name: "Breadcrumb"
39 -})
40 -
35 +const HomeIcon = "fluent:home-24-regular"
36 const router = useRouter()
37 const route = useRoute()
43 -
38 const items = ref<Page[]>([])
39
40 function goto(page: Partial<Page>) {
frontend/src/layouts/common/Toolbar/FullscreenSwitch.vue
-5
@@ -10,14 +10,9 @@ import Icon from "@/components/common/Icon.vue"
10 import { useFullscreenSwitch } from "@/composables/useFullscreenSwitch"
11
12 const { isFullscreen, toggle } = useFullscreenSwitch()
13 -
13 const OpenIcon = "fluent:full-screen-maximize-24-regular"
14 const CloseIcon = "fluent:full-screen-minimize-24-regular"
15
17 -defineOptions({
18 - name: "FullscreenSwitch"
19 -})
20 -
16 function toggleFullscreen(e?: MouseEvent) {
17 toggle()
18 return e
frontend/src/layouts/common/Toolbar/LocaleSwitch.vue
+3 -1
@@ -12,7 +12,9 @@ import { computed, h, type VNodeChild } from "vue"
12
13 const MultiLanguageIcon = "ion:language-outline"
14
15 -const { getAvailableLocales, getLocale, setLocale, t } = useStoreI18n()
15 +const storeI18n = useStoreI18n()
16 +
17 +const { getAvailableLocales, getLocale, setLocale, t } = storeI18n
18
19 const list = computed(() =>
20 getAvailableLocales().map(i => ({
frontend/src/layouts/common/Toolbar/Notifications.vue
+8 -6
@@ -1,5 +1,5 @@
1 <template>
2 - <n-popover :show-arrow="false" placement="bottom" content-style="padding:0" style="width: 280px">
2 + <n-popover :show-arrow="false" placement="bottom" content-class="!p-0 w-72">
3 <template #trigger>
4 <n-badge :show="hasUnread" dot :color="primaryColor">
5 <Icon :name="BellIcon" :size="21" class="trigger-icon"></Icon>
@@ -9,9 +9,9 @@
9 <n-text strong depth="1">Notifications</n-text>
10 </template>
11 <template #default>
12 - <NotificationsList :max-items="MAX_ITEMS" style="max-height: 50vh">
12 + <NotificationsList :max-items="MAX_ITEMS" class="max-h-50vh">
13 <template #last>
14 - <div class="p-4 flex justify-center" v-if="list.length > MAX_ITEMS">
14 + <div class="flex justify-center p-4" v-if="list.length > MAX_ITEMS">
15 <n-button text @click="showDrawer = true">View all</n-button>
16 </div>
17 </template>
@@ -22,8 +22,8 @@
22 </template>
23 </n-popover>
24
25 - <n-drawer v-model:show="showDrawer" :width="400" style="max-width: 90vw" :trap-focus="false">
26 - <n-drawer-content title="Notifications" closable body-content-style="padding:0">
25 + <n-drawer v-model:show="showDrawer" :width="400" class="max-w-90vw" :trap-focus="false">
26 + <n-drawer-content title="Notifications" closable body-content-class="!p-0">
27 <NotificationsList />
28 <template #footer>
29 <NotificationsToolbar />
@@ -44,7 +44,9 @@ import { useHealthchecksNotify } from "@/composables/useHealthchecksNotify"
44
45 const BellIcon = "ph:bell"
46
47 -const primaryColor = computed(() => useThemeStore().primaryColor)
47 +const themeStore = useThemeStore()
48 +
49 +const primaryColor = computed(() => themeStore.primaryColor)
50 const hasUnread = useNotifications().hasUnread
51
52 const showDrawer = ref(false)
frontend/src/layouts/common/Toolbar/Search.vue
+27 -8
@@ -1,6 +1,6 @@
1 <template>
2 <div class="flex items-center search-btn" @click="openBox">
3 - <Icon :name="SearchIcon" :size="16"></Icon>
3 + <Icon :name="SearchIcon" :size="16" class="search-btn-icon"></Icon>
4 <span>Search</span>
5 <n-text code class="search-command">
6 <span :class="{ win: commandIcon === 'CTRL' }">{{ commandIcon }}</span>
@@ -17,11 +17,6 @@ import Icon from "@/components/common/Icon.vue"
17 import { useSearchDialog } from "@/composables/useSearchDialog"
18
19 const SearchIcon = "ion:search-outline"
20 -
21 -defineOptions({
22 - name: "Search"
23 -})
24 -
20 const commandIcon = ref("⌘")
21
22 function openBox() {
@@ -42,7 +37,7 @@ onMounted(() => {
37 height: 32px;
38 cursor: pointer;
39 padding: 4px 10px;
45 - padding-right: 5px;
40 + padding-right: 6px;
41 outline: none;
42 border: none;
43
@@ -96,7 +91,8 @@ onMounted(() => {
91 }
92
93 @media (max-width: 1000px) {
99 - padding-right: 10px;
94 + padding-right: 8px !important;
95 + padding-left: 8px !important;
96
97 & > span {
98 display: none;
@@ -106,4 +102,27 @@ onMounted(() => {
102 }
103 }
104 }
105 +
106 +.direction-rtl {
107 + .search-btn {
108 + padding-right: 10px;
109 + padding-left: 6px;
110 +
111 + .search-btn-icon {
112 + transform: rotateY(180deg);
113 + }
114 +
115 + :deep() {
116 + & > code {
117 + border-top-left-radius: 10px;
118 + border-bottom-left-radius: 10px;
119 + padding-left: 10px;
120 +
121 + border-top-right-radius: var(--n-code-border-radius);
122 + border-bottom-right-radius: var(--n-code-border-radius);
123 + padding-right: 0.35em;
124 + }
125 + }
126 + }
127 +}
128 </style>
frontend/src/layouts/common/Toolbar/ThemeSwitch.vue
+4 -8
@@ -23,12 +23,8 @@ const Sunny = "ion:sunny"
23 const Moon = "ion:moon"
24 const SunnyOutline = "ion:sunny-outline"
25 const MoonOutline = "ion:moon-outline"
26 -
27 -defineOptions({
28 - name: "ThemeSwitch"
29 -})
30 -
31 -const isThemeDark = computed<boolean>(() => useThemeStore().isThemeDark)
26 +const themeStore = useThemeStore()
27 +const isThemeDark = computed<boolean>(() => themeStore.isThemeDark)
28
29 function toggleTheme(event?: MouseEvent) {
30 const isAppearanceTransition =
@@ -38,7 +34,7 @@ function toggleTheme(event?: MouseEvent) {
34 !window.matchMedia("(prefers-reduced-motion: reduce)").matches
35
36 if (!isAppearanceTransition || !event) {
41 - useThemeStore().toggleTheme()
37 + themeStore.toggleTheme()
38 return
39 }
40
@@ -48,7 +44,7 @@ function toggleTheme(event?: MouseEvent) {
44
45 // @ts-expect-error: Transition API
46 const transition = document.startViewTransition(async () => {
51 - useThemeStore().toggleTheme()
47 + themeStore.toggleTheme()
48 await nextTick()
49 })
50
frontend/src/layouts/common/Toolbar/index.vue
+86 -22
@@ -1,16 +1,16 @@
1 <template>
2 <header class="toolbar" :class="{ boxed }">
3 <div class="wrap flex items-center justify-end gap-3">
4 - <Logo mini class="cursor-pointer" @click="openNav()" />
4 + <div class="logo-box flex items-center gap-2 cursor-pointer" @click="openNav()">
5 + <Logo mini />
6 + <Icon :size="20" name="carbon:chevron-right"></Icon>
7 + </div>
8
9 <Breadcrumb class="grow" />
10 <PinnedPages />
11
12 <div class="bubble flex items-center">
13 <Search />
11 - <!--
12 - <LocaleSwitch />
13 - -->
14 <FullscreenSwitch />
15 <ThemeSwitch />
16 <Notifications />
@@ -29,62 +29,79 @@ import Search from "./Search.vue"
29 import PinnedPages from "./PinnedPages.vue"
30 import ThemeSwitch from "./ThemeSwitch.vue"
31 import Notifications from "./Notifications.vue"
32 -// import LocaleSwitch from "./LocaleSwitch.vue"
32 import FullscreenSwitch from "./FullscreenSwitch.vue"
33 +import Icon from "@/components/common/Icon.vue"
34 import { useLoadingBar } from "naive-ui"
35 import { useRouter } from "vue-router"
36 import { useMainStore } from "@/stores/main"
37 import { useThemeStore } from "@/stores/theme"
38
39 -const router = useRouter()
40 -
41 -defineOptions({
42 - name: "Toolbar"
43 -})
44 -
39 const props = defineProps<{
40 boxed: boolean
41 }>()
42 const { boxed } = toRefs(props)
43
44 +const router = useRouter()
45 +const themeStore = useThemeStore()
46 +const mainStore = useMainStore()
47 +
48 function openNav() {
51 - useThemeStore().openSidebar()
49 + themeStore.openSidebar()
50 }
51
52 onMounted(() => {
53 const loadingBar = useLoadingBar()
54 router.beforeEach(() => loadingBar?.start())
55 router.afterEach(() => loadingBar?.finish())
58 - useMainStore().setLoadingBar(loadingBar)
56 + mainStore.setLoadingBar(loadingBar)
57 })
58 </script>
59
60 <style lang="scss" scoped>
61 +@import "@/assets/scss/functions.scss";
62 +
63 .toolbar {
64 position: sticky;
65 top: 0;
66 left: 0;
67 - backdrop-filter: blur(15px);
67 height: var(--toolbar-height);
69 - // width: calc(100% - 1px);
68 width: 100%;
69 max-width: 100%;
70 padding: 0 var(--view-padding);
71 z-index: 3;
74 - //box-shadow: 0px 20px 20px 0px var(--bg-body);
72 overflow: hidden;
73 + background: var(--bg-body);
74
75 &::before {
76 content: "";
77 width: 100%;
78 height: 100%;
81 - background-color: var(--bg-body);
82 - background: linear-gradient(var(--bg-body), rgba(255, 255, 255, 0) 100%);
79 position: absolute;
80 display: block;
81 top: 0;
82 left: 0;
83 z-index: -1;
84 + backdrop-filter: blur(20px);
85 + // mask-image: scrimGradient(#000);
86 + mask-image: linear-gradient(
87 + to bottom,
88 + hsl(0, 0%, 0%) 0%,
89 + hsla(0, 0%, 0%, 0.979) 0.6%,
90 + hsla(0, 0%, 0%, 0.962) 2.2%,
91 + hsla(0, 0%, 0%, 0.946) 4.9%,
92 + hsla(0, 0%, 0%, 0.929) 8.6%,
93 + hsla(0, 0%, 0%, 0.91) 13.3%,
94 + hsla(0, 0%, 0%, 0.884) 18.8%,
95 + hsla(0, 0%, 0%, 0.851) 25.1%,
96 + hsla(0, 0%, 0%, 0.806) 32.3%,
97 + hsla(0, 0%, 0%, 0.749) 40.2%,
98 + hsla(0, 0%, 0%, 0.677) 48.7%,
99 + hsla(0, 0%, 0%, 0.587) 57.9%,
100 + hsla(0, 0%, 0%, 0.476) 67.7%,
101 + hsla(0, 0%, 0%, 0.343) 78%,
102 + hsla(0, 0%, 0%, 0.185) 88.8%,
103 + hsla(0, 0%, 0%, 0) 100%
104 + );
105 }
106
107 .wrap {
@@ -102,7 +119,7 @@ onMounted(() => {
119 gap: 14px;
120 }
121
105 - @media (max-width: 900px) {
122 + @media (max-width: 850px) {
123 .pinned-pages {
124 display: none;
125 }
@@ -126,9 +143,56 @@ onMounted(() => {
143 }
144
145 &.gradient-bg-sidebar {
129 - &::before {
130 - background-color: var(--bg-sidebar);
131 - background: linear-gradient(var(--bg-sidebar), rgba(255, 255, 255, 0) 100%);
146 + background-color: var(--bg-sidebar);
147 + background: linear-gradient(
148 + to bottom,
149 + rgba(var(--bg-sidebar-rgb), 1) 0%,
150 + rgba(var(--bg-sidebar-rgb), 0.945) 8.6%,
151 + rgba(var(--bg-sidebar-rgb), 0.888) 16.2%,
152 + rgba(var(--bg-sidebar-rgb), 0.83) 22.9%,
153 + rgba(var(--bg-sidebar-rgb), 0.769) 28.9%,
154 + rgba(var(--bg-sidebar-rgb), 0.707) 34.4%,
155 + rgba(var(--bg-sidebar-rgb), 0.644) 39.5%,
156 + rgba(var(--bg-sidebar-rgb), 0.578) 44.5%,
157 + rgba(var(--bg-sidebar-rgb), 0.511) 49.5%,
158 + rgba(var(--bg-sidebar-rgb), 0.443) 54.7%,
159 + rgba(var(--bg-sidebar-rgb), 0.373) 60.3%,
160 + rgba(var(--bg-sidebar-rgb), 0.301) 66.4%,
161 + rgba(var(--bg-sidebar-rgb), 0.228) 73.3%,
162 + rgba(var(--bg-sidebar-rgb), 0.153) 81%,
163 + rgba(var(--bg-sidebar-rgb), 0.077) 89.9%,
164 + rgba(var(--bg-sidebar-rgb), 0) 100%
165 + );
166 + }
167 + &.gradient-bg-body {
168 + background-color: var(--bg-body);
169 + background: linear-gradient(
170 + to bottom,
171 + rgba(var(--bg-body-rgb), 1) 0%,
172 + rgba(var(--bg-body-rgb), 0.738) 19%,
173 + rgba(var(--bg-body-rgb), 0.541) 34%,
174 + rgba(var(--bg-body-rgb), 0.382) 47%,
175 + rgba(var(--bg-body-rgb), 0.278) 56.5%,
176 + rgba(var(--bg-body-rgb), 0.194) 65%,
177 + rgba(var(--bg-body-rgb), 0.126) 73%,
178 + rgba(var(--bg-body-rgb), 0.075) 80.2%,
179 + rgba(var(--bg-body-rgb), 0.042) 86.1%,
180 + rgba(var(--bg-body-rgb), 0.021) 91%,
181 + rgba(var(--bg-body-rgb), 0.008) 95.2%,
182 + rgba(var(--bg-body-rgb), 0.002) 98.2%,
183 + rgba(var(--bg-body-rgb), 0) 100%
184 + );
185 + }
186 +}
187 +
188 +.direction-rtl {
189 + .toolbar {
190 + .wrap {
191 + .logo-box {
192 + .n-icon {
193 + transform: rotateY(180deg);
194 + }
195 + }
196 }
197 }
198 }
frontend/src/layouts/common/rtlProvider.ts new
+70
@@ -0,0 +1,70 @@
1 +import {
2 + unstableAlertRtl,
3 + unstableAvatarGroupRtl,
4 + // unstableBadgeRtl,
5 + unstableButtonRtl,
6 + unstableNotificationRtl,
7 + unstableMessageRtl,
8 + unstableDataTableRtl,
9 + unstableButtonGroupRtl,
10 + unstableCardRtl,
11 + unstableCheckboxRtl,
12 + unstableCollapseRtl,
13 + unstableCollapseTransitionRtl,
14 + unstableDialogRtl,
15 + unstableDrawerRtl,
16 + unstableDynamicInputRtl,
17 + unstableFlexRtl,
18 + unstableInputRtl,
19 + unstableInputNumberRtl,
20 + unstableListRtl,
21 + unstablePaginationRtl,
22 + unstableRadioRtl,
23 + unstableSelectRtl,
24 + unstableSpaceRtl,
25 + unstableStatisticRtl,
26 + unstableStepsRtl,
27 + unstableTableRtl,
28 + unstableTagRtl,
29 + unstableThingRtl,
30 + unstableTreeRtl,
31 + unstableRowRtl,
32 + unstableScrollbarRtl
33 +} from "naive-ui"
34 +import type { RtlItem } from "naive-ui/es/config-provider/src/internal-interface"
35 +
36 +const rtlStyles: RtlItem[] = [
37 + unstableAlertRtl,
38 + unstableAvatarGroupRtl,
39 + // unstableBadgeRtl,
40 + unstableButtonRtl,
41 + unstableNotificationRtl,
42 + unstableMessageRtl,
43 + unstableDataTableRtl,
44 + unstableButtonGroupRtl,
45 + unstableCardRtl,
46 + unstableCheckboxRtl,
47 + unstableCollapseRtl,
48 + unstableCollapseTransitionRtl,
49 + unstableDialogRtl,
50 + unstableDrawerRtl,
51 + unstableDynamicInputRtl,
52 + unstableFlexRtl,
53 + unstableInputRtl,
54 + unstableInputNumberRtl,
55 + unstableListRtl,
56 + unstablePaginationRtl,
57 + unstableRadioRtl,
58 + unstableSelectRtl,
59 + unstableSpaceRtl,
60 + unstableStatisticRtl,
61 + unstableStepsRtl,
62 + unstableTableRtl,
63 + unstableTagRtl,
64 + unstableThingRtl,
65 + unstableTreeRtl,
66 + unstableRowRtl,
67 + unstableScrollbarRtl
68 +]
69 +
70 +export { rtlStyles }
frontend/src/stores/theme.ts
+11 -2
@@ -24,6 +24,7 @@ export const useThemeStore = defineStore("theme", {
24 themeName: osTheme.value || ThemeEnum.Dark,
25 routerTransition: RouterTransition.FadeUp,
26 routerTransitionDuration: 0.3,
27 + rtl: false,
28 boxed: {
29 enabled: false,
30 toolbar: true,
@@ -68,6 +69,9 @@ export const useThemeStore = defineStore("theme", {
69 setLayout(layout: Layout): void {
70 this.layout = layout
71 },
72 + setRTL(rtl: boolean): void {
73 + this.rtl = rtl
74 + },
75 setBoxed(boxed: boolean): void {
76 this.boxed.enabled = boxed
77 },
@@ -313,6 +317,7 @@ export const useThemeStore = defineStore("theme", {
317 const buttonColorSecondaryPressed = naive.buttonColor2Pressed
318
319 const bgSidebar = this.sidebarBackground
320 + const bgSidebarRGB = hex2rgb(bgSidebar).join(", ")
321 const bgBody = this.bodyBackground
322 const bgBodyRGB = hex2rgb(bgBody).join(", ")
323
@@ -362,15 +367,16 @@ export const useThemeStore = defineStore("theme", {
367 const secondary4RGB = hex2rgb(secondary4).join(", ")
368
369 return {
365 - "bg-sidebar": `${bgSidebar}`,
370 "bg-body": `${bgBody}`,
371 "bg-body-rgb": `${bgBodyRGB}`,
372 + "bg-sidebar": `${bgSidebar}`,
373 + "bg-sidebar-rgb": `${bgSidebarRGB}`,
374
375 "fg-color": `${fgColor}`,
376 "fg-secondary-color": `${fgSecondaryColor}`,
377 "bg-color": `${bgColor}`,
372 - "bg-secondary-color": `${bgSecondaryColor}`,
378 "bg-color-rgb": `${bgColorRGB}`,
379 + "bg-secondary-color": `${bgSecondaryColor}`,
380
381 "border-color": `${borderColor}`,
382 "bezier-ease": `${bezierEase}`,
@@ -463,6 +469,9 @@ export const useThemeStore = defineStore("theme", {
469 isBoxed(state): boolean {
470 return state.boxed.enabled
471 },
472 + isRTL(state): boolean {
473 + return state.rtl
474 + },
475 isFooterShown(state): boolean {
476 return state.footer.show
477 },
frontend/src/types/theme.d.ts
-1
@@ -1,5 +1,4 @@
1 export enum Layout {
2 - VerticalNav = "VerticalNav",
2 HorizontalNav = "HorizontalNav",
3 Blank = "Blank"
4 }
frontend/src/utils/auth.ts
+7 -5
@@ -44,26 +44,28 @@ export function authCheck(route: RouteLocationNormalized) {
44 const meta: RouteMetaAuth = route.meta
45 const { checkAuth, authRedirect, auth, roles } = meta
46
47 + const authStore = useAuthStore()
48 +
49 if (route?.redirectedFrom?.name === "Logout") {
48 - useAuthStore().setLogout()
50 + authStore.setLogout()
51 }
52
53 if (auth === true) {
52 - if (!useAuthStore().isLogged) {
54 + if (!authStore.isLogged) {
55 window.location.href = "/login" + window.location.search
56 return false
57 }
58
57 - if (roles && !useAuthStore().isRoleGranted(roles)) {
59 + if (roles && !authStore.isRoleGranted(roles)) {
60 window.location.href = "/login" + window.location.search
61 return false
62 }
63 }
64
65 if (checkAuth === true) {
64 - if (useAuthStore().isLogged) {
66 + if (authStore.isLogged) {
67 if (roles) {
66 - if (useAuthStore().isRoleGranted(roles)) {
68 + if (authStore.isRoleGranted(roles)) {
69 return authRedirect || "/"
70 } else {
71 return route.path
frontend/src/views/Profile.vue
+12 -14
@@ -1,6 +1,6 @@
1 <template>
2 <div class="page">
3 - <n-card class="header flex flex-col" content-style="padding:0">
3 + <n-card class="header flex flex-col" content-class="!p-0">
4 <div class="user-info flex flex-wrap">
5 <div class="propic">
6 <n-avatar :size="100" :src="userPic" round />
@@ -22,7 +22,7 @@
22 <div class="item">
23 <n-tooltip placement="top">
24 <template #trigger>
25 - <div class="item-wrap">
25 + <div class="tooltip-wrap">
26 <Icon :name="RoleIcon"></Icon>
27 <span>{{ userRole }}</span>
28 </div>
@@ -31,7 +31,7 @@
31 </n-tooltip>
32 </div>
33 <div class="item" v-if="userEmail">
34 - <div class="item-wrap">
34 + <div class="tooltip-wrap">
35 <Icon :name="EmailIcon"></Icon>
36 <span>{{ userEmail }}</span>
37 </div>
@@ -59,7 +59,7 @@
59 </div>
60 </n-card>
61 <div class="main">
62 - <n-tabs :tab-style="{ display: 'none' }" v-model:value="tabActive" animated>
62 + <n-tabs tab-class="!hidden" v-model:value="tabActive" animated>
63 <n-tab-pane name="settings">
64 <ProfileSettings />
65 </n-tab-pane>
@@ -84,11 +84,12 @@ const EditIcon = "uil:image-edit"
84 const EmailIcon = "carbon:email"
85
86 const tabActive = ref("settings")
87 +const authStore = useAuthStore()
88
88 -const userRole = useAuthStore().userRoleName
89 -const userName = useAuthStore().userName
90 -const userEmail = useAuthStore().userEmail
91 -const userPic = ref(useAuthStore().userPic)
89 +const userRole = authStore.userRoleName
90 +const userName = authStore.userName
91 +const userEmail = authStore.userEmail
92 +const userPic = ref(authStore.userPic)
93
94 function setCroppedImage(result: ImageCropperResult) {
95 const canvas = result.canvas as HTMLCanvasElement
@@ -141,14 +142,11 @@ function setCroppedImage(result: ImageCropperResult) {
142 gap: 24px;
143
144 .item {
144 - .item-wrap {
145 + .tooltip-wrap {
146 display: flex;
147 align-items: center;
147 -
148 - span {
149 - line-height: 1;
150 - margin-left: 8px;
151 - }
148 + gap: 8px;
149 + line-height: 1;
150 }
151 }
152 }
frontend/src/views/auth/Login.vue
+7 -79
@@ -1,5 +1,5 @@
1 <template>
2 - <div class="page">
2 + <div class="page-auth">
3 <div class="flex wrapper justify-center" v-if="!isLogged">
4 <div class="image-box basis-2/3" v-if="align === 'right'"></div>
5 <div class="form-box basis-1/3 flex items-center justify-center" :class="{ centered: align === 'center' }">
@@ -32,46 +32,28 @@ const { formType } = toRefs(props)
32
33 const route = useRoute()
34 const align = ref<Align>("left")
35 -const activeColor = ref("")
35 const type = ref<FormType | undefined>(formType.value || undefined)
36
38 -const primaryColor = computed(() => useThemeStore().primaryColor)
39 -const isLogged = computed(() => useAuthStore().isLogged)
37 +const themeStore = useThemeStore()
38 +const activeColor = computed(() => themeStore.primaryColor)
39 +const authStore = useAuthStore()
40 +const isLogged = computed(() => authStore.isLogged)
41
42 onBeforeMount(() => {
43 if (route.query.step) {
44 const step = route.query.step as FormType
45 type.value = step
46 }
46 - activeColor.value = primaryColor.value
47 })
48 </script>
49
50 <style lang="scss" scoped>
51 -@import "@/assets/scss/common.scss";
52 -
53 -.page {
54 - min-height: 100vh;
55 -
56 - .settings {
57 - position: fixed;
58 - top: 10px;
59 - left: 50%;
60 - transform: translateX(-50%);
61 - background-color: var(--bg-secondary-color);
62 - height: 44px;
63 - width: 300px;
64 - border-radius: 50px;
65 - padding: 5px;
66 - z-index: 1;
67 - }
51 +@import "./main.scss";
52
53 +.page-auth {
54 .wrapper {
70 - min-height: 100vh;
71 -
55 .image-box {
56 background-color: v-bind(activeColor);
74 - position: relative;
57
58 video {
59 position: absolute;
@@ -82,60 +64,6 @@ onBeforeMount(() => {
64 object-fit: cover;
65 object-position: center;
66 }
85 -
86 - /*
87 - &::after {
88 - content: "";
89 - width: 100%;
90 - height: 100%;
91 - position: absolute;
92 - top: 0;
93 - left: 0;
94 - background-image: url(@/assets/images/pattern-onboard.png);
95 - background-size: 500px;
96 - background-position: center center;
97 - }
98 - */
99 - }
100 -
101 - .form-box {
102 - padding: 50px;
103 -
104 - &.centered {
105 - flex-basis: 100%;
106 - .form-wrap {
107 - padding: 60px;
108 - width: 100%;
109 - max-width: 500px;
110 - background-color: var(--bg-color);
111 - border-radius: 20px;
112 - @apply shadow-xl;
113 - }
114 -
115 - @media (max-width: 600px) {
116 - padding: 4%;
117 - .form-wrap {
118 - padding: 8%;
119 - }
120 - }
121 - }
122 - }
123 - }
124 - @media (max-width: 800px) {
125 - .settings {
126 - width: 112px;
127 - .colors {
128 - display: none;
129 - }
130 - }
131 - .wrapper {
132 - .image-box {
133 - display: none;
134 - }
135 -
136 - .form-box {
137 - flex-basis: 100%;
138 - }
67 }
68 }
69 }
frontend/src/views/auth/main.scss new
+59
@@ -0,0 +1,59 @@
1 +.page-auth {
2 + min-height: 100vh;
3 +
4 + .wrapper {
5 + min-height: 100vh;
6 +
7 + .image-box {
8 + position: relative;
9 +
10 + /*
11 + &::after {
12 + content: "";
13 + width: 100%;
14 + height: 100%;
15 + position: absolute;
16 + top: 0;
17 + left: 0;
18 + background-image: url(@/assets/images/pattern-onboard.png);
19 + background-size: 500px;
20 + background-position: center center;
21 + }
22 + */
23 + }
24 +
25 + .form-box {
26 + padding: 50px;
27 +
28 + &.centered {
29 + flex-basis: 100%;
30 + .form-wrap {
31 + padding: 60px;
32 + width: 100%;
33 + max-width: 500px;
34 + background-color: var(--bg-color);
35 + border-radius: 20px;
36 + @apply shadow-xl;
37 + }
38 +
39 + @media (max-width: 600px) {
40 + padding: 4%;
41 + .form-wrap {
42 + padding: 8%;
43 + }
44 + }
45 + }
46 + }
47 + }
48 + @media (max-width: 800px) {
49 + .wrapper {
50 + .image-box {
51 + display: none;
52 + }
53 +
54 + .form-box {
55 + flex-basis: 100%;
56 + }
57 + }
58 + }
59 +}
frontend/tailwind.config.js
+34
@@ -20,9 +20,43 @@ export default {
20 extend: {
21 screens: {
22 xs: "460px"
23 + },
24 + height: {
25 + 0.75: "0.1875rem",
26 + 150: "37.5rem",
27 + "60vh": "60vh"
28 + },
29 + width: {
30 + 5.5: "1.375rem",
31 + 150: "37.5rem"
32 + },
33 + spacing: {
34 + "20vh": "20vh",
35 + "50vw": "50vw"
36 + },
37 + maxHeight: {
38 + "50vh": "50vh"
39 + },
40 + maxWidth: {
41 + "90vw": "90vw"
42 }
43 }
44 },
45 + safelist: [
46 + "p-4",
47 + "!p-1",
48 + "flex",
49 + "flex-col",
50 + "!justify-start",
51 + "opacity-50",
52 + "hover:text-red-500",
53 + "hover:opacity-100",
54 + "items-center",
55 + "gap-5",
56 + "w-72",
57 + "max-h-50vh",
58 + "max-w-90vw"
59 + ],
60 plugins: [
61 plugin(function ({ addBase, theme }) {
62 addBase({