Initial Version
20
core/src/App.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<RouterView></RouterView>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router';
|
||||
import { onMounted } from 'vue';
|
||||
import { useTheme } from 'vuetify';
|
||||
import { useLayoutStore } from '@KTXC/stores/layoutStore';
|
||||
|
||||
const theme = useTheme();
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
// Apply saved theme on mount
|
||||
onMounted(() => {
|
||||
if (layoutStore.theme) {
|
||||
theme.global.name.value = layoutStore.theme;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
16
core/src/assets/images/favicon.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<svg width="36" height="35" viewBox="0 0 36 35" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.64931 15.8644L6.96164 13.552L6.96405 13.5496H11.3143L9.58336 15.2806L9.13646 15.7275L7.36391 17.5L7.58344 17.7201L17.5137 27.6498L27.6634 17.5L25.8903 15.7275L25.7654 15.602L23.7131 13.5496H28.0633L28.0657 13.552L29.8781 15.3644L32.0137 17.5L17.5137 32L3.01367 17.5L4.64931 15.8644ZM17.5137 3L25.8921 11.3784H21.5419L17.5137 7.35024L13.4855 11.3784H9.13525L17.5137 3Z" fill="#096DD9"/>
|
||||
<path d="M7.36453 17.4999L9.13708 15.7274L9.58398 15.2805L7.85366 13.5496H6.96467L6.96226 13.552L4.64993 15.8643L6.86938 18.0729L7.36453 17.4999Z" fill="url(#paint0_linear_112117_33940)"/>
|
||||
<path d="M25.8911 15.7274L27.6643 17.4999L27.4888 17.6754L27.4894 17.676L29.8789 15.3643L28.0666 13.552L28.0641 13.5496H27.888L25.7663 15.6019L25.8911 15.7274Z" fill="url(#paint1_linear_112117_33940)"/>
|
||||
<path d="M6.95946 13.5496L6.96187 13.552L9.13669 15.7274L17.5139 24.104L28.0684 13.5496H6.95946Z" fill="#1890FF"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_112117_33940" x1="8.63954" y1="14.0887" x2="5.58137" y2="17.1469" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#023B95"/>
|
||||
<stop offset="0.9637" stop-color="#096CD9" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_112117_33940" x1="26.282" y1="14.1278" x2="28.7548" y2="16.9379" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#023B95"/>
|
||||
<stop offset="1" stop-color="#096DD9" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
BIN
core/src/assets/images/maintenance/Error404.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
core/src/assets/images/maintenance/Error500.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
core/src/assets/images/maintenance/TwoCone.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
core/src/assets/images/maintenance/coming-soon.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
353
core/src/assets/images/maintenance/under-construction.svg
Normal file
@@ -0,0 +1,353 @@
|
||||
<svg shape-rendering="geometricPrecision" text-rendering="geometricPrecision" viewBox="0 0 532 475" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m266 474.5c146.63 0 265.5-60.442 265.5-135s-118.87-135-265.5-135-265.5 60.442-265.5 135 118.87 135 265.5 135z" fill="#fff"/>
|
||||
<path d="M246.525,353.382l-54.356-31.019c-.888-.51-2.32-.499-3.208.011l-.566.322c-.888.51-.877,1.331.011,1.83l18.526,10.351-18.948,10.939-18.303-10.562c-.888-.51-2.32-.51-3.197,0l-.566.322c-.888.511-.888,1.343,0,1.853l55.277,31.896.599-.71.3.177l21.822-12.569.167.099.322-.377.466-.266-.167-.089l1.821-2.208Z" fill="#f5f5f5"/>
|
||||
<path d="M51.2325,387.902L25.5411,373.079c-1.3881-.797-1.3881-2.092,0-2.89L297.759,213.098c1.388-.797,3.62-.797,5.009,0l25.691,14.823c1.388.797,1.388,2.092,0,2.89L56.2412,387.902c-1.3881.797-3.6206.797-5.0087,0Z" fill="#f5f5f5"/>
|
||||
<path d="M154.674,361.659c6.593,3.706,6.659,9.841.144,13.691-6.505,3.849-17.127,3.96-23.72.255-6.594-3.706-6.66-9.841-.145-13.69c6.505-3.861,17.127-3.972,23.721-.256Z" fill="#f5f5f5"/>
|
||||
<path d="M230.504,419.116L252.88,405.42c.827-.512.827-1.329,0-1.841l-22.376-13.696c-.826-.511-2.172-.511-3.008,0L205.12,403.579c-.827.512-.827,1.329,0,1.841l22.376,13.696c.836.512,2.182.512,3.008,0Z" fill="#f5f5f5"/>
|
||||
<g transform="translate(1e-6 1e-6)">
|
||||
<path d="M208.508,402.685l19.495-11.245c.365-.21.838-.319,1.303-.319.474,0,.947.1,1.312.319l19.486,11.245c.364.209.537.482.537.755v1.421c0,.273-.173.546-.537.756l-19.486,11.244c-.365.21-.838.319-1.312.319s-.948-.1-1.303-.319l-19.495-11.244c-.364-.21-.537-.483-.537-.756v-1.421c0-.264.173-.537.537-.755Z" fill="#fa8c16"/>
|
||||
<path d="M207.971,403.44v1.421c0,.273.173.546.537.756l19.495,11.244c.365.21.838.319,1.303.319v-26.059c-.474,0-.948.1-1.303.319l-19.495,11.245c-.364.218-.537.491-.537.755Z" opacity=".1"/>
|
||||
<path d="M229.307,391.121v26.05c.473,0,.947-.1,1.311-.319l19.486-11.245c.365-.209.538-.482.538-.755v-1.421c0-.273-.173-.546-.538-.755l-19.486-11.245c-.364-.201-.838-.31-1.311-.31Z" opacity=".3"/>
|
||||
<path d="M230.618,415.45l19.495-11.245c.72-.419.72-1.092,0-1.511l-19.495-11.245c-.72-.419-1.895-.419-2.614,0l-19.495,11.245c-.72.419-.72,1.092,0,1.511l19.495,11.245c.719.419,1.894.419,2.614,0Z" fill="#fa8c16"/>
|
||||
<path d="M230.618,415.45l19.495-11.245c.72-.419.72-1.092,0-1.511l-19.495-11.245c-.72-.419-1.895-.419-2.614,0l-19.495,11.245c-.72.419-.72,1.092,0,1.511l19.495,11.245c.719.419,1.894.419,2.614,0Z" fill="#fff" opacity=".5"/>
|
||||
<path d="M229.307,391.13c.473,0,.947.101,1.311.31l19.495,11.245c.72.419.72,1.092,0,1.511l-19.495,11.245c-.364.21-.838.31-1.311.31v-24.621Z" opacity=".05"/>
|
||||
<path d="M217.273,402.029l8.618-25.895c0-.009,0-.009.009-.018l.009-.028c.136-.364.446-.719.938-1.001c1.357-.783,3.553-.783,4.91,0c.492.282.802.637.939,1.001h.009l.009.028c0,0,0,0,0,.009l8.627,25.895c.792,2.249-.319,4.68-3.343,6.428-4.801,2.768-12.572,2.768-17.373,0-3.033-1.739-4.145-4.17-3.352-6.419Z" fill="#fa8c16"/>
|
||||
<path d="M223.421,383.564l.009-.028l1.23-3.678c-.201.628-.018,1.284.537,1.848.21.21.456.41.766.583c1.849,1.065,4.846,1.065,6.695,0c.31-.182.565-.373.766-.583.564-.574.737-1.229.537-1.857l1.23,3.687.009.018c.2.62.109,1.257-.273,1.849-.292.455-.747.883-1.376,1.247-.446.255-.956.446-1.485.61-1.722.519-3.771.519-5.493,0-.528-.164-1.039-.355-1.485-.61-.629-.364-1.084-.792-1.376-1.247-.4-.592-.491-1.229-.291-1.839Z" fill="#fafafa"/>
|
||||
<path d="M218.483,398.396l.009-.028l1.23-3.687c-.2.61-.219,1.238-.073,1.848.155.646.501,1.275,1.039,1.857.446.483,1.011.929,1.712,1.339c3.817,2.203,9.994,2.203,13.811,0c.701-.401,1.266-.856,1.712-1.339.547-.582.884-1.22,1.039-1.857.146-.61.137-1.238-.064-1.848l1.221,3.678.009.037c.2.61.246,1.238.137,1.857-.255,1.402-1.303,2.768-3.171,3.842-4.309,2.486-11.287,2.486-15.596,0-1.858-1.074-2.915-2.44-3.17-3.842-.091-.619-.055-1.247.155-1.857Z" fill="#fafafa"/>
|
||||
<path d="M220.953,390.975l.009-.027l1.23-3.688c-.201.61-.155,1.248.118,1.849.31.682.911,1.32,1.822,1.857.018.009.037.027.055.036c2.833,1.63,7.424,1.63,10.248,0c.019-.009.037-.027.055-.036.911-.537,1.512-1.175,1.822-1.857.273-.61.319-1.239.118-1.849l1.23,3.679.009.027c.201.61.201,1.247,0,1.858-.218.646-.674,1.283-1.348,1.848-.291.246-.61.473-.984.692-.947.546-2.077.938-3.27,1.174-1.777.346-3.717.346-5.493,0-1.194-.236-2.314-.619-3.271-1.174-.373-.219-.701-.446-.984-.692-.674-.565-1.129-1.193-1.348-1.848-.219-.601-.219-1.23-.018-1.849Z" fill="#fafafa"/>
|
||||
<path d="M229.307,374.504c.892,0,1.776.191,2.459.583.492.282.802.637.939,1.001h.009l.009.028c0,0,0,0,0,.009l8.627,25.895c.792,2.249-.319,4.68-3.343,6.428-2.396,1.384-5.539,2.076-8.682,2.076.027,0,.045-.009.073-.009c3.89-.619,6.513-4.334,5.967-8.24l-3.59-25.777-2.468-1.994Z" opacity=".1"/>
|
||||
<path d="M231.766,377.928c1.357-.783,1.357-2.049,0-2.832s-3.553-.783-4.91,0-1.357,2.049,0,2.832c1.348.783,3.553.783,4.91,0Z" fill="#fa8c16"/>
|
||||
<path d="M231.766,377.928c1.357-.783,1.357-2.049,0-2.832s-3.553-.783-4.91,0-1.357,2.049,0,2.832c1.348.783,3.553.783,4.91,0Z" fill="#fff" opacity=".5"/>
|
||||
</g>
|
||||
<path d="M418.987,376.681l20.202-11.66c.755-.433.755-1.132,0-1.564l-20.202-11.66c-.755-.433-1.965-.433-2.719,0l-20.202,11.66c-.755.432-.755,1.131,0,1.564l20.202,11.66c.754.433,1.975.433,2.719,0Z" fill="#f5f5f5"/>
|
||||
<g transform="translate(1e-6)">
|
||||
<path d="M398.486,361.438l17.937-10.351c.333-.2.766-.289,1.199-.289s.877.1,1.21.289l17.937,10.351c.333.199.499.443.499.699v1.309c0,.255-.166.499-.499.699l-17.937,10.35c-.333.2-.777.289-1.21.289s-.866-.1-1.199-.289l-17.937-10.35c-.333-.2-.5-.444-.5-.699v-1.309c.011-.256.167-.5.5-.699Z" fill="#fa8c16"/>
|
||||
<path d="M397.998,362.137v1.309c0,.255.167.499.5.699l17.937,10.35c.333.2.766.289,1.199.289v-23.986c-.433,0-.866.1-1.199.289l-17.937,10.351c-.345.199-.5.443-.5.699Z" opacity=".1"/>
|
||||
<path d="M417.633,350.798v23.986c.433,0,.877-.1,1.21-.289l17.937-10.35c.333-.2.499-.444.499-.699v-1.309c0-.256-.166-.5-.499-.699l-17.937-10.351c-.333-.2-.777-.289-1.21-.289Z" opacity=".3"/>
|
||||
<path d="M418.843,373.186l17.938-10.35c.666-.389.666-1.01,0-1.387l-17.938-10.351c-.666-.388-1.742-.388-2.408,0l-17.937,10.351c-.666.388-.666,1.009,0,1.387l17.937,10.35c.655.378,1.742.378,2.408,0Z" fill="#fa8c16"/>
|
||||
<path d="M418.843,373.186l17.938-10.35c.666-.389.666-1.01,0-1.387l-17.938-10.351c-.666-.388-1.742-.388-2.408,0l-17.937,10.351c-.666.388-.666,1.009,0,1.387l17.937,10.35c.655.378,1.742.378,2.408,0Z" fill="#fff" opacity=".5"/>
|
||||
<path d="M417.633,350.798c.433,0,.877.1,1.21.289l17.937,10.351c.666.388.666,1.009,0,1.386l-17.937,10.351c-.333.189-.766.289-1.21.289v-22.666Z" opacity=".05"/>
|
||||
<path d="M406.556,360.828l7.936-23.831v-.011l.011-.022c.123-.333.411-.666.866-.921c1.254-.721,3.275-.721,4.518,0c.455.267.744.588.866.921l.011.022c0,0,0,0,0,.011l7.936,23.842c.733,2.074-.289,4.315-3.075,5.913-4.417,2.552-11.577,2.552-15.994,0-2.775-1.609-3.797-3.85-3.075-5.924Z" fill="#fa8c16"/>
|
||||
<path d="M412.217,343.831l.011-.022l1.132-3.384c-.189.577-.022,1.187.489,1.709.188.188.421.377.71.543c1.698.977,4.462.977,6.16,0c.289-.166.522-.344.711-.543.51-.522.677-1.132.488-1.709l1.132,3.395.011.022c.189.566.1,1.154-.244,1.709-.266.421-.688.81-1.265,1.142-.411.233-.877.411-1.366.566-1.587.477-3.474.477-5.061,0-.488-.144-.955-.321-1.365-.566-.577-.332-.999-.732-1.266-1.142-.377-.566-.466-1.154-.277-1.72Z" fill="#fafafa"/>
|
||||
<path d="M407.666,357.488l.011-.022l1.132-3.395c-.189.566-.2,1.143-.067,1.709.145.588.456,1.176.955,1.708.411.444.932.855,1.576,1.232c3.508,2.03,9.202,2.03,12.709,0c.644-.377,1.166-.788,1.577-1.232.499-.543.81-1.12.954-1.708.133-.566.122-1.143-.066-1.698l1.132,3.384.011.033c.189.566.222,1.143.122,1.709-.233,1.287-1.199,2.552-2.919,3.539-3.963,2.285-10.39,2.285-14.352,0-1.71-.987-2.686-2.241-2.92-3.539-.077-.577-.044-1.154.145-1.72Z" fill="#fafafa"/>
|
||||
<path d="M409.942,350.654l.011-.022l1.132-3.395c-.189.566-.144,1.143.111,1.709.288.621.843,1.22,1.676,1.708.022.011.033.022.055.034c2.609,1.508,6.827,1.508,9.435,0c.022-.012.034-.023.056-.034.832-.499,1.398-1.087,1.676-1.708.255-.555.288-1.143.111-1.709l1.132,3.395.011.022c.189.566.189,1.143,0,1.709-.2.599-.621,1.176-1.243,1.697-.266.222-.566.433-.91.633-.877.51-1.909.865-3.008,1.076-1.643.321-3.419.321-5.062,0-1.099-.211-2.131-.577-3.008-1.076-.344-.2-.644-.411-.91-.633-.622-.521-1.043-1.098-1.243-1.697-.211-.555-.211-1.143-.022-1.709Z" fill="#fafafa"/>
|
||||
<path d="M417.633,335.5c.821,0,1.631.177,2.264.543.455.266.744.588.866.921l.011.022c0,0,0,0,0,.011l7.936,23.842c.733,2.074-.288,4.315-3.074,5.913-2.209,1.276-5.106,1.908-7.992,1.908.022,0,.044-.011.067-.011c3.585-.577,5.993-3.994,5.494-7.577l-3.297-23.731-2.275-1.841Z" opacity=".1"/>
|
||||
<path d="M419.898,338.65c1.254-.721,1.254-1.886,0-2.607s-3.275-.721-4.518,0c-1.254.721-1.254,1.886,0,2.607c1.254.722,3.264.722,4.518,0Z" fill="#fa8c16"/>
|
||||
<path d="M419.898,338.65c1.254-.721,1.254-1.886,0-2.607s-3.275-.721-4.518,0c-1.254.721-1.254,1.886,0,2.607c1.254.722,3.264.722,4.518,0Z" fill="#fff" opacity=".5"/>
|
||||
</g>
|
||||
<g transform="translate(4e-6 2e-6)">
|
||||
<path d="m50.491 357.3s2.7195-13.291-0.4107-25.317c-3.1301-12.026-9.8788-21.09-17.837-24.396-7.9696-3.306-17.027 2.063-9.9565 10.073s20.89 18.35 21.744 39.995l6.4601-0.355z" fill="#52c41a"/>
|
||||
<path d="m50.491 357.3s2.7195-13.291-0.4107-25.317c-3.1301-12.026-9.8788-21.09-17.837-24.396-7.9696-3.306-17.027 2.063-9.9565 10.073s20.89 18.35 21.744 39.995l6.4601-0.355z" opacity=".15"/>
|
||||
<path d="M49.1816,353.194c.0333,0,.0666.011.0999,0c.2553-.023.444-.233.4218-.488-1.6983-24.053-15.24-38.275-22.1552-42.436-.2109-.133-.4995-.055-.6327.156-.1332.222-.0555.499.1554.632c6.782,4.072,20.0351,18.039,21.7223,41.714.0111.222.1776.388.3885.422Z" fill="#fff"/>
|
||||
</g>
|
||||
<g transform="translate(7e-6 1e-6)">
|
||||
<path d="M10.8098,336.242c-.87689,2.685,1.0323,4.548,2.8304,6.057c1.3875,1.154,2.8971,2.419,3.2856,4.183.7659,3.572-3.4632,6.867-2.553,10.406.3885,1.487,1.6317,2.607,2.9859,3.328c1.1211.61,2.3309.999,3.5075,1.498.5883.177,1.1766.322,1.7649.422c1.7315.299,3.563.809,4.8062,2.141c1.9535,2.119,1.7094,5.38,2.4086,8.176.0111.033.0222.067.0222.1c1.4985,5.813,8.4914,8.199,13.3864,4.715l9.8788-7.034c3.9071-5.258,2.9193-14.688-3.3521-17.773-1.332-.654-2.8527-1.031-3.9183-2.074-1.2875-1.254-1.5983-3.195-1.676-4.993-.0777-1.797,0-3.661-.7326-5.314-1.0212-2.285-3.4632-3.683-5.9273-4.105-2.8859-.499-5.783.677-8.5135-.321-2.7417-.999-5.2835-2.607-8.114-3.317-3.1079-.777-6.6932-.355-9.013,2.152-.4773.51-.8547,1.098-1.0767,1.753Z" fill="#95de64"/>
|
||||
<path d="M39.7466,352.029c-.0222-.067-.0666-.122-.1221-.167-7.348-10.395-18.8585-15.332-22.7546-16.197-.222-.045-.4328.089-.4883.31-.0444.222.0888.433.3107.489c3.6852.821,14.3743,5.38,21.578,14.921-10.2118-3.273-17.8373,1.509-17.915,1.564-.1887.123-.2442.378-.1221.566.0777.111.1998.178.333.189.0777,0,.1665-.022.2331-.067.0777-.055,7.9252-4.981,18.3035-1.109c3.774,5.425,6.3824,12.359,6.116,20.99-.0111.211.1554.399.3663.422.0111,0,.0222,0,.0333,0c.222.011.4107-.167.4218-.4.2664-8.864-2.4198-15.953-6.2936-21.511Z" fill="#fff"/>
|
||||
</g>
|
||||
<path d="m321.48 20.847-5.694-3.306-266.63 153.84v200.76c0 1.931 0.677 3.284 1.7648 3.917h0.0111l5.6831 3.306c1.0989 0.632 2.6196 0.543 4.2956-0.422l254.5-146.85c3.352-1.931 6.071-6.635 6.071-10.506v-200.74z" fill="#d9d9d9"/>
|
||||
<path d="m54.854 375.45c0 3.872 2.7195 5.437 6.0716 3.506l254.5-146.85c3.352-1.931 6.072-6.635 6.072-10.506v-200.76l-266.64 153.85v200.76z" fill="#096dd9"/>
|
||||
<path d="m54.854 375.45c0 3.872 2.7195 5.437 6.0716 3.506l254.5-146.85c3.352-1.931 6.072-6.635 6.072-10.506v-200.76l-266.64 153.85v200.76z" fill="#fff" stroke="#f5f5f5" stroke-miterlimit="10"/>
|
||||
<path d="m54.854 174.69v23.753l266.63-153.84v-23.753l-266.63 153.84z" fill="#1890ff"/>
|
||||
<path d="m54.854 198.44-5.6942-3.306v-23.753l5.6942 3.306v23.753z" fill="#096dd9"/>
|
||||
<path d="m54.854 198.44-5.6942-3.306v-23.753l5.6942 3.306v23.753z" opacity=".2"/>
|
||||
<path d="m265.48 62.55v5.658c0 1.1538 0.822 1.6308 1.821 1.0539l37.106-21.467v-9.8516l-37.106 21.467c-0.999 0.5658-1.821 1.9859-1.821 3.1397z" fill="#fafafa"/>
|
||||
<path d="M313.968,32.4066c1.01-.5769,1.821-.1109,1.821,1.0539v5.6581c0,1.1649-.811,2.5738-1.821,3.1507L304.4,47.7942v-9.8516l9.568-5.536Z" fill="#455a64"/>
|
||||
<path d="M313.268,40.1614l-1.066-.3328c.111-.4549.167-.9208.167-1.3757c0-1.0872-.355-1.9193-.977-2.2743-.455-.2662-1.01-.2441-1.554.0777-1.199.6878-2.131,2.6404-2.131,4.4376c0,1.0873.355,1.9193.977,2.2743.211.1221.433.1776.677.1776.288,0,.588-.0888.877-.2552.721-.4105,1.343-1.2869,1.72-2.3076l1.066.3328c.044.0111.078.0222.122.0222.166,0,.322-.1109.377-.2773.067-.2108-.044-.4327-.255-.4993Zm-3.43,2.0413c-.3.1665-.555.1997-.766.0777-.355-.2108-.577-.8099-.577-1.5975c0-1.4867.799-3.2063,1.732-3.7499.177-.0998.333-.1553.477-.1553.1,0,.2.0222.289.0777.355.2108.577.8098.577,1.5975.011,1.4977-.788,3.2062-1.732,3.7498Z" fill="#fafafa"/>
|
||||
<path d="m314.08 0.94365c-1.099-0.67674-2.642-0.59908-4.351 0.38829l-254.5 146.84c-3.3521 1.931-6.0715 6.635-6.0715 10.506v12.703l5.6942 3.306 266.63-153.84v-12.714c0-1.9193-0.666-3.2728-1.753-3.9051-1.021-0.59908-4.673-2.6848-5.65-3.2839z" fill="#d9d9d9"/>
|
||||
<path d="m54.854 161.99c0-3.871 2.7195-8.575 6.0716-10.506l254.48-146.84c3.352-1.9304 6.072-0.3661 6.072 3.5058v12.703l-266.63 153.84v-12.703z" fill="#f0f0f0"/>
|
||||
<path d="M319.784,4.26071c-1.11-.68784-2.653-.62127-4.374.3772L60.9254,151.48c-1.6761.965-3.1968,2.63-4.2846,4.527l-5.7053-3.295c1.11-1.909,2.6196-3.573,4.3068-4.538L309.727,1.32076c1.71-.97629,3.252-1.053949,4.34-.377205.999.599085,4.64,2.684785,5.661,3.272775.011.02219.045.03329.056.04438Z" fill="#d9d9d9"/>
|
||||
<path d="M292.342,25.3504c-1.577.9097-2.842,3.1175-2.842,4.9258c0,1.8195,1.276,2.5517,2.842,1.642c1.576-.9097,2.841-3.1175,2.841-4.9258.011-1.8084-1.265-2.5406-2.841-1.642Z" fill="#52c41a"/>
|
||||
<path d="M302.308,19.6151c-1.576.9097-2.841,3.1174-2.841,4.9258s1.276,2.5516,2.841,1.6419c1.577-.9097,2.842-3.1174,2.842-4.9258s-1.277-2.5406-2.842-1.6419Z" fill="#fa8c16"/>
|
||||
<path d="m312.26 13.89c-1.576 0.9097-2.841 3.1174-2.841 4.9258 0 1.8194 1.276 2.5517 2.841 1.6419 1.577-0.9097 2.842-3.1174 2.842-4.9258 0-1.8194-1.265-2.5516-2.842-1.6419z" fill="#f5222d"/>
|
||||
<path d="M74.1789,298.001c-.1887,0-.3663-.044-.5328-.144-.333-.189-.5328-.544-.5328-.921v-65.089c0-.378.1998-.733.5328-.921l78.0869-45.054c.333-.188.733-.188,1.066,0c.333.189.532.544.532.921v65.079c0,.377-.199.732-.532.92L74.7116,297.846c-.1553.111-.344.155-.5327.155Zm1.0766-65.533v62.615L151.2,251.261v-62.615L75.2555,232.468Z" fill="#40a9ff"/>
|
||||
<path d="M75.101,283.024l-1.8315-1.087L96.901,242.098c.2997-.499.9324-.666,1.443-.389l15.24,8.632l16.072-32.162c.145-.3.422-.511.755-.566.322-.067.666.022.91.244l21.656,19.115-1.41,1.598-20.623-18.195-15.961,31.929c-.134.267-.367.455-.644.544-.278.089-.577.055-.833-.089l-15.3063-8.675-23.0987,38.94Z" fill="#40a9ff"/>
|
||||
<path d="M109.455,234.343c-.589,0-1.144-.144-1.654-.433-1.155-.665-1.787-1.964-1.787-3.672c0-3.062,2.075-6.657,4.728-8.188c1.476-.843,2.919-.954,4.074-.288c1.154.666,1.787,1.964,1.787,3.672c0,3.062-2.076,6.657-4.729,8.188-.821.477-1.654.721-2.419.721Zm3.718-10.75c-.366,0-.81.144-1.288.421-1.953,1.121-3.596,3.972-3.596,6.224c0,.854.233,1.476.655,1.72.422.233,1.077.133,1.809-.289c1.954-1.12,3.597-3.972,3.597-6.224c0-.854-.233-1.475-.655-1.719-.145-.089-.322-.133-.522-.133Z" fill="#40a9ff"/>
|
||||
<path d="M161.71,183.942L227.454,146c1.499-.865,2.709-.166,2.709,1.564v5.481c0,1.731-1.21,3.827-2.709,4.693L161.71,195.68c-1.498.865-2.708.166-2.708-1.565v-5.48c0-1.731,1.21-3.828,2.708-4.693Z" fill="#096dd9"/>
|
||||
<path d="M160.09,203.357l48.661-28.08c.6-.343,1.088-.066,1.088.622v3.361c0,.688-.488,1.531-1.088,1.875l-48.661,28.08c-.6.343-1.088.066-1.088-.622v-3.361c0-.688.488-1.52,1.088-1.875Z" fill="#f5f5f5"/>
|
||||
<path d="M159.002,218.19c-.222,0-.422-.111-.544-.311-.177-.299-.066-.688.233-.854l96.435-55.648c.3-.167.688-.067.855.233.178.299.067.687-.233.854l-96.435,55.648c-.1.056-.2.078-.311.078Z" fill="#f5f5f5"/>
|
||||
<path d="M159.002,224.058c-.222,0-.422-.111-.544-.31-.177-.3-.066-.688.233-.854l96.435-55.649c.3-.166.688-.066.855.233.178.3.067.688-.233.854l-96.435,55.649c-.1.055-.2.077-.311.077Z" fill="#f5f5f5"/>
|
||||
<path d="M159.002,229.928c-.222,0-.422-.111-.544-.311-.177-.3-.066-.688.233-.854l96.435-55.649c.3-.166.688-.066.855.233.178.3.067.688-.233.855L159.313,229.85c-.1.055-.2.078-.311.078Z" fill="#f5f5f5"/>
|
||||
<path d="M159.002,235.796c-.222,0-.422-.111-.544-.31-.177-.3-.066-.688.233-.855l96.435-55.648c.3-.166.688-.067.855.233.178.3.067.688-.233.854l-96.435,55.649c-.1.044-.2.077-.311.077Z" fill="#f5f5f5"/>
|
||||
<path d="M159.002,241.665c-.222,0-.422-.111-.544-.311-.177-.299-.066-.688.233-.854l96.435-55.648c.3-.167.688-.067.855.233.178.299.067.687-.233.854l-96.435,55.648c-.1.045-.2.078-.311.078Z" fill="#f5f5f5"/>
|
||||
<path d="M159.002,247.523c-.222,0-.422-.111-.544-.311-.177-.299-.066-.677.233-.854l45.754-26.404c.299-.178.688-.067.854.233.178.299.067.676-.233.854l-45.753,26.404c-.1.056-.2.078-.311.078Z" fill="#f5f5f5"/>
|
||||
<path d="m81.471 345.41 55.232-31.918c1.277-0.732 2.309-2.529 2.309-4.005v-29.71c0-1.475-1.032-2.074-2.309-1.331l-55.232 31.918c-1.2765 0.732-2.3088 2.529-2.3088 4.005v29.71c0 1.464 1.0323 2.063 2.3088 1.331z" fill="#fafafa" stroke="#f5f5f5" stroke-miterlimit="10" stroke-width=".9621"/>
|
||||
<path d="M84.6563,312.391l10.7002-6.18c.6438-.366,1.1544-.066,1.1544.666v12.348c0,.732-.5217,1.631-1.1544,1.997l-10.7002,6.179c-.6438.366-1.1543.067-1.1543-.665v-12.348c0-.732.5216-1.631,1.1543-1.997Z" fill="#bae7ff"/>
|
||||
<path d="M101.318,302.784l32.9-19.027c.255-.144.466-.033.466.266v1.62c0,.3-.211.655-.466.799l-32.9,19.026c-.256.145-.466.034-.466-.266v-1.62c0-.299.21-.654.466-.798Z" fill="#bae7ff"/>
|
||||
<path d="M101.318,308.941l32.9-19.027c.255-.144.466-.033.466.267v1.619c0,.3-.211.655-.466.799l-32.9,19.027c-.256.144-.466.033-.466-.267v-1.619c0-.289.21-.644.466-.799Z" fill="#bae7ff"/>
|
||||
<path d="M101.318,315.109l32.9-19.026c.255-.144.466-.034.466.266v1.62c0,.299-.211.654-.466.799l-32.9,19.026c-.256.144-.466.033-.466-.266v-1.62c0-.288.21-.655.466-.799Z" fill="#bae7ff"/>
|
||||
<path d="M83.9681,331.251l50.2489-29c.255-.144.466-.022.466.266v1.62c0,.3-.211.655-.466.799l-50.2489,29c-.2553.144-.4661.022-.4661-.266v-1.62c0-.299.2108-.654.4661-.799Z" fill="#bae7ff"/>
|
||||
<path d="M83.9681,337.42l50.2489-29c.255-.145.466-.023.466.266v1.62c0,.299-.211.654-.466.798L83.9681,340.105c-.2553.144-.4661.022-.4661-.267v-1.619c0-.3.2108-.655.4661-.799Z" fill="#bae7ff"/>
|
||||
<path d="M77.0523,308.23c-.1332,0-.2775-.066-.3441-.199-.111-.189-.0444-.433.1443-.544l1.1543-.666c.1887-.111.4329-.044.5439.145.111.188.0444.432-.1443.543l-1.1543.666c-.0555.044-.1332.055-.1998.055Z" fill="#bae7ff"/>
|
||||
<path d="M80.648,306.167c-.1332,0-.2775-.067-.3441-.2-.111-.188-.0444-.432.1443-.543l2.4308-1.409c.1887-.111.4329-.045.5439.144s.0444.433-.1443.544l-2.4308,1.409c-.0666.033-.1332.055-.1998.055Z" fill="#bae7ff"/>
|
||||
<path d="M85.5112,303.36c-.1332,0-.2775-.066-.3441-.199-.111-.189-.0444-.433.1443-.544l2.4309-1.409c.1887-.111.4329-.044.5439.144.111.189.0444.433-.1443.544l-2.4309,1.409c-.0666.033-.1332.055-.1998.055Z" fill="#bae7ff"/>
|
||||
<path d="M90.3726,300.543c-.1332,0-.2775-.067-.3441-.2-.111-.189-.0444-.433.1443-.544l2.4308-1.409c.1887-.111.4329-.044.5439.144.111.189.0444.433-.1443.544l-2.4308,1.409c-.0555.044-.1221.056-.1998.056Z" fill="#bae7ff"/>
|
||||
<path d="M95.2456,297.735c-.1332,0-.2775-.066-.3441-.199-.111-.189-.0444-.433.1443-.544l2.4309-1.409c.1887-.111.4329-.044.5439.144.111.189.0444.433-.1443.544l-2.4309,1.409c-.0666.044-.1332.055-.1998.055Z" fill="#bae7ff"/>
|
||||
<path d="M100.107,294.929c-.1333,0-.2776-.067-.3442-.2-.111-.189-.0444-.433.1443-.544l2.4309-1.409c.189-.11.433-.044.544.145.111.188.044.432-.144.543l-2.431,1.409c-.056.045-.133.056-.2.056Z" fill="#bae7ff"/>
|
||||
<path d="M105.158,292.022c-.133,0-.278-.067-.344-.2-.111-.188-.045-.432.144-.543l2.431-1.409c.188-.111.433-.045.544.144s.044.433-.145.544l-2.43,1.409c-.067.033-.134.055-.2.055Z" fill="#bae7ff"/>
|
||||
<path d="M110.197,289.105c-.133,0-.278-.067-.344-.2-.111-.189-.045-.433.144-.544l2.431-1.409c.189-.111.433-.044.544.144.111.189.044.433-.145.544l-2.43,1.409c-.056.044-.134.056-.2.056Z" fill="#bae7ff"/>
|
||||
<path d="M115.248,286.187c-.134,0-.278-.067-.345-.2-.111-.189-.044-.433.145-.544l2.431-1.409c.188-.111.433-.044.544.145.111.188.044.432-.145.543l-2.431,1.409c-.066.044-.133.056-.199.056Z" fill="#bae7ff"/>
|
||||
<path d="M120.287,283.28c-.134,0-.278-.067-.344-.2-.111-.188-.045-.433.144-.543l2.431-1.409c.188-.111.433-.045.544.144.111.188.044.432-.145.543l-2.431,1.409c-.055.034-.122.056-.199.056Z" fill="#bae7ff"/>
|
||||
<path d="M125.337,280.362c-.133,0-.277-.066-.344-.199-.111-.189-.044-.433.145-.544l2.43-1.409c.189-.111.433-.044.544.144.111.189.045.433-.144.544l-2.431,1.409c-.066.044-.133.055-.2.055Z" fill="#bae7ff"/>
|
||||
<path d="M130.386,277.456c-.133,0-.277-.067-.344-.2-.111-.189-.044-.433.144-.544l2.431-1.409c.189-.111.433-.044.544.145.111.188.045.432-.144.543l-2.431,1.409c-.067.033-.133.056-.2.056Z" fill="#bae7ff"/>
|
||||
<path d="M135.427,274.538c-.133,0-.277-.067-.344-.2-.111-.189-.044-.433.144-.544l2.431-1.409c.189-.111.433-.044.544.145.111.188.045.432-.144.543l-2.431,1.409c-.067.045-.133.056-.2.056Z" fill="#bae7ff"/>
|
||||
<path d="M140.476,271.62c-.133,0-.277-.066-.344-.2-.111-.188-.044-.432.144-.543l2.431-1.409c.189-.111.433-.044.544.144.111.189.044.433-.144.544l-2.431,1.409c-.067.044-.133.055-.2.055Z" fill="#bae7ff"/>
|
||||
<path d="M145.517,268.713c-.133,0-.277-.066-.344-.199-.111-.189-.044-.433.144-.544l2.431-1.409c.189-.111.433-.044.544.144.111.189.044.433-.144.544l-2.431,1.409c-.056.033-.133.055-.2.055Z" fill="#bae7ff"/>
|
||||
<path d="M150.566,265.795c-.133,0-.278-.066-.344-.199-.111-.189-.045-.433.144-.544l2.431-1.409c.189-.111.433-.044.544.144.111.189.044.433-.144.544l-2.431,1.409c-.067.044-.133.055-.2.055Z" fill="#bae7ff"/>
|
||||
<path d="M155.605,262.889c-.133,0-.278-.066-.344-.199-.111-.189-.045-.433.144-.544l2.431-1.409c.189-.111.433-.044.544.144.111.189.044.433-.144.544l-2.431,1.409c-.056.033-.133.055-.2.055Z" fill="#bae7ff"/>
|
||||
<path d="M160.656,259.971c-.133,0-.278-.066-.344-.199-.111-.189-.045-.433.144-.544l2.431-1.409c.189-.111.433-.044.544.144.111.189.044.433-.145.544l-2.43,1.409c-.067.044-.134.055-.2.055Z" fill="#bae7ff"/>
|
||||
<path d="M165.695,257.053c-.133,0-.278-.066-.344-.199-.111-.189-.045-.433.144-.544l2.431-1.409c.189-.111.433-.044.544.144.111.189.044.433-.145.544l-2.43,1.409c-.056.044-.122.055-.2.055Z" fill="#bae7ff"/>
|
||||
<path d="M170.746,254.147c-.134,0-.278-.067-.344-.2-.111-.189-.045-.433.144-.544l2.431-1.409c.188-.111.433-.044.544.144.111.189.044.433-.145.544l-2.431,1.409c-.066.033-.133.056-.199.056Z" fill="#bae7ff"/>
|
||||
<path d="M175.796,251.229c-.133,0-.277-.067-.344-.2-.111-.188-.044-.432.145-.543l2.43-1.409c.189-.111.433-.045.544.144s.045.433-.144.544l-2.431,1.409c-.066.044-.133.055-.2.055Z" fill="#bae7ff"/>
|
||||
<path d="M180.835,248.322c-.133,0-.277-.066-.344-.199-.111-.189-.044-.433.145-.544l2.431-1.409c.188-.111.432-.044.543.144.111.189.045.433-.144.544l-2.431,1.409c-.055.033-.133.055-.2.055Z" fill="#bae7ff"/>
|
||||
<path d="M185.708,245.515c-.133,0-.277-.066-.344-.2-.111-.188-.044-.432.145-.543l2.431-1.409c.188-.111.432-.044.543.144.111.189.045.433-.144.544l-2.431,1.409c-.066.033-.133.055-.2.055Z" fill="#bae7ff"/>
|
||||
<path d="M190.57,242.697c-.133,0-.278-.066-.344-.199-.111-.189-.045-.433.144-.544l2.431-1.409c.189-.111.433-.044.544.144.111.189.044.433-.145.544l-2.43,1.409c-.067.044-.134.055-.2.055Z" fill="#bae7ff"/>
|
||||
<path d="M195.431,239.891c-.133,0-.277-.067-.344-.2-.111-.189-.044-.433.144-.544l1.155-.665c.188-.111.433-.045.544.144.111.188.044.432-.145.543l-1.154.666c-.056.044-.122.056-.2.056Z" fill="#bae7ff"/>
|
||||
<path d="M75.2765,352.364c-.222,0-.3995-.178-.3995-.4v-1.331c0-.222.1775-.4.3995-.4s.3996.178.3996.4v1.331c0,.211-.1776.4-.3996.4Z" fill="#bae7ff"/>
|
||||
<path d="M75.2765,348.469c-.222,0-.3995-.177-.3995-.399v-2.563c0-.222.1775-.399.3995-.399s.3996.177.3996.399v2.563c0,.222-.1776.399-.3996.399Zm0-5.125c-.222,0-.3995-.178-.3995-.4v-2.563c0-.221.1775-.399.3995-.399s.3996.178.3996.399v2.563c0,.222-.1776.4-.3996.4Zm0-5.115c-.222,0-.3995-.177-.3995-.399v-2.563c0-.222.1775-.399.3995-.399s.3996.177.3996.399v2.563c0,.222-.1776.399-.3996.399Zm0-5.114c-.222,0-.3995-.178-.3995-.4v-2.562c0-.222.1775-.4.3995-.4s.3996.178.3996.4v2.562c0,.222-.1776.4-.3996.4Zm0-5.115c-.222,0-.3995-.177-.3995-.399v-2.563c0-.222.1775-.399.3995-.399s.3996.177.3996.399v2.563c0,.222-.1776.399-.3996.399Zm0-5.114c-.222,0-.3995-.178-.3995-.399v-2.563c0-.222.1775-.4.3995-.4s.3996.178.3996.4v2.563c0,.221-.1776.399-.3996.399Zm0-5.126c-.222,0-.3995-.177-.3995-.399v-2.563c0-.222.1775-.399.3995-.399s.3996.177.3996.399v2.563c0,.222-.1776.399-.3996.399Z" fill="#bae7ff"/>
|
||||
<path d="M75.2765,312.646c-.222,0-.3995-.177-.3995-.399v-1.332c0-.221.1775-.399.3995-.399s.3996.178.3996.399v1.332c0,.222-.1776.399-.3996.399Z" fill="#bae7ff"/>
|
||||
<path d="M195.464,285.022c-.133,0-.277-.067-.344-.2-.111-.188-.044-.432.145-.543l1.154-.666c.189-.111.433-.044.544.144.111.189.044.433-.144.544l-1.155.665c-.055.045-.122.056-.2.056Z" fill="#bae7ff"/>
|
||||
<path d="M80.648,351.31c-.1332,0-.2775-.067-.3441-.2-.111-.189-.0444-.433.1443-.544l2.4308-1.409c.1887-.111.4329-.044.5439.145.111.188.0444.432-.1443.543l-2.4308,1.409c-.0666.045-.1332.056-.1998.056Z" fill="#bae7ff"/>
|
||||
<path d="M85.521,348.502c-.1332,0-.2775-.066-.3441-.199-.111-.189-.0444-.433.1443-.544l2.4309-1.409c.1887-.111.4328-.044.5438.144.111.189.0444.433-.1442.544l-2.4309,1.409c-.0666.033-.1332.055-.1998.055Z" fill="#bae7ff"/>
|
||||
<path d="M90.3823,345.685c-.1332,0-.2775-.067-.3441-.2-.111-.189-.0444-.433.1443-.544l2.4309-1.409c.1887-.111.4329-.044.5439.145.111.188.0444.432-.1443.543l-2.4309,1.409c-.0555.045-.1221.056-.1998.056Z" fill="#bae7ff"/>
|
||||
<path d="M95.2554,342.878c-.1332,0-.2775-.067-.3441-.2-.111-.188-.0444-.432.1443-.543l2.4308-1.409c.1887-.111.4329-.045.5439.144s.0444.433-.1443.544l-2.4308,1.408c-.0555.045-.1332.056-.1998.056Z" fill="#bae7ff"/>
|
||||
<path d="M100.128,340.071c-.1328,0-.2771-.066-.3437-.199-.111-.189-.0444-.433.1443-.544l2.4304-1.409c.189-.111.433-.044.544.144.111.189.045.433-.144.544l-2.431,1.409c-.066.033-.133.055-.2.055Z" fill="#bae7ff"/>
|
||||
<path d="M105.167,337.153c-.133,0-.277-.066-.344-.199-.111-.189-.044-.433.145-.544l2.431-1.409c.188-.111.432-.044.543.144.111.189.045.433-.144.544l-2.431,1.409c-.055.044-.122.055-.2.055Z" fill="#bae7ff"/>
|
||||
<path d="M110.218,334.247c-.133,0-.277-.066-.344-.2-.111-.188-.044-.432.144-.543l2.431-1.409c.189-.111.433-.045.544.144s.045.433-.144.544l-2.431,1.409c-.067.033-.133.055-.2.055Z" fill="#bae7ff"/>
|
||||
<path d="M115.269,331.329c-.133,0-.277-.066-.344-.2-.111-.188-.044-.432.144-.543l2.431-1.409c.189-.111.433-.044.544.144.111.189.044.433-.144.544l-2.431,1.409c-.067.033-.133.055-.2.055Z" fill="#bae7ff"/>
|
||||
<path d="M120.308,328.411c-.133,0-.277-.066-.344-.2-.111-.188-.044-.432.144-.543l2.431-1.409c.189-.111.433-.044.544.144.111.189.044.433-.144.544l-2.431,1.409c-.056.044-.133.055-.2.055Z" fill="#bae7ff"/>
|
||||
<path d="M125.359,325.504c-.133,0-.278-.066-.344-.199-.111-.189-.045-.433.144-.544l2.431-1.409c.189-.111.433-.044.544.144.111.189.044.433-.144.544l-2.431,1.409c-.067.033-.134.055-.2.055Z" fill="#bae7ff"/>
|
||||
<path d="M130.398,322.587c-.133,0-.278-.067-.344-.2-.111-.188-.045-.432.144-.543l2.431-1.409c.189-.111.433-.045.544.144s.044.433-.144.544l-2.431,1.408c-.056.045-.122.056-.2.056Z" fill="#bae7ff"/>
|
||||
<path d="M135.449,319.68c-.133,0-.278-.066-.344-.199-.111-.189-.045-.433.144-.544l2.431-1.409c.188-.111.433-.044.544.144.111.189.044.433-.145.544l-2.43,1.409c-.067.033-.134.055-.2.055Z" fill="#bae7ff"/>
|
||||
<path d="M140.498,316.762c-.134,0-.278-.066-.345-.199-.111-.189-.044-.433.145-.544l2.431-1.409c.188-.111.433-.044.544.144.111.189.044.433-.145.544l-2.431,1.409c-.066.033-.133.055-.199.055Z" fill="#bae7ff"/>
|
||||
<path d="M145.539,313.845c-.134,0-.278-.067-.345-.2-.111-.189-.044-.433.145-.544l2.431-1.408c.188-.111.433-.045.544.144.111.188.044.432-.145.543l-2.431,1.409c-.055.045-.133.056-.199.056Z" fill="#bae7ff"/>
|
||||
<path d="M150.587,310.938c-.133,0-.277-.067-.344-.2-.111-.188-.044-.432.145-.543l2.43-1.409c.189-.111.433-.045.544.144s.045.433-.144.544l-2.431,1.409c-.066.033-.133.055-.2.055Z" fill="#bae7ff"/>
|
||||
<path d="M155.626,308.02c-.133,0-.277-.067-.344-.2-.111-.188-.044-.432.145-.543l2.431-1.409c.188-.111.432-.045.543.144s.045.433-.144.544l-2.431,1.409c-.055.044-.133.055-.2.055Z" fill="#bae7ff"/>
|
||||
<path d="M160.677,305.103c-.133,0-.277-.067-.344-.2-.111-.189-.044-.433.144-.544l2.431-1.409c.189-.111.433-.044.544.145.111.188.045.432-.144.543l-2.431,1.409c-.067.044-.133.056-.2.056Z" fill="#bae7ff"/>
|
||||
<path d="M165.716,302.196c-.133,0-.277-.067-.344-.2-.111-.188-.044-.433.145-.543l2.43-1.409c.189-.111.433-.045.544.144.111.188.045.432-.144.543l-2.431,1.409c-.055.034-.122.056-.2.056Z" fill="#bae7ff"/>
|
||||
<path d="M170.767,299.278c-.133,0-.277-.067-.344-.2-.111-.188-.044-.432.144-.543l2.431-1.409c.189-.111.433-.045.544.144.111.188.044.432-.144.543l-2.431,1.409c-.067.045-.133.056-.2.056Z" fill="#bae7ff"/>
|
||||
<path d="M175.818,296.372c-.133,0-.278-.067-.344-.2-.111-.189-.045-.433.144-.544l2.431-1.409c.189-.111.433-.044.544.145.111.188.044.432-.144.543l-2.431,1.409c-.067.033-.134.056-.2.056Z" fill="#bae7ff"/>
|
||||
<path d="M180.857,293.454c-.133,0-.278-.067-.344-.2-.111-.189-.045-.433.144-.544l2.431-1.409c.189-.111.433-.044.544.145.111.188.044.432-.144.543l-2.431,1.409c-.056.033-.133.056-.2.056Z" fill="#bae7ff"/>
|
||||
<path d="M185.73,290.647c-.133,0-.278-.067-.344-.2-.111-.189-.045-.433.144-.544l2.431-1.409c.189-.111.433-.044.544.144.111.189.044.433-.144.544l-2.431,1.409c-.067.033-.133.056-.2.056Z" fill="#bae7ff"/>
|
||||
<path d="M190.603,287.829c-.133,0-.277-.067-.344-.2-.111-.189-.044-.433.144-.544l2.431-1.409c.189-.111.433-.044.544.145.111.188.044.432-.144.543l-2.431,1.409c-.067.045-.133.056-.2.056Z" fill="#bae7ff"/>
|
||||
<path d="M77.0523,353.384c-.1332,0-.2775-.066-.3441-.2-.111-.188-.0444-.432.1443-.543l1.1543-.666c.1887-.111.4329-.044.5439.144.111.189.0444.433-.1443.544l-1.1543.666c-.0555.033-.1332.055-.1998.055Z" fill="#bae7ff"/>
|
||||
<path d="M198.407,241.566c-.222,0-.399-.178-.399-.4v-1.331c0-.222.177-.399.399-.399s.4.177.4.399v1.331c0,.222-.178.4-.4.4Z" fill="#bae7ff"/>
|
||||
<path d="M198.407,277.377c-.222,0-.399-.177-.399-.399v-2.563c0-.222.177-.399.399-.399s.4.177.4.399v2.563c0,.222-.178.399-.4.399Zm0-5.114c-.222,0-.399-.177-.399-.399v-2.563c0-.222.177-.399.399-.399s.4.177.4.399v2.563c0,.222-.178.399-.4.399Zm0-5.114c-.222,0-.399-.178-.399-.4v-2.563c0-.221.177-.399.399-.399s.4.178.4.399v2.563c0,.222-.178.4-.4.4Zm0-5.115c-.222,0-.399-.177-.399-.399v-2.563c0-.222.177-.399.399-.399s.4.177.4.399v2.563c0,.222-.178.399-.4.399Zm0-5.114c-.222,0-.399-.178-.399-.4v-2.562c0-.222.177-.4.399-.4s.4.178.4.4v2.562c0,.222-.178.4-.4.4Zm0-5.115c-.222,0-.399-.177-.399-.399v-2.563c0-.222.177-.399.399-.399s.4.177.4.399v2.563c0,.211-.178.399-.4.399Zm0-5.125c-.222,0-.399-.178-.399-.399v-2.563c0-.222.177-.4.399-.4s.4.178.4.4v2.563c0,.221-.178.399-.4.399Z" fill="#bae7ff"/>
|
||||
<path d="M198.407,281.272c-.222,0-.399-.178-.399-.4v-1.331c0-.222.177-.399.399-.399s.4.177.4.399v1.331c0,.222-.178.4-.4.4Z" fill="#bae7ff"/>
|
||||
<path d="M196.632,241.255c-.067,0-.133-.022-.2-.055-.122-.067-.2-.2-.2-.344v-4.105c0-.144.078-.277.2-.344l3.552-2.052c.122-.067.278-.067.4,0c.122.066.2.199.2.344v4.104c0,.145-.078.278-.2.344l-3.552,2.053c-.067.044-.133.055-.2.055Zm.4-4.271v3.184l2.752-1.587v-3.184l-2.752,1.587Z" fill="#bae7ff"/>
|
||||
<path d="M196.632,286.408c-.067,0-.133-.022-.2-.055-.122-.067-.2-.2-.2-.344v-4.105c0-.144.078-.277.2-.344l3.552-2.052c.122-.067.278-.067.4,0c.122.066.2.2.2.344v4.105c0,.144-.078.277-.2.344l-3.552,2.052c-.067.033-.133.055-.2.055Zm.4-4.271v3.184l2.752-1.586v-3.184l-2.752,1.586Z" fill="#bae7ff"/>
|
||||
<path d="M73.5012,312.335c-.0666,0-.1332-.022-.1998-.055-.1221-.067-.1998-.2-.1998-.344v-4.105c0-.144.0777-.277.1998-.344l3.5519-2.052c.1221-.067.2775-.067.3996,0c.1221.066.1998.199.1998.344v4.104c0,.145-.0777.278-.1998.344L73.701,312.28c-.0666.044-.1332.055-.1998.055Zm.3995-4.271v3.184l2.7528-1.586v-3.184l-2.7528,1.586Z" fill="#bae7ff"/>
|
||||
<path d="M73.5012,357.489c-.0666,0-.1332-.023-.1998-.056-.1221-.066-.1998-.2-.1998-.344v-4.105c0-.144.0777-.277.1998-.344l3.5519-2.052c.1221-.067.2775-.067.3996,0s.1998.2.1998.344v4.105c0,.144-.0777.277-.1998.344l-3.5519,2.052c-.0666.033-.1332.056-.1998.056Zm.3995-4.272v3.184l2.7528-1.586v-3.184l-2.7528,1.586Z" fill="#bae7ff"/>
|
||||
<path d="M200.193,261.846c0,1.32-.799,2.851-1.787,3.417s-1.787-.034-1.787-1.354.799-2.851,1.787-3.417s1.787.034,1.787,1.354Z" fill="#bae7ff"/>
|
||||
<path d="M141.043,270.732c0,1.32-.799,2.851-1.787,3.417s-1.787-.033-1.787-1.353.799-2.851,1.787-3.417c.988-.577,1.787.033,1.787,1.353Z" fill="#bae7ff"/>
|
||||
<path d="M141.043,315.874c0,1.321-.799,2.852-1.787,3.417-.988.566-1.787-.033-1.787-1.353s.799-2.851,1.787-3.417s1.787.044,1.787,1.353Z" fill="#bae7ff"/>
|
||||
<path d="M77.0644,332.993c0,1.32-.7992,2.851-1.7871,3.417s-1.7871-.033-1.7871-1.353c0-1.321.7992-2.852,1.7871-3.417.9879-.577,1.7871.033,1.7871,1.353Z" fill="#bae7ff"/>
|
||||
<path d="M143.675,277.522c-.111,0-.222-.055-.289-.166-.089-.156-.033-.355.122-.444l50.637-29.222c.156-.089.356-.033.456.122.088.155.033.355-.123.444l-50.637,29.222c-.055.033-.111.044-.166.044Z" fill="#8c8c8c"/>
|
||||
<path d="M143.675,280.606c-.111,0-.222-.055-.289-.166-.089-.156-.033-.355.122-.444l50.637-29.222c.156-.089.356-.045.456.122.088.155.033.355-.123.444l-50.637,29.222c-.055.022-.111.044-.166.044Z" fill="#8c8c8c"/>
|
||||
<path d="M143.675,283.679c-.111,0-.222-.055-.289-.166-.089-.155-.033-.355.122-.444l50.637-29.222c.156-.089.356-.044.456.122.088.155.033.355-.123.444l-50.637,29.222c-.055.033-.111.044-.166.044Z" fill="#8c8c8c"/>
|
||||
<path d="M143.675,286.764c-.111,0-.222-.056-.289-.167-.089-.155-.033-.355.122-.443l50.637-29.222c.156-.089.356-.034.456.122.088.155.033.355-.123.443l-50.637,29.222c-.055.034-.111.045-.166.045Z" fill="#8c8c8c"/>
|
||||
<path d="M143.675,289.848c-.111,0-.222-.056-.289-.167-.089-.155-.033-.355.122-.443l50.637-29.223c.156-.088.356-.044.456.123.088.155.033.355-.123.443l-50.637,29.222c-.055.022-.111.045-.166.045Z" fill="#8c8c8c"/>
|
||||
<path d="M143.675,292.921c-.111,0-.222-.056-.289-.167-.089-.155-.033-.355.122-.444l24.02-13.867c.156-.089.355-.045.444.122.089.155.033.355-.122.444l-24.02,13.867c-.044.034-.1.045-.155.045Z" fill="#8c8c8c"/>
|
||||
<path d="M143.675,300.021c-.111,0-.222-.055-.289-.166-.089-.156-.033-.355.122-.444l50.637-29.222c.156-.089.356-.045.456.122.088.155.033.355-.123.444l-50.637,29.222c-.055.033-.111.044-.166.044Z" fill="#8c8c8c"/>
|
||||
<path d="M143.675,303.094c-.111,0-.222-.055-.289-.166-.089-.155-.033-.355.122-.444l50.637-29.222c.156-.089.356-.044.456.122.088.155.033.355-.123.444L143.841,303.05c-.055.033-.111.044-.166.044Z" fill="#8c8c8c"/>
|
||||
<path d="M143.675,306.178c-.111,0-.222-.055-.289-.166-.089-.155-.033-.355.122-.444l50.637-29.222c.156-.089.356-.033.456.122.088.155.033.355-.123.444l-50.637,29.222c-.055.033-.111.044-.166.044Z" fill="#8c8c8c"/>
|
||||
<path d="M143.675,309.262c-.111,0-.222-.055-.289-.166-.089-.155-.033-.355.122-.444l24.02-13.867c.156-.089.355-.034.455.122.089.155.034.355-.122.454l-24.02,13.868c-.055.022-.111.033-.166.033Z" fill="#8c8c8c"/>
|
||||
<path d="M220.151,272.684c-.044,0-.089-.011-.133-.033-.078-.044-.133-.133-.133-.233v-46.607c0-.099.055-.177.133-.232l37.495-21.634c.078-.044.189-.044.266,0c.078.044.134.133.134.233v46.606c0,.1-.056.189-.134.233l-37.495,21.634c-.044.022-.088.033-.133.033Zm.267-46.717v45.996l36.962-21.334v-45.996l-36.962,21.334Z" fill="#f5f5f5"/>
|
||||
<path d="M223.038,251.039c-.045,0-.089-.011-.133-.033-.078-.044-.134-.133-.134-.233v-23.298c0-.099.056-.177.134-.233l31.723-18.305c.078-.044.189-.044.266,0c.078.044.133.133.133.233v23.298c0,.1-.055.177-.133.233l-31.723,18.305c-.044.022-.089.033-.133.033Zm.266-23.408v22.687l31.191-18.006v-22.687l-31.191,18.006Z" fill="#bae7ff"/>
|
||||
<path d="M223.336,253.924l8.036-4.637c.167-.1.311-.023.311.177v.954c0,.2-.133.433-.311.533l-8.036,4.637c-.166.1-.311.022-.311-.177v-.955c.011-.188.145-.432.311-.532Z" fill="#bae7ff"/>
|
||||
<path d="M223.336,257.252l30.147-17.351c.167-.1.311-.022.311.177v.954c0,.2-.133.433-.311.533l-30.147,17.351c-.166.1-.311.022-.311-.177v-.954c.011-.189.145-.433.311-.533Z" fill="#f5f5f5"/>
|
||||
<path d="M223.336,263.92l30.147-17.351c.167-.1.311-.022.311.177v.954c0,.2-.133.433-.311.533l-30.147,17.351c-.166.1-.311.022-.311-.177v-.954c.011-.2.145-.444.311-.533Z" fill="#f5f5f5"/>
|
||||
<path d="M223.336,260.581l30.147-17.351c.167-.1.311-.022.311.177v.955c0,.199-.133.432-.311.532l-30.147,17.351c-.166.1-.311.023-.311-.177v-.954c.011-.189.145-.433.311-.533Z" fill="#f5f5f5"/>
|
||||
<path d="M254.76,232.734c-.011,0-.023,0-.045,0l-11.033-1.742c-.144-.022-.244-.155-.222-.299s.144-.244.3-.222l11.033,1.742c.144.022.244.155.222.299-.011.122-.122.222-.255.222Z" fill="#bae7ff"/>
|
||||
<path d="M234.081,229.484c-.011,0-.022,0-.044,0l-11.044-1.742c-.145-.022-.245-.155-.222-.299.022-.144.155-.244.299-.222l11.045,1.742c.144.022.244.155.222.299-.011.122-.134.222-.256.222Z" fill="#bae7ff"/>
|
||||
<path d="M223.037,251.04c-.055,0-.111-.022-.166-.056-.122-.088-.144-.255-.044-.377l11.033-14.478c.089-.111.255-.144.377-.044.122.089.144.255.045.377L223.248,250.94c-.055.067-.133.1-.211.1Z" fill="#bae7ff"/>
|
||||
<path d="M243.705,223.925c-.055,0-.111-.022-.166-.055-.122-.089-.144-.255-.044-.377l11.055-14.5c.089-.111.255-.145.377-.045.122.089.145.255.045.377l-11.056,14.5c-.055.067-.133.1-.211.1Z" fill="#bae7ff"/>
|
||||
<path d="M238.898,228.307c-.644.366-1.166,1.476-1.166,2.452s.522,1.487,1.166,1.121s1.165-1.476,1.165-2.452-.532-1.498-1.165-1.121Z" fill="#bae7ff"/>
|
||||
<path d="M241.385,225.29l-.888.51v-.466c0-.3-.155-.444-.344-.333l-2.52,1.453c-.188.111-.344.444-.344.733v.466l-.888.51c-.399.233-.721.91-.721,1.52v4.515c0,.61.322.921.721.688l4.984-2.873c.4-.233.721-.91.721-1.52v-4.516c0-.61-.321-.92-.721-.687Zm-2.486,7.477c-.955.555-1.743-.2-1.743-1.675c0-1.476.777-3.129,1.743-3.683.954-.555,1.742.199,1.742,1.675-.011,1.475-.788,3.128-1.742,3.683Z" fill="#bae7ff"/>
|
||||
<path d="M266.294,246.047c-.045,0-.089-.011-.133-.033-.078-.045-.134-.133-.134-.233v-46.607c0-.1.056-.177.134-.233l37.495-21.633c.077-.045.188-.045.266,0c.078.044.133.133.133.233v46.606c0,.1-.055.189-.133.233l-37.495,21.634c-.033.022-.078.033-.133.033Zm.266-46.717v45.996l36.962-21.334v-45.996L266.56,199.33Z" fill="#f5f5f5"/>
|
||||
<path d="M269.18,224.413c-.044,0-.088-.011-.133-.033-.077-.044-.133-.133-.133-.233v-23.298c0-.099.056-.177.133-.233l31.723-18.305c.078-.044.189-.044.267,0s.133.133.133.233v23.298c0,.1-.055.188-.133.233L269.314,224.38c-.034.022-.089.033-.134.033Zm.267-23.419v22.687l31.19-18.006v-22.687l-31.19,18.006Z" fill="#bae7ff"/>
|
||||
<path d="M269.49,227.298l8.037-4.637c.166-.1.311-.023.311.177v.954c0,.2-.134.433-.311.533l-8.037,4.637c-.166.1-.31.022-.31-.177v-.954c0-.2.144-.433.31-.533Z" fill="#bae7ff"/>
|
||||
<path d="M269.49,230.626l30.148-17.351c.166-.1.31-.022.31.177v.955c0,.199-.133.432-.31.532L269.49,232.29c-.166.1-.31.022-.31-.177v-.954c0-.2.144-.433.31-.533Z" fill="#f5f5f5"/>
|
||||
<path d="M269.49,237.283l30.148-17.351c.166-.1.31-.023.31.177v.954c0,.2-.133.433-.31.533L269.49,238.947c-.166.1-.31.022-.31-.177v-.955c0-.199.144-.432.31-.532Z" fill="#f5f5f5"/>
|
||||
<path d="M269.49,233.954l30.148-17.351c.166-.1.31-.022.31.178v.954c0,.199-.133.432-.31.532L269.49,235.618c-.166.1-.31.023-.31-.177v-.954c0-.2.144-.433.31-.533Z" fill="#f5f5f5"/>
|
||||
<path d="M285.042,198.342c-2.387,1.376-4.329,4.737-4.329,7.489c0,2.762,1.942,3.871,4.329,2.496c2.386-1.376,4.329-4.737,4.329-7.489c0-2.751-1.932-3.872-4.329-2.496Zm2.264,4.049l-3.23,3.861c-.311.377-.566.289-.566-.211v-3.616c0-.488.4-.877.888-.855l2.597.1c.489.023.633.344.311.721Z" fill="#bae7ff"/>
|
||||
<path d="M255.649,100.803c-.422-.189-.988-.134-1.599.221L80.0167,201.271c-1.3208.766-2.3975,2.63-2.3975,4.161v10.062c0,.732.2442,1.265.6549,1.531-.4107-.266-4.0404-2.319-4.4844-2.585-.4217-.255-.6881-.788-.6881-1.542v-10.062c0-1.531,1.0766-3.395,2.4086-4.161L249.533,98.4173c.677-.3883,1.287-.4105,1.72-.1553.389.233,3.541,2.019,4.396,2.541Z" fill="#096dd9"/>
|
||||
<path d="M80.0161,201.271L254.05,101.025c1.332-.766,2.397-.145,2.397,1.386v10.063c0,1.531-1.076,3.395-2.397,4.16L80.0161,216.881c-1.332.765-2.4087.144-2.4087-1.387v-10.062c0-1.531,1.0767-3.395,2.4087-4.161Z" fill="#91d5ff"/>
|
||||
<path d="M78.3185,203.069l-4.5066-2.608c-.4328.755-.7103,1.609-.7103,2.375v10.062c0,.754.2664,1.287.6881,1.542.444.266,4.0737,2.319,4.4844,2.585-.4107-.266-.6549-.799-.6549-1.531v-10.062c0-.766.2664-1.609.6993-2.363Z" opacity=".15"/>
|
||||
<path d="M255.649,100.803c-.854-.522-4.007-2.308-4.395-2.541-.433-.2552-1.044-.233-1.721.1553L75.5108,198.675c-.666.377-1.2654,1.043-1.6983,1.786l4.5065,2.608c.4329-.755,1.0323-1.409,1.6983-1.798L254.051,101.024c.61-.355,1.176-.41,1.598-.221Z" fill="#fff" opacity=".4"/>
|
||||
<path d="M247.434,112.651c0,1.442-1.076,3.217-2.408,3.994l-58.319,33.649c-1.331.765-2.408.221-2.408-1.221s1.077-3.217,2.397-3.994l58.319-33.648c1.343-.766,2.419-.222,2.419,1.22Z" fill="#fafafa"/>
|
||||
<path d="M267.649,97.5518l93.183-53.7845c1.198-.6878,2.164-.1331,2.164,1.2537v84.094c0,1.375-.966,3.062-2.164,3.75l-93.183,53.784c-1.199.688-2.165.133-2.165-1.254v-84.093c0-1.3871.966-3.0623,2.165-3.7502Z" fill="#f0f0f0"/>
|
||||
<path d="M495.62,287.328c11.977,6.745,12.099,17.884.267,24.884-11.833,7.001-31.124,7.212-43.112.466-11.977-6.745-12.099-17.883-.266-24.884c11.832-7,31.135-7.2,43.111-.466Z" fill="#fafafa"/>
|
||||
<path d="m484.92 129.11v174.57l8.591-6.823 1.343-171.32-9.934 3.572z" fill="#d9d9d9"/>
|
||||
<path d="m474.17 123.4c3.208-2.974 12.776 2.618 12.776 5.791l-2.03 174.45-17.816-7.357 7.07-172.88z" fill="#f0f0f0"/>
|
||||
<path d="M476.207,93.1135c3.208-2.6737,6.393-3.3837,8.935-2.3742v-.0111l15.784,7.533-.366.9984c1.032,1.3094,1.798,3.0844,2.186,5.3254c1.399,8.065-2.564,18.86-8.857,24.096-1.721,1.432-3.441,2.286-5.051,2.63l-.377,1.043-15.74-7.522c-2.641-.843-4.65-3.462-5.372-7.633-1.398-8.054,2.564-18.838,8.858-24.0855Z" fill="#1890ff"/>
|
||||
<path d="M476.207,93.1135c3.208-2.6737,6.393-3.3837,8.935-2.3742v-.0111l15.784,7.533-.366.9984c1.032,1.3094,1.798,3.0844,2.186,5.3254c1.399,8.065-2.564,18.86-8.857,24.096-1.721,1.432-3.441,2.286-5.051,2.63l-.377,1.043-15.74-7.522c-2.641-.843-4.65-3.462-5.372-7.633-1.398-8.054,2.564-18.838,8.858-24.0855Z" opacity=".15"/>
|
||||
<g transform="translate(1e-6 1e-6)">
|
||||
<path d="m417.9 109.38-20.391 38.331-6.504-4.139 20.324-37.986 6.571 3.794z" fill="#f0f0f0"/>
|
||||
<path d="m378.54 158.89 18.97-11.183-6.505-4.138-19.047 11.538 6.582 3.783z" fill="#f0f0f0"/>
|
||||
<path d="m421.14 111.11-3.241-1.731-20.39 38.331-18.97 11.183v3.639l22.022-13.114 20.579-38.308z" fill="#d9d9d9"/>
|
||||
<path d="M425.28,104.818l51.836,11.349c2.897-.466,4.873-3.195,4.406-6.102-.466-2.895-3.196-4.87-6.104-4.404-.056.011-.134.022-.189.033l-51.381-8.8307c-2.187.4327-3.608,2.5627-3.175,4.7487.422,2.141,2.476,3.55,4.607,3.206Z" fill="#f0f0f0"/>
|
||||
<path d="M425.28,104.818l51.836,11.349c2.897-.466,4.873-3.195,4.406-6.102-.466-2.895-3.196-4.87-6.104-4.404-.056.011-.134.022-.189.033l-51.381-8.8307c-2.187.4327-3.608,2.5627-3.175,4.7487.422,2.141,2.476,3.55,4.607,3.206Z" fill="#f0f0f0" opacity=".7"/>
|
||||
<path d="M412.326,86.7785c2.287-1.8971,4.551-2.4074,6.36-1.6863v-.0111l11.156,5.7246-.256.71c.733.9319,1.288,2.1967,1.554,3.7942.999,5.7361-1.831,13.4131-6.304,17.1401-1.221,1.021-2.442,1.631-3.597,1.875l-.266.744-11.122-5.725v-.011c-1.876-.599-3.308-2.452-3.83-5.414-.987-5.7357,1.832-13.4128,6.305-17.1405Z" fill="#096dd9"/>
|
||||
<path d="M412.326,86.7785c2.287-1.8971,4.551-2.4074,6.36-1.6863v-.0111l11.156,5.7246-.256.71c.733.9319,1.288,2.1967,1.554,3.7942.999,5.7361-1.831,13.4131-6.304,17.1401-1.221,1.021-2.442,1.631-3.597,1.875l-.266.744-11.122-5.725v-.011c-1.876-.599-3.308-2.452-3.83-5.414-.987-5.7357,1.832-13.4128,6.305-17.1405Z" opacity=".15"/>
|
||||
<path d="m423.72 92.592c-4.473 3.7277-7.293 11.405-6.305 17.14 0.999 5.736 5.428 7.367 9.901 3.639 4.473-3.727 7.293-11.405 6.305-17.14-0.988-5.7468-5.428-7.3776-9.901-3.6389z" fill="#1890ff"/>
|
||||
<path d="M421.639,106.205c.122.687.344,1.253.644,1.675.887,1.298,2.486,1.398,4.095.066c2.132-1.786,3.486-5.447,3.008-8.1982-.31-1.7862-1.298-2.7292-2.541-2.6848-.689,0-1.432.3107-2.187.9541-2.142,1.7862-3.497,5.4469-3.019,8.1879Z" fill="#096dd9"/>
|
||||
<path d="M421.639,106.205c.122.687.344,1.253.644,1.675.887,1.298,2.486,1.398,4.095.066c2.132-1.786,3.486-5.447,3.008-8.1982-.31-1.7862-1.298-2.7292-2.541-2.6848-.689,0-1.432.3107-2.187.9541-2.142,1.7862-3.497,5.4469-3.019,8.1879Z" opacity=".3"/>
|
||||
<path d="M421.639,106.205c.122.687.344,1.253.644,1.675.688,0,1.442-.322,2.197-.954c2.131-1.786,3.486-5.448,3.008-8.1878-.122-.6878-.344-1.2536-.643-1.6752-.689,0-1.432.3106-2.187.9541-2.142,1.7861-3.497,5.4469-3.019,8.1879Z" fill="#455a64"/>
|
||||
<path d="m371.96 158.74 6.582 3.783v-3.627l-6.582-3.784v3.628z" fill="#f0f0f0"/>
|
||||
<path d="m418.53 94.4-6.582-3.8164-21.689-27.913 5.328-1.0872 22.943 32.816z" fill="#f0f0f0"/>
|
||||
<path d="M384.245,65.2119c-.378-.1664-.888-.122-1.443.1997L289.619,119.196c-1.188.688-2.165,2.374-2.165,3.75v84.094c0,.654.222,1.142.589,1.375-.367-.244-3.641-2.085-4.041-2.329-.377-.233-.621-.71-.621-1.387v-84.094c0-1.376.966-3.062,2.175-3.75l93.172-53.7843c.611-.355,1.166-.3661,1.554-.1442.355.1997,3.197,1.8083,3.963,2.2854Z" fill="#096dd9" opacity=".7"/>
|
||||
<path d="M289.62,119.185l93.182-53.7846c1.199-.6878,2.165-.1331,2.165,1.2536v84.094c0,1.375-.966,3.062-2.165,3.75L289.62,208.282c-1.199.688-2.165.133-2.165-1.254v-84.093c0-1.376.966-3.062,2.165-3.75Z" fill="#1890ff"/>
|
||||
<path d="M288.088,120.805l-4.063-2.341c-.399.677-.632,1.442-.632,2.141v84.094c0,.677.244,1.165.621,1.387.4.244,3.674,2.085,4.04,2.329-.366-.244-.588-.721-.588-1.375v-84.094c-.011-.699.233-1.465.622-2.141Z" opacity=".15"/>
|
||||
<path d="M384.245,65.2117c-.765-.466-3.607-2.0746-3.962-2.2854-.389-.233-.944-.2108-1.554.1442L285.557,116.855c-.599.344-1.143.932-1.532,1.609l4.063,2.341c.388-.677.932-1.276,1.532-1.62l93.182-53.7847c.555-.3107,1.066-.3661,1.443-.1886Z" fill="#fff" opacity=".5"/>
|
||||
<path d="M348.448,147.087l7.371,10.351-33.289-30.276-35.075,59.387v20.491c0,1.375.966,1.941,2.165,1.253l93.182-53.784c1.199-.688,2.165-2.374,2.165-3.75v-11.261l-24.819-17.861-11.7,25.45Z" fill="#fafafa" opacity=".95"/>
|
||||
<path d="m352.46 97.296c4.34-2.5073 7.87-0.4771 7.87 4.5379 0 5.014-3.518 11.116-7.87 13.623-4.34 2.508-7.869 0.477-7.869-4.537 0-5.026 3.518-11.117 7.869-13.624z" fill="#fafafa" opacity=".95"/>
|
||||
<path d="M383.768,66.1103c.189,0,.2.4881.2.5436v84.0941c0,1.02-.777,2.374-1.665,2.884L289.12,207.417c-.255.144-.411.166-.466.166-.189,0-.2-.488-.2-.544v-84.093c0-1.021.777-2.374,1.665-2.885l93.183-53.7843c.255-.1442.41-.1664.466-.1664Zm0-.9985c-.289,0-.622.0999-.966.2996L289.62,119.196c-1.199.688-2.165,2.374-2.165,3.75v84.093c0,.977.488,1.543,1.199,1.543.288,0,.621-.1.966-.3l93.182-53.785c1.199-.687,2.165-2.374,2.165-3.749v-84.0941c0-.9763-.489-1.5421-1.199-1.5421Z" fill="#096dd9"/>
|
||||
<path d="m418.53 94.4 2.609-2.8845-24.564-35.046-23.998 13.169v4.7483l22.599-12.137 23.354 32.151z" fill="#d9d9d9"/>
|
||||
<path d="m372.58 74.386-7.593-4.3822v-4.7483l7.593 4.3822v4.7483z" fill="#d9d9d9"/>
|
||||
<path d="m364.99 65.256 23.987-13.18 7.603 4.3933-23.997 13.169-7.593-4.3822z" fill="#f0f0f0"/>
|
||||
</g>
|
||||
<path d="m492.32 100.76c-6.294 5.248-10.256 16.031-8.858 24.097 1.399 8.065 7.637 10.351 13.919 5.114 6.294-5.247 10.257-16.031 8.858-24.096-1.398-8.0659-7.637-10.351-13.919-5.115z" fill="#1890ff"/>
|
||||
<path d="M489.405,119.895c.166.965.477,1.764.91,2.363c1.243,1.819,3.496,1.964,5.761.089c2.996-2.508,4.895-7.666,4.229-11.527-.433-2.507-1.821-3.839-3.575-3.772-.965.011-2.009.444-3.074,1.331-3.03,2.518-4.929,7.677-4.251,11.516Z" fill="#096dd9"/>
|
||||
<path d="M489.405,119.895c.166.965.477,1.764.91,2.363c1.243,1.819,3.496,1.964,5.761.089c2.996-2.508,4.895-7.666,4.229-11.527-.433-2.507-1.821-3.839-3.575-3.772-.965.011-2.009.444-3.074,1.331-3.03,2.518-4.929,7.677-4.251,11.516Z" opacity=".3"/>
|
||||
<path d="M489.405,119.895c.166.965.477,1.764.91,2.363.965-.011,2.02-.444,3.086-1.342c2.996-2.508,4.895-7.667,4.229-11.505-.167-.965-.478-1.764-.911-2.363-.965.011-2.009.444-3.074,1.331-3.019,2.519-4.917,7.677-4.24,11.516Z" fill="#455a64"/>
|
||||
<path d="M221.784,239.834l-30.769,83.683-.011.011c-.067.211-.244.411-.544.577-.777.444-2.031.444-2.808,0-.333-.188-.522-.432-.577-.688-.011-.077-.011-.155,0-.233.011-.188.044-.377.111-.576l30.846-84.15c.377-1.031,1.521-1.564,2.553-1.176c1.054.389,1.576,1.52,1.199,2.552Z" fill="#d9d9d9"/>
|
||||
<path d="m181.77 294.02-2.22 3.843 30.846 17.801 2.22-3.843-30.846-17.801z" fill="#d9d9d9"/>
|
||||
<path d="m204.2 281.07-2.22 3.843 30.847 17.801 2.22-3.843-30.847-17.801z" fill="#91d5ff"/>
|
||||
<path d="m204.2 281.07-2.22 3.843 30.847 17.801 2.22-3.843-30.847-17.801z" opacity=".6"/>
|
||||
<path d="M196.688,234.087l16.449-9.497c1.343-.776,2.853-.887,4.13-.321c1.276.565,2.197,1.764,2.519,3.272l27.317,127.716c.022.089.033.178.033.267.011.088.011.188.011.277-.044.244-.233.488-.577.688-.777.444-2.031.444-2.808,0-.289-.178-.466-.377-.544-.588-.011-.044-.022-.078-.022-.122L215.901,228.374c-.066-.278-.177-.444-.255-.489-.078-.033-.278-.011-.522.134l-16.461,9.496" fill="#d9d9d9"/>
|
||||
<path d="M210.496,295.205l-10.024-5.781c-.333-.188-.599-.654-.599-1.031v-.244c0-.378.266-.843.599-1.032l24.043-13.879c.333-.188.865-.188,1.198,0l10.023,5.78c.333.189.6.655.6,1.032v.233c0,.377-.267.843-.6,1.032l-24.042,13.878c-.333.2-.865.2-1.198.012Z" fill="#f5f5f5"/>
|
||||
<path d="M211.096,293.929v1.42c.222,0,.433-.044.599-.144l12.021-6.934l12.021-6.934c.333-.189.6-.654.6-1.032v-.233c0-.188-.067-.399-.178-.588l-25.063,14.445Z" opacity=".15"/>
|
||||
<path d="M200.051,287.561c-.111.188-.178.399-.178.588v.244c0,.377.266.843.599,1.031l10.024,5.78c.166.1.377.145.599.145v-1.42l-11.044-6.368Z" opacity=".1"/>
|
||||
<path d="M206.155,274.891l-16.649-9.607c-.333-.189-.6-.655-.6-1.032v-.233c0-.377.267-.843.6-1.032l24.042-13.879c.333-.188.866-.188,1.199,0l16.649,9.608c.333.188.6.654.6,1.032v.233c0,.377-.267.843-.6,1.031l-24.042,13.879c-.333.2-.866.2-1.199,0Z" fill="#f5f5f5"/>
|
||||
<path d="M206.754,273.626v1.42c.222,0,.433-.044.599-.144l12.021-6.934l12.021-6.934c.333-.188.6-.654.6-1.031v-.233c0-.189-.067-.4-.178-.588l-25.063,14.444Z" opacity=".15"/>
|
||||
<path d="M189.072,263.42c-.111.189-.177.399-.177.588v.233c0,.377.266.843.599,1.032l16.65,9.607c.166.1.377.144.599.144v-1.42L189.072,263.42Z" opacity=".1"/>
|
||||
<path d="M223.537,356.134l-10.024-5.78c-.333-.189-.599-.655-.599-1.032v-.233c0-.377.266-.843.599-1.032l24.043-13.878c.333-.189.865-.189,1.198,0l10.024,5.78c.333.188.599.654.599,1.031v.233c0,.378-.266.844-.599,1.032l-24.043,13.879c-.333.189-.877.189-1.198,0Z" fill="#f5f5f5"/>
|
||||
<path d="M213.092,348.49c-.111.189-.178.4-.178.588v.233c0,.377.266.843.599,1.032l10.024,5.78c.166.1.377.144.599.144v-1.42l-11.044-6.357Z" opacity=".1"/>
|
||||
<path d="M224.137,354.858v1.42c.211,0,.433-.044.599-.144l12.021-6.934l12.021-6.934c.333-.188.6-.654.6-1.031v-.233c0-.189-.067-.4-.178-.588l-25.063,14.444Z" opacity=".15"/>
|
||||
<path d="M219.187,335.82l-10.023-5.78c-.333-.188-.6-.654-.6-1.031v-.233c0-.378.267-.843.6-1.032l24.042-13.879c.333-.189.866-.189,1.199,0l10.023,5.78c.333.189.599.655.599,1.032v.233c0,.377-.266.843-.599,1.032L220.386,335.82c-.333.189-.866.189-1.199,0Z" fill="#f5f5f5"/>
|
||||
<path d="M208.742,328.177c-.111.188-.178.399-.178.588v.233c0,.377.267.843.6,1.032l10.023,5.78c.166.099.377.144.599.144v-1.42l-11.044-6.357Z" opacity=".1"/>
|
||||
<path d="M219.787,334.545v1.42c.211,0,.433-.045.6-.144l12.021-6.934l12.021-6.934c.333-.189.599-.655.599-1.032v-.233c0-.189-.067-.399-.178-.588l-25.063,14.445Z" opacity=".15"/>
|
||||
<path d="M214.845,315.518l-10.023-5.78c-.333-.189-.599-.655-.599-1.032v-.233c0-.377.266-.843.599-1.032l24.042-13.879c.333-.188.866-.188,1.199,0l10.023,5.78c.333.189.599.655.599,1.032v.233c0,.377-.266.843-.599,1.032l-24.042,13.879c-.333.188-.866.188-1.199,0Z" fill="#f5f5f5"/>
|
||||
<path d="M204.4,307.863c-.111.188-.177.399-.177.588v.233c0,.377.266.843.599,1.032l5.006,2.895l5.006,2.896c.167.1.377.144.599.144v-1.42L204.4,307.863Z" opacity=".1"/>
|
||||
<path d="M215.445,314.242v1.42c.211,0,.433-.044.6-.144l12.021-6.934l12.021-6.934c.333-.188.599-.654.599-1.032v-.233c0-.188-.066-.399-.177-.588l-25.064,14.445Z" opacity=".15"/>
|
||||
<path d="m240.38 304.8-0.844-8.853-7.514 0.444s2.553 9.696 2.974 10.594c1.31 0.178 5.384-2.185 5.384-2.185z" fill="#ffa8a7"/>
|
||||
<path d="M242.718,307.863c.056.322.034.666.012.987-.078,1.143-.145,2.297-.222,3.439-.034.466-.067.899-.167,1.354-.111.51-.033,1.109-.044,1.631-.011.566-.011,1.142-.011,1.708c0,.222-.045.511.011.732.066.233-3.586,2.386-5.062,3.218-.089.044-.166.089-.266.133-1.343.544-2.409-.533-2.409-.533-1.077-2.751.944-4.515.944-4.515s.188.011.133-1.442c-.011-1.032-.033-2.075-.144-3.107-.089-.754-.245-1.519-.389-2.263-.144-.787-.211-1.464-.433-2.518-.055-.266-.166-.544-.055-.799.077-.166.222-.299.366-.41c1.41-1.099,2.975-1.764,4.773-1.809.2-.011.422,0,.566.133.133.111.178.289.222.444.1.377.155.899.366,1.232.156.255.444.51.666.699.422.388.921.876,1.088,1.442.022.089.044.166.055.244Z" fill="#bae7ff"/>
|
||||
<path d="M243.44,308.229c.688.255.533,2.13.122,3.938-.411,1.809-.633,2.441-.4,3.939.233,1.497.367,2.163-.233,2.729-.599.554-5.439,2.884-5.439,2.884s.933-2.318,1.021-5.647c.089-3.328.289-5.469,1.81-7c1.52-1.531,3.119-.843,3.119-.843Z" fill="#263238"/>
|
||||
<path d="M231.263,318.835c0,0,1.909,1.498,4.084,1.72c2.176.222,2.276-.611,2.476-3.195.199-2.574.244-6.136,1.232-7.711.999-1.575,2.686-2.296,3.363-2.141.133-.078.81.41,1.032.71c0,0-1.487-.166-2.83,1.176s-1.554,4.138-1.587,6.69c-.034,2.54-.111,5.491-2.132,5.824-2.02.333-4.184-.51-5.538-1.608-.789-.633-.1-1.465-.1-1.465Z" fill="#37474f"/>
|
||||
<path d="M235.503,316.017c-.177.233-.333.466-.466.699-.644,1.131-.71,2.318-.388,3.727-2.598-.199-3.375-1.608-3.375-1.608s-.155-1.287,1.321-2.252c.189-.122.377-.222.555-.3.011,0,.022-.011.022-.011c1.243-.544,2.331-.255,2.331-.255Z" fill="#91d5ff"/>
|
||||
<path d="M235.646,315.074c0,0-.267.177-.3.477-.022.288.1.965.755,1.331.277.155.511.089.067-.444-.178-.21-.322-.366-.4-.61-.122-.355-.122-.754-.122-.754Z" fill="#263238"/>
|
||||
<path d="M235.602,313.243c0,0-.199.2-.199.433-.012.233.432,1.187.91,1.387.477.21.222-.278-.011-.511-.122-.133-.356-.488-.5-.787-.133-.256-.2-.522-.2-.522Z" fill="#263238"/>
|
||||
<path d="M235.458,311.269c0,0-.178.188-.111.454.044.2.855.766,1.154.81.211.034.378-.122,0-.377-.188-.122-.499-.299-.71-.488-.222-.189-.333-.399-.333-.399Z" fill="#263238"/>
|
||||
<path d="M240.087,299.332c0,0-3.752-.5-6.971,1.841c0,0-.688-1.864-4.118-14.012-1.265-4.493-2.098-8.298-2.852-11.172-.844-3.206-1.144-4.482-1.321-6.756-.6-7.699-.788-30.165-1.11-33.06c0,0,15.984-4.882,15.995-4.849.754,3.473,1.354,7.134-.023,13.835-1.332,9.385-2.508,19.659-2.941,24.718c0,0,1.443,2.984,2.142,7.788.433,2.973,1.199,21.667,1.199,21.667Z" fill="#096dd9"/>
|
||||
<path d="M205.645,307.03c-.299-.055-.144,1.01-.066,1.398.078.389.666,1.398,2.819,2.031c2.154.643,4.129.244,5.506.565c1.265.289,3.685,1.831,4.961,2.153c1.676.432,3.652.099,4.585-.411.355-.2,1.098-.721,1.121-1.076.022-.422.055-.888-.045-.888l-18.881-3.772Z" fill="#37474f"/>
|
||||
<path d="M224.525,310.803c-.444.654-1.365,1.153-2.12,1.386-1.21.378-2.486.378-3.718.045-.722-.2-1.554-.633-2.209-.999-.777-.421-1.587-.632-2.431-.887-1.077-.322-3.152-.333-4.273-.455s-1.732-.333-2.642-.999c-.688-.51-.777-1.597-.81-2.474-.011-.188-.011-.466.066-.632.1-.2.233-.233.433-.344.444-.266.833-.621,1.299-.854.633-.322,3.13-1.087,4.151-.732l.544.122c.422.088,2.065.266,2.497.277.433.011.888-.078,1.233-.333.288-.222.499-.566.821-.732.355-.178.788-.122,1.177-.011.721.188,1.309.299,2.031.033.777-.288,1.465-.987,2.231-1.309.211-.089.51-.1.744.144.199.211.199.588.177.877-.066,1.009.156,2.008.267,3.017.166,1.376.987,2.874.532,4.86Z" fill="#bae7ff"/>
|
||||
<path d="M213.704,303.88c-.455-.056-1.021.299-1.277.677-.155.233-.255.399-.355.665-.111.322-.189.666-.111.999.022.088.111.144.189.133.077-.011.155-.089.166-.167.089-.466.178-.887.422-1.287.111-.177.189-.332.355-.466.156-.122.344-.221.544-.277.056-.011.144-.044.255-.033.045,0,.134.011.134.011c0-.1-.078-.155-.111-.166-.067-.067-.111-.078-.211-.089Z" fill="#263238"/>
|
||||
<path d="M215.103,304.08c-.233-.045-.488,0-.699.133-.244.155-.344.388-.466.643-.145.3-.256.61-.289.943-.022.167-.078.832.244.755.078-.023.133-.089.167-.156.033-.077.044-.155.055-.233.045-.344.122-.676.255-.998.145-.366.422-.832.844-.899.044-.011.1-.011.144-.011.011-.078-.077-.122-.144-.144-.044-.011-.078-.022-.111-.033Z" fill="#263238"/>
|
||||
<path d="M216.324,303.758c-.177.044-.344.122-.466.255-.111.122-.244.344-.3.511-.133.355-.222.621-.244,1.042-.022.267,0,.544.122.788.045.1.145.2.245.155.111-.044.111-.21.133-.299.033-.178.033-.366.055-.544.067-.665.344-1.287.777-1.786.022-.022.056-.078.056-.078-.011-.044-.1-.066-.145-.066-.077-.011-.155,0-.233.022Z" fill="#263238"/>
|
||||
<path d="M212.271,303.847c-.51-.089-1.72.443-2.508,1.941s-.888,3.972-.888,3.972-1.343-.078-2.609-1.509c-.333-.377-.566-.843-.655-1.342.034-1.065.4-1.809.911-2.252.899-.777,1.809-1.276,4.129-1.232c2.086.033,2.153.544,2.153.544l-.533-.122Z" fill="#91d5ff"/>
|
||||
<path d="M222.816,296.402c0,0,.266,4.726.455,5.991.1.71-.411,1.454-2.531,2.086-1.487.444-3.063-.011-3.085-.422l-1.01-4.493l6.171-3.162Z" fill="#ffa8a7"/>
|
||||
<path d="M240,232.378l-25.707,2.663c0,0-5.594,33.637-5.728,37.232-.1,2.607,7.781,27.89,7.781,27.89s4.884.666,6.927-1.753c-.222-3.394-.589-12.004-.822-15.099-.388-4.981-.766-6.9-2.175-11.826c0,0,6.893-17.165,9.224-24.986c11.566-4.371,10.5-14.121,10.5-14.121Z" fill="#0050b3"/>
|
||||
<path d="M240,232.378l-25.707,2.663c0,0-5.594,33.637-5.728,37.232-.1,2.607,7.781,27.89,7.781,27.89s4.884.666,6.927-1.753c-.222-3.394-.589-12.004-.822-15.099-.388-4.981-.766-6.9-2.175-11.826c0,0,5.994-17.418,8.325-25.239C240.167,241.875,240,232.378,240,232.378Z" fill="#096dd9"/>
|
||||
<g transform="translate(2e-6)">
|
||||
<path d="m221.23 191.57 1.065 9.175 9.835-1.431-1.044-9.896-9.856 2.152z" fill="#ffa8a7"/>
|
||||
<path d="m214.95 179.68s-2.264 4.36-2.009 4.682 1.909 0.843 1.909 0.843l0.1-5.525z" fill="#f28f8f"/>
|
||||
<path d="M215.979,182.455c-1.465,0-2.653-1.154-2.653-2.574s1.188-2.574,2.653-2.574s2.653,1.154,2.653,2.574-1.188,2.574-2.653,2.574Zm0-4.582c-1.143,0-2.065.899-2.065,2.008s.922,2.008,2.065,2.008s2.065-.899,2.065-2.008-.933-2.008-2.065-2.008Z" fill="#263238"/>
|
||||
<path d="M221.129,171.261c-4.517.921-5.816,2.851-6.238,9.807-.444,7.267.111,8.82,1.088,9.796.666.666,3.563.977,5.306.899c6.316-.299,8.724-2.319,11.2-5.98c2.897-4.304,3.662-10.129.555-12.514-4.385-3.361-10.079-2.385-11.911-2.008Z" fill="#ffa8a7"/>
|
||||
<path d="M215.979,190.864c.666.666,3.563.977,5.305.899l-.133-1.021c0,0-2.719.322-5.172.122Z" fill="#f28f8f"/>
|
||||
<path d="M213.57,179.337l8.025,1.01-.321.544-7.704-.966v-.588Z" fill="#263238"/>
|
||||
<path d="M214.979,173.48c.211.133.433.233.678.299-.189.855.088,1.809.71,2.43s1.565.899,2.431.71c-.189.499-.133,1.076.133,1.531s.755.777,1.276.854c-.144-.022-.022,1.221.012,1.331.133.5.321.722.788.921c0,0,.71-2.684,3.052-2.352c2.353.344,2.664,3.24,1.188,5.004-1.477,1.753-2.842,1.098-2.864,1.087.133.067.189.921.233,1.087.144.499.344,1.454.6,1.92.51.943.643,1.653,2.375,2.418.843.377,4.728-.066,5.539-1.009c1.376-3.462,3.318-4.749,4.861-7.822.7-1.398,1.499-3.029,1.577-4.615.066-1.376.099-2.851-1.732-4.227.533-.621.61-1.586.178-2.296-.422-.699-1.321-1.088-2.12-.91.233-1.243-.4-2.596-1.488-3.229-.688-.399-1.532-.499-2.298-.31-.222.055-1.942.754-1.886,1.043-.234-1.276-1.421-2.297-2.72-2.352-1.299-.045-2.553.898-2.875,2.152-.644-.987-1.687-1.72-2.841-1.986-.999-.233-2.131-.1-2.875.599s-.855,2.23-.311,3.251c-.777-.067-1.532.144-2.076.81-.466.577-.71,1.264-.599,2.008.1.787.4,1.242,1.054,1.653Z" fill="#263238"/>
|
||||
<path d="M220.952,183.098l.078-3.039c-.866-.023-1.588.643-1.61,1.475-.011.843.666,1.542,1.532,1.564Z" fill="#263238"/>
|
||||
</g>
|
||||
<path d="M234.947,194.547c2.742-.288,5.239.244,6.705,2.896c1.465,2.64,3.307,12.27,4.029,16.63.677,4.105.988,4.859-1.232,6.301-1.465.966-7.57,4.127-7.57,4.127l-1.932-29.954Z" fill="#ffa8a7"/>
|
||||
<path d="M233.783,194.37c2.997-.344,5.328-.222,6.46.81c1.132,1.031,2.331,2.085,3.142,6.079.81,3.994,1.875,9.697,1.875,9.697s-2.22,4.67-10.311,4.016l-1.166-20.602Z" fill="#91d5ff"/>
|
||||
<path d="M233.783,194.37c2.997-.344,5.328-.222,6.46.81c1.132,1.031,2.331,2.085,3.142,6.079.81,3.994,1.875,9.697,1.875,9.697s-2.22,4.67-10.311,4.016l-1.166-20.602Z" fill="#e6f7ff" opacity=".3"/>
|
||||
<path d="M239.776,199.007c-.455-2.474-3.707-5.081-6.759-4.582c0,0-.722.056-1.388.178-.344,1.486-6.871,3.162-9.845,1.808-1.177.311-4.218.899-5.195,1.22-2.797.932-3.952,3.684-4.396,12.814c0,0,1.732,20.868,1.743,26.715c4.729,1.353,10.778,1.586,16.583.288c8.347-1.863,9.479-5.07,9.479-5.07l-.799-9.585c-.011-.011,1.599-18.183.577-23.786Z" fill="#f5f5f5"/>
|
||||
<g transform="translate(1e-6 5e-6)">
|
||||
<g transform="translate(1e-6)">
|
||||
<path d="M177.906,186.382l1.998,11.727c.045.233.233.432.555.554.666.244,1.643.078,2.176-.377.266-.222.388-.477.344-.71l-1.998-11.726-3.075.532Z" fill="#455a64"/>
|
||||
<path d="M181.514,188.878l-2.786,2.297-.822-4.793l3.086-.521.522,3.017Z" opacity=".15"/>
|
||||
<path d="M180.936,176.187l.41,2.385l1.865-1.542.844,4.903c.255,1.476-.478,3.451-1.632,4.405l-3.274,2.718c-.544.444-1.088.599-1.532.466-.045-.011-1.998-.721-2.42-.899-.433-.189-.755-.643-.877-1.353l-.843-4.904l7.459-6.179Z" fill="#37474f"/>
|
||||
<path d="M179.161,189.056l3.274-2.718c1.155-.954,1.887-2.929,1.632-4.416l-.844-4.903-7.448,6.179.844,4.904c.244,1.497,1.387,1.919,2.542.954Z" fill="#455a64"/>
|
||||
<path d="m175.76 183.21 7.448-6.179-1.221-7.156-7.459 6.179 1.232 7.156z" fill="#f0f0f0"/>
|
||||
<path d="m175.76 183.21-1.232-7.155-2.264-0.844 1.221 7.156 2.275 0.843z" fill="#ebebeb"/>
|
||||
<path d="M176.131,185.339l7.448-6.179.166.976-7.437,6.224-.177-1.021Z" fill="#fafafa"/>
|
||||
<path d="M176.131,185.339l-2.276-.843.178,1.032l2.276.832-.178-1.021Z" fill="#e6e6e6"/>
|
||||
<path d="m172.65 177.4s0.987 1.586 2.375 1.442l-0.477-2.784-2.265-0.844 0.367 2.186z" fill="#91d5ff"/>
|
||||
<path d="m172.65 177.4s0.987 1.586 2.375 1.442l-0.477-2.784-2.265-0.844 0.367 2.186z" fill="#096dd9" opacity=".5"/>
|
||||
<path d="M175.01,178.849c0,0,1.587.41,2.753-.81.865-.899.754-3.528,2.641-3.894c1.332-.266,2.476.987,2.476.987l-.899-5.247-7.46,6.179.489,2.785Z" fill="#1890ff"/>
|
||||
<path d="m181.99 169.87-2.275-0.843-7.448 6.179 2.264 0.843 7.459-6.179z" fill="#91d5ff"/>
|
||||
<path d="m181.99 169.87-2.275-0.843-7.448 6.179 2.264 0.843 7.459-6.179z" fill="#69c0ff" opacity=".7"/>
|
||||
</g>
|
||||
<g transform="translate(0 1e-6)">
|
||||
<path d="M181.957,191.497c1.121.066,2.12,1.087,2.153,1.863.034.777-1.631,1.332-1.631,1.332l-.522-3.195Z" fill="#f28f8f"/>
|
||||
<path d="M216.81,197.565c-1.887.4-2.109-.144-7.17,2.918-4.706,2.851-12.898,7.555-15.962,9.308-3.274-2.995-6.77-7.633-8.047-9.874-.344-.599-.477-1.52-.411-2.208.178-1.919-.244-2.096-.31-3.827-.045-1.342.333-2.075.022-2.208-.4-.166-1.321-.133-1.865.921-.277.532-.366,1.021-.588,2.097c0,0-2.853,2.54-3.153-.011l-.555-3.218c-.865,0-2.475.278-2.886,2.219-.355,1.687-.588,5.37.666,7.733.6,1.131,3.142,2.086,4.54,3.006c1.565,1.032,6.327,11.228,9.246,13.191c1.799,1.176,2.964.966,5.284.178c5.206-1.764,17.926-8.121,17.926-8.121s5.317-3.162,3.263-12.104Z" fill="#ffa8a7"/>
|
||||
<path d="m217.29 197.44c-4.118-0.089-6.438 2.086-10.945 4.693-4.795 2.762-6.482 3.739-6.482 3.739s-1.265 6.257 3.53 9.108l10.855-5.048c3.042-1.964 5.184-7.422 3.042-12.492z" fill="#91d5ff"/>
|
||||
<path d="m217.29 197.44c-4.118-0.089-6.438 2.086-10.945 4.693-4.795 2.762-6.482 3.739-6.482 3.739s-1.265 6.257 3.53 9.108l10.855-5.048c3.042-1.964 5.184-7.422 3.042-12.492z" fill="#e6f7ff" opacity=".3"/>
|
||||
</g>
|
||||
</g>
|
||||
<path d="M199.351,252.781l-30.736,83.583-.011.022-.066.189c-.089.178-.256.333-.5.488-.777.444-2.031.444-2.808,0-.333-.2-.522-.444-.577-.688-.011-.077-.011-.155,0-.233.011-.199.044-.388.122-.577l30.846-84.149c.378-1.031,1.51-1.553,2.553-1.176c1.033.366,1.554,1.509,1.177,2.541Z" fill="#d9d9d9"/>
|
||||
<path d="M198.662,237.515c-.91.533-1.543,1.942-1.321,2.963l27.317,127.704c.022.1.044.211.044.311.012.067.012.144,0,.222-.044.244-.233.499-.577.699-.777.444-2.031.444-2.808,0-.289-.167-.466-.366-.544-.577c0-.011,0-.011-.011-.022l-.033-.167L193.445,241.31c-.577-2.685.833-5.869,3.219-7.234" fill="#d9d9d9"/>
|
||||
<path d="M157.75,345.983h-28.859l1.298,20.502c-.022,1.908,1.221,3.816,3.719,5.303c4.995,2.962,13.153,3.051,18.226.2c2.553-1.443,3.829-3.351,3.829-5.27l1.787-20.735Z" fill="#bae7ff"/>
|
||||
<path d="M133.163,339.604c-5.672,3.195-5.727,8.465-.122,11.782c5.606,3.317,14.741,3.417,20.424.222c5.672-3.196,5.727-8.465.122-11.782-5.617-3.318-14.752-3.417-20.424-.222Z" fill="#455a64"/>
|
||||
<path d="M156.495,345.739c0,.377-.078.754-.2,1.131-.444,1.354-1.621,2.608-3.452,3.639-2.564,1.432-5.972,2.197-9.612,2.164-3.63-.045-7.027-.877-9.557-2.374-1.776-1.054-2.942-2.319-3.341-3.639-.145-.389-.2-.799-.2-1.21.033-1.764,1.321-3.45,3.663-4.759c2.542-1.442,5.971-2.219,9.601-2.163c3.641.033,7.037.876,9.568,2.374c2.287,1.375,3.552,3.084,3.53,4.837Z" fill="#e6f7ff"/>
|
||||
<path d="M143.375,348.024c-3.629-.033-7.048.743-9.601,2.186-.045.022-.089.055-.133.077.011.011.022.011.033.022c2.531,1.487,5.927,2.319,9.557,2.375c3.641.033,7.048-.744,9.612-2.164.045-.022.089-.055.133-.089-.011-.011-.022-.011-.033-.022-2.531-1.486-5.916-2.33-9.568-2.385Z" fill="#1890ff"/>
|
||||
<path d="M122.875,366.524c-3.629-.033-1.322,1.034-3.875,2.476c6.226,1.291,0-1.5,0-1.5c1.259-.629-4-1.264-2,1.21s2.101,2.418,5.731,2.474c3.641.033.705.236,3.269-1.184v-1.29-.71c-2.531-1.487.527-1.42-3.125-1.476Z" fill="#1890ff"/>
|
||||
<path d="M144.308,352.662c-.977-.211-1.876-.544-2.631-.988-.944-.566-1.565-1.242-1.787-1.941-.078-.211-.111-.433-.111-.644c0-.288.078-.566.211-.843-2.331.322-4.485.976-6.216,1.964-.045.022-.089.055-.133.077.011.012.022.012.033.023c2.531,1.486,5.927,2.318,9.557,2.374.355,0,.721-.011,1.077-.022Z" opacity=".15"/>
|
||||
<path d="M148.792,353.693c.566-.322,1.021-.055,1.01.599-.011.644-.466,1.431-1.032,1.753s-1.021.056-1.01-.599.466-1.431,1.032-1.753Z" fill="#263238"/>
|
||||
<path d="M129.223,351.718c-1.244,3.184-2.209,7.522.577,9.586.155.111.3.21.444.31l-.011,1.343c-.367-.222-.744-.477-1.11-.744-4.14-3.062-1.61-9.774-.022-13.035l.122,2.54Z" fill="#455a64"/>
|
||||
<path d="M149.269,355.135c-.155.322-4.14,7.933-10.678,9.464-.755.177-1.498.266-2.253.266-2.031,0-4.063-.633-6.094-1.908l.011-1.343c2.709,1.875,5.428,2.519,8.092,1.908c6.038-1.42,9.912-8.831,9.934-8.897.145-.277.489-.377.755-.233.278.122.389.455.233.743Z" fill="#455a64"/>
|
||||
<path d="M142.5,357.5c.931-1.392.206-4.897-1.5-5-1.752-.105-3.064,3.562-2.103,5c.846,1.265,2.758,1.265,3.603,0Z" fill="#1890ff"/>
|
||||
<path d="M141.42,357.616c.93-1.392,1.375-4.423-.5-5-1.5-.461-2.984,3.446-2.023,4.884.845,1.265,1.677,1.381,2.523.116Z" fill="#147bd9"/>
|
||||
<path d="M292.228,454.049l-44.963-26.25c-1.02-.6-1.02-1.562,0-2.163L354.983,362.95c1.02-.601,2.689-.601,3.709,0l45.043,26.21c1.02.6,1.02,1.562,0,2.163L295.924,454.049c-1.007.601-2.663.601-3.696,0Z" fill="#fafafa"/>
|
||||
<path d="M350.303,376.467l-5.134,2.965c-.519.292-1.344.292-1.862,0l-1.636-.944c-.519-.293-.838-1.011-.705-1.596l17.038-80.613c.119-.585.638-1.29,1.157-1.596l5.134-2.964c.519-.293,1.343-.293,1.862,0l1.636.944c.519.292.838,1.01.705,1.595L351.46,374.872c-.133.585-.652,1.303-1.157,1.595Z" fill="#f0f0f0"/>
|
||||
<path d="M350.303,376.467l-5.134,2.965c-.519.292-.838.066-.705-.519L361.502,298.3c.12-.585.638-1.29,1.157-1.596l5.134-2.964c.519-.293.838-.067.705.518L351.46,374.872c-.133.585-.652,1.303-1.157,1.595Z" fill="#d9d9d9"/>
|
||||
<path d="m347.57 371.2 1.517-7.192 3.498-2.007 32.373 18.678 1.517 8.947-3.485 2.007-35.42-20.433z" fill="#d9d9d9"/>
|
||||
<path d="m382.99 391.64-35.42-20.433 1.517-7.192 32.373 18.678 1.53 8.947z" fill="#f0f0f0"/>
|
||||
<path d="M388.756,398.655l-5.134,2.965c-.519.292-1.344.292-1.862,0l-1.636-.944c-.519-.293-1.011-1.011-1.117-1.596L361.889,298.313c-.093-.585.239-1.302.758-1.595l5.134-2.964c.519-.293,1.343-.293,1.862,0l1.636.943c.519.293,1.011,1.011,1.117,1.596L389.514,397.06c.093.585-.239,1.303-.758,1.595Z" fill="#37474f"/>
|
||||
<path d="M361.902,298.3l.652,3.815l16.479,96.965c.107.585.599,1.303,1.104,1.596l1.636.944c.519.292,1.357.292,1.862,0l5.121-2.965c.519-.306.851-1.01.758-1.595L372.449,296.638l-.066-.345c-.106-.585-.599-1.303-1.104-1.596l-1.636-.943c-.519-.293-1.357-.293-1.862,0l-5.134,2.964c-.505.293-.851.997-.745,1.582Z" fill="#d9d9d9"/>
|
||||
<path d="M372.384,296.292L389.502,397.06c.093.584-.239,1.302-.758,1.595l-5.134,2.964c-.519.293-1.011.067-1.117-.518L365.375,300.32c-.093-.585.239-1.302.758-1.595l5.134-2.964c.519-.293,1.024-.054,1.117.531Z" fill="#f0f0f0"/>
|
||||
<path d="M269.867,422.876l-5.134,2.964c-.519.293-1.343.293-1.862,0l-1.636-.944c-.519-.292-.838-1.01-.705-1.595l17.038-80.614c.12-.585.639-1.289,1.157-1.595l5.134-2.964c.519-.293,1.344-.293,1.862,0l1.636.943c.519.293.838,1.011.705,1.596L271.024,421.28c-.119.585-.638,1.303-1.157,1.596Z" fill="#f0f0f0"/>
|
||||
<path d="M269.867,422.876l-5.134,2.964c-.519.293-.838.067-.705-.518l17.038-80.614c.12-.585.639-1.29,1.157-1.595l5.134-2.965c.519-.292.838-.066.705.519L271.024,421.28c-.119.585-.638,1.303-1.157,1.596Z" fill="#d9d9d9"/>
|
||||
<path d="m267.05 417.73 1.516-7.192 3.498-2.008 32.374 18.678 1.516 8.947-3.485 2.021-35.419-20.446z" fill="#d9d9d9"/>
|
||||
<path d="m302.47 438.18-35.419-20.445 1.516-7.192 32.387 18.691 1.516 8.946z" fill="#f0f0f0"/>
|
||||
<path d="M308.33,445.077l-5.134,2.964c-.519.293-1.343.293-1.862,0l-1.636-.943c-.519-.293-1.011-1.011-1.117-1.596L281.463,344.735c-.093-.585.239-1.303.758-1.595l5.134-2.965c.519-.292,1.343-.292,1.862,0l1.636.944c.519.293,1.011,1.011,1.117,1.596l17.118,100.767c.093.571-.239,1.289-.758,1.595Z" fill="#37474f"/>
|
||||
<path d="M281.476,344.722l.652,3.815l16.479,96.965c.107.585.599,1.303,1.104,1.595l1.636.944c.519.293,1.357.293,1.862,0l5.121-2.964c.519-.306.851-1.011.758-1.596L292.024,343.047l-.067-.346c-.106-.585-.598-1.303-1.104-1.595l-1.636-.944c-.518-.293-1.356-.293-1.862,0l-5.134,2.964c-.505.293-.851,1.011-.745,1.596Z" fill="#d9d9d9"/>
|
||||
<path d="M291.959,342.701l17.117,100.767c.094.585-.239,1.303-.758,1.595l-5.134,2.965c-.518.292-1.011.066-1.117-.519L284.949,346.742c-.093-.585.24-1.303.758-1.595l5.134-2.965c.519-.306,1.024-.066,1.118.519Z" fill="#f0f0f0"/>
|
||||
<path d="m277.79 357.78 108.39-62.535-1.769-1.023-108.37 62.547 1.756 1.011z" fill="#ff7875"/>
|
||||
<path d="m277.79 357.78 108.39-62.535-1.769-1.023-108.37 62.547 1.756 1.011z" fill="#fff" opacity=".4"/>
|
||||
<path d="m277.79 357.77 5.267 31.28 108.39-62.534-5.267-31.28-108.39 62.534z" fill="#ff7875"/>
|
||||
<path d="m276.03 356.76 5.254 31.281 1.769 1.01-5.267-31.28-1.756-1.011z" fill="#d9d9d9"/>
|
||||
<path d="m277.12 363.28 2.913 17.309 1.756 1.01-2.9-17.295-1.769-1.024z" fill="#cf1322"/>
|
||||
<path d="m277.12 363.28 2.913 17.309 1.756 1.01-2.9-17.295" opacity=".2"/>
|
||||
<path d="m287.74 352.04-4.682 37.01-5.267-31.28 9.949-5.73z" fill="#ebebeb"/>
|
||||
<path d="m381.49 332.25 4.682-37.01 5.267 31.281-9.949 5.729z" fill="#ebebeb"/>
|
||||
<path d="m301.25 378.55 4.682-37.01-10.76 6.195-4.682 37.024 10.76-6.209z" fill="#ebebeb"/>
|
||||
<path d="m319.45 368.05 4.681-37.01-10.76 6.194-4.681 37.024 10.76-6.208z" fill="#ebebeb"/>
|
||||
<path d="m337.66 357.55 4.681-37.01-10.76 6.208-4.682 37.01 10.761-6.208z" fill="#ebebeb"/>
|
||||
<path d="m355.85 347.06 4.682-37.023-10.76 6.208-4.682 37.01 10.76-6.195z" fill="#ebebeb"/>
|
||||
<path d="m374.06 336.56 4.682-37.024-10.76 6.208-4.682 37.01 10.76-6.194z" fill="#ebebeb"/>
|
||||
<path d="m295.17 347.74-1.769-1.01 10.774-6.208 1.755 1.023-10.76 6.195z" fill="#fafafa"/>
|
||||
<path d="m313.37 337.24-1.756-1.01 10.76-6.208 1.756 1.023-10.76 6.195z" fill="#fafafa"/>
|
||||
<path d="m331.58 326.75-1.769-1.023 10.773-6.208 1.756 1.023-10.76 6.208z" fill="#fafafa"/>
|
||||
<path d="m349.77 316.24-1.755-1.024 10.76-6.208 1.755 1.024-10.76 6.208z" fill="#fafafa"/>
|
||||
<path d="m367.98 305.74-1.769-1.024 10.773-6.208 1.756 1.024-10.76 6.208z" fill="#fafafa"/>
|
||||
<path d="m277.79 357.77-1.756-1.01 9.936-5.743 1.769 1.024-9.949 5.729z" fill="#fafafa"/>
|
||||
<path d="m278.9 364.3 2.899 17.296 108.39-62.548-2.899-17.295-108.39 62.547z" fill="#cf1322"/>
|
||||
<path d="M284.171,375.797c-.626-.307-1.049-1.003-1.254-2.09l-1.419-7.497c-.063-.345.143-.641.603-.916l.488-.276c.46-.262.729-.227.792.117l1.387,7.339c.079.437.241.74.484.908s.511.162.832-.017c.306-.179.527-.449.66-.837.133-.389.167-.802.074-1.225l-1.387-7.339c-.063-.344.128-.641.603-.916l.474-.262c.46-.262.729-.227.792.117l1.419,7.498c.205,1.086.111,2.072-.28,2.97-.391.899-1.054,1.604-1.946,2.113-.935.511-1.695.619-2.322.313Z" fill="#fff"/>
|
||||
<path d="M290.483,372.52l-1.955-10.319c-.063-.344.129-.641.603-.916l.307-.179c.153-.096.293-.139.406-.128.099-.002.213.076.315.22l3.155,4.019c.159.223.378.565.655,1.024l.041-.054c-.165-.515-.286-.925-.335-1.216l-.976-5.14c-.064-.344.128-.641.602-.916l.293-.165c.46-.262.729-.228.792.117l1.955,10.319c.063.344-.143.641-.603.916l-.237.138c-.153.096-.293.139-.406.141s-.227-.075-.315-.22l-3.341-4.175c-.101-.118-.276-.407-.525-.841l-.054.081c.164.462.255.806.302,1.045l1.009,5.298c.063.345-.143.655-.603.916l-.293.166c-.46.248-.729.214-.792-.131Z" fill="#fff"/>
|
||||
<path d="M297.826,368.704c-.086-.065-.16-.21-.206-.435l-1.892-10.001c-.047-.225-.036-.411.017-.559.054-.147.15-.255.303-.351l2.288-1.309c1.046-.593,1.892-.57,2.569.095.676.651,1.181,1.931,1.542,3.812.362,1.894.379,3.45.065,4.666s-1.002,2.121-2.034,2.713l-2.287,1.308c-.153.11-.28.112-.365.061Zm2.188-3.421c.419-.248.678-.678.764-1.318s.011-1.596-.241-2.894c-.237-1.285-.508-2.144-.799-2.577-.29-.433-.645-.519-1.063-.271l-.767.441l1.339,7.06.767-.441Z" fill="#fff"/>
|
||||
<path d="M304.758,364.736c-.086-.064-.159-.209-.206-.434l-1.892-10.001c-.046-.226-.036-.412.017-.559.054-.148.151-.256.304-.352l3.556-2.025c.126-.069.225-.058.297.047.087.091.147.29.195.568l.095.503c.048.279.066.504.027.665-.025.16-.107.268-.233.337l-2.12,1.212.441,2.331l1.883-1.074c.112-.069.225-.058.297.047.073.092.147.289.195.568l.095.503c.048.278.066.504.027.664-.039.161-.107.282-.233.351l-1.883,1.074.49,2.61l2.134-1.213c.125-.068.224-.057.297.048.072.091.147.289.194.568l.095.503c.048.278.066.504.041.664s-.093.268-.219.337l-3.556,2.025c-.111.082-.238.098-.338.033Z" fill="#fff"/>
|
||||
<path d="M310.048,361.36l-1.923-10.16c-.047-.225-.036-.411.017-.558.054-.148.164-.27.331-.366l2.009-1.144c.739-.427,1.372-.519,1.913-.277.54.229.921.926,1.141,2.052.236,1.258.033,2.379-.624,3.376l.015.066c.198.023.398.152.6.374s.376.471.509.774l1.295,2.687.074.171c.062.292-.172.575-.66.864l-.419.235c-.418.247-.7.24-.804-.024l-1.225-2.702c-.116-.197-.232-.314-.345-.352-.114-.037-.268-.008-.464.103l-.292.165.693,3.643c.063.344-.143.654-.631.93l-.404.234c-.46.275-.729.254-.806-.091Zm1.403-7.447c.182-.097.305-.298.383-.606.079-.307.071-.679-.008-1.143-.08-.451-.212-.741-.37-.884s-.342-.152-.551-.042l-.642.359.505,2.689.683-.373Z" fill="#fff"/>
|
||||
<path d="M318.291,355.785c-.69-.638-1.224-1.957-1.603-3.971-.363-1.934-.366-3.529-.024-4.772.356-1.257,1.099-2.19,2.214-2.837.377-.22.741-.36,1.078-.42.267-.045.44.111.503.482.079.41.101.809.052,1.182-.049.387-.157.588-.34.605-.282.046-.548.131-.799.282-.474.275-.788.774-.901,1.494-.126.734-.079,1.69.156,2.896.236,1.205.52,2.037.868,2.495.348.459.787.543,1.302.24.154-.083.279-.165.376-.26s.222-.217.388-.367c.027-.027.055-.054.097-.081.167-.097.31-.033.441.19.131.224.237.554.316.991.031.159.034.319.009.452-.025.147-.093.281-.231.43-.125.136-.291.299-.513.476-.208.177-.445.328-.682.466-1.115.673-2.018.665-2.707.027Z" fill="#fff"/>
|
||||
<path d="M321.946,348.851c-.725-3.841-.042-6.368,2.064-7.567c1.032-.592,1.893-.556,2.611.068s1.251,1.93,1.629,3.917c.378,1.973.382,3.582.026,4.826s-1.043,2.148-2.075,2.74c-2.106,1.199-3.529-.129-4.255-3.984Zm4.494,1.134c.135-.322.197-.762.185-1.346-.012-.585-.111-1.341-.286-2.295-.173-.927-.357-1.642-.55-2.13s-.396-.776-.61-.892c-.214-.115-.467-.084-.76.082-.293.165-.5.409-.649.717-.134.322-.21.776-.198,1.361.011.584.097,1.341.285,2.281.174.941.357,1.642.564,2.129.193.489.397.79.624.892.214.116.482.084.761-.081.293-.153.499-.396.634-.718Z" fill="#fff"/>
|
||||
<path d="M330.301,349.802l-1.954-10.319c-.063-.345.128-.641.602-.916l.307-.179c.153-.096.293-.139.406-.128.113-.002.214.076.315.22l3.155,4.019c.16.223.378.564.641,1.024l.041-.054c-.165-.515-.286-.925-.334-1.216l-.977-5.14c-.063-.344.128-.641.602-.916l.293-.165c.461-.262.729-.228.793.117l1.954,10.319c.063.344-.143.641-.603.916l-.222.138c-.154.096-.294.138-.407.141-.113.002-.227-.076-.314-.22l-3.342-4.175c-.101-.118-.276-.407-.524-.841l-.055.081c.164.462.256.806.303,1.044l1.008,5.299c.063.344-.128.654-.602.916l-.293.165c-.461.262-.73.214-.793-.13Z" fill="#fff"/>
|
||||
<path d="M338.162,345.736c-.408.062-.733.015-.962-.153-.144-.131-.232-.288-.264-.474-.062-.304-.055-.637.008-1.024s.157-.615.311-.697c.069-.042.182-.044.338-.007.156.023.283.047.354.046.353.046.648-.013.885-.151.251-.138.459-.354.609-.61.15-.269.2-.589.121-.986-.062-.292-.151-.503-.281-.66-.13-.143-.287-.247-.443-.297-.17-.049-.397-.098-.694-.132-.693-.119-1.234-.348-1.608-.713s-.64-.971-.812-1.845c-.175-.967-.094-1.86.257-2.664.35-.818.916-1.441,1.683-1.869.28-.152.574-.264.897-.323.323-.06.591-.065.79.01.17.05.272.208.319.433.048.278.068.597.033.97-.035.359-.115.587-.254.67-.056.041-.183.056-.366.073-.352.021-.619.079-.828.203-.321.179-.528.396-.608.65-.079.255-.103.494-.056.733.079.397.211.66.411.776s.483.203.88.262l.311.047c.637.107,1.121.35,1.453.702.331.353.582.933.739,1.741.111.609.123,1.22.023,1.834-.101.627-.315,1.203-.643,1.741-.327.539-.784.973-1.37,1.304-.419.208-.811.349-1.233.41Z" fill="#fff"/>
|
||||
<path d="M343.816,342.1l-1.639-8.69-1.339.758c-.126.069-.225.058-.298-.047-.086-.091-.146-.29-.194-.568l-.095-.503c-.048-.279-.066-.504-.027-.665.039-.16.107-.281.233-.35l4.574-2.604c.126-.069.239-.058.311.034.087.091.147.289.209.567l.095.504c.048.278.066.504.027.664s-.107.281-.247.351l-1.353.771l1.64,8.69c.063.344-.128.641-.602.916l-.489.276c-.474.275-.728.24-.806-.104Z" fill="#fff"/>
|
||||
<path d="M348.156,339.621l-1.923-10.16c-.047-.226-.037-.412.017-.559.053-.148.164-.269.345-.366l2.008-1.144c.739-.427,1.386-.519,1.927-.277.541.228.921.912,1.142,2.064.236,1.259.033,2.38-.624,3.377l.015.066c.198.023.398.152.6.374s.376.484.509.787l1.295,2.687.074.171c.062.292-.172.576-.66.865l-.418.247c-.433.248-.701.24-.805-.024l-1.224-2.701c-.117-.197-.232-.315-.346-.352-.113-.038-.268-.008-.463.102l-.293.166.693,3.642c.063.345-.142.655-.63.93l-.405.234c-.488.249-.771.215-.834-.129Zm1.389-7.434c.182-.097.304-.299.383-.619.078-.308.085-.68-.009-1.13-.093-.451-.212-.754-.37-.884-.158-.143-.356-.152-.551-.042l-.642.358.519,2.689.67-.372Z" fill="#fff"/>
|
||||
<path d="M355.007,335.403c-.627-.307-1.05-1.003-1.255-2.089l-1.418-7.498c-.063-.344.142-.654.602-.916l.488-.276c.461-.262.729-.227.793.117l1.387,7.339c.079.437.24.74.484.921.229.169.511.163.818-.016.306-.179.527-.449.66-.838.133-.388.168-.787.074-1.224l-1.387-7.339c-.063-.344.142-.641.602-.916l.461-.262c.46-.262.729-.227.792.117l1.419,7.498c.205,1.086.111,2.072-.28,2.971-.391.885-1.054,1.603-1.947,2.113-.893.496-1.666.604-2.293.298Z" fill="#fff"/>
|
||||
<path d="M361.763,331c-.689-.638-1.223-1.957-1.602-3.971-.363-1.934-.366-3.529-.024-4.773.355-1.257,1.099-2.189,2.214-2.836.377-.22.74-.36,1.078-.42.267-.045.439.111.503.482.079.41.101.809.052,1.182-.049.387-.158.588-.341.605-.281.046-.547.131-.798.282-.474.275-.789.774-.901,1.494-.127.734-.08,1.69.156,2.895.236,1.206.52,2.038.868,2.496.347.459.786.543,1.302.24.154-.083.279-.165.376-.26s.221-.217.387-.367c.028-.027.056-.054.098-.081.167-.097.309-.033.441.19.131.224.236.554.316.991.031.159.034.319.009.452-.026.147-.094.281-.232.43-.124.136-.29.298-.512.476-.222.177-.445.328-.682.465-1.129.674-2.032.666-2.708.028Z" fill="#fff"/>
|
||||
<path d="M367.999,328.31l-1.654-8.69-1.339.758c-.126.069-.225.058-.298-.047-.086-.091-.146-.29-.194-.568l-.095-.503c-.048-.279-.066-.504-.027-.665.025-.16.107-.281.233-.35l4.574-2.604c.126-.069.239-.058.311.034.087.091.147.289.195.568l.095.503c.047.278.066.504.027.664-.025.16-.107.282-.247.351l-1.353.771l1.654,8.69c.063.344-.142.641-.602.916l-.503.276c-.446.275-.714.24-.777-.104Z" fill="#fff"/>
|
||||
<path d="M372.309,325.843l-1.954-10.319c-.064-.358.127-.668.602-.93l.502-.289c.46-.262.715-.213.793.144l1.954,10.319c.063.358-.128.667-.588.929l-.502.289c-.475.263-.729.214-.807-.143Z" fill="#fff"/>
|
||||
<path d="M374.315,318.982c-.725-3.842-.042-6.368,2.064-7.567c1.018-.579,1.893-.556,2.611.068.704.624,1.252,1.929,1.63,3.916.377,1.974.381,3.583.025,4.826-.356,1.244-1.043,2.149-2.075,2.741-2.106,1.199-3.515-.13-4.255-3.984Zm4.508,1.133c.135-.321.197-.761.185-1.346s-.111-1.341-.285-2.295c-.174-.927-.357-1.641-.551-2.13-.193-.488-.396-.776-.61-.892-.214-.115-.467-.083-.76.082s-.499.409-.648.718c-.135.321-.211.775-.199,1.36s.097,1.341.271,2.281c.174.941.357,1.642.564,2.13.193.488.397.79.625.892.228.115.481.083.76-.082.293-.152.5-.396.648-.718Z" fill="#fff"/>
|
||||
<path d="M382.672,319.933l-1.955-10.319c-.063-.344.143-.641.603-.916l.307-.179c.153-.096.293-.139.406-.128.113-.002.213.076.315.22l3.155,4.019c.159.223.378.565.641,1.025l.041-.054c-.166-.529-.286-.925-.335-1.217l-.976-5.139c-.064-.345.128-.641.602-.917l.293-.165c.46-.262.729-.227.792.117l1.955,10.319c.063.345-.143.655-.603.917l-.223.124c-.153.096-.294.138-.42.141-.113.002-.214-.076-.315-.22l-3.341-4.175c-.102-.118-.277-.407-.525-.854l-.055.081c.164.462.256.806.303,1.044l1.008,5.299c.063.344-.142.641-.602.916l-.293.166c-.446.288-.701.24-.778-.105Z" fill="#fff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 78 KiB |
BIN
core/src/assets/images/users/avatar-1.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
core/src/assets/images/users/avatar-2.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
core/src/assets/images/users/avatar-3.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
core/src/assets/images/users/avatar-4.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
core/src/assets/images/users/avatar-5.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
core/src/assets/images/users/avatar-group.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
16
core/src/components/shared/BaseBreadcrumb.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<v-breadcrumbs :items="breadcrumbs">
|
||||
<template v-slot:title>
|
||||
{{ title }}
|
||||
</template>
|
||||
</v-breadcrumbs>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
title: string;
|
||||
breadcrumbs: any[];
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
16
core/src/components/shared/UiParentCard.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>{{ title }}</v-card-title>
|
||||
<v-card-text>
|
||||
<slot />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
title: string;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
6
core/src/composables/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Core composables - reusable composition functions
|
||||
*/
|
||||
|
||||
export { useClipboard } from './useClipboard'
|
||||
export { useUser } from './useUser'
|
||||
45
core/src/composables/useClipboard.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
/**
|
||||
* Composable for clipboard operations with visual feedback
|
||||
*
|
||||
* @param timeout - Duration in ms to show success state (default: 2000)
|
||||
* @returns Object containing copiedKey ref and copyToClipboard function
|
||||
*
|
||||
* @example
|
||||
* ```vue
|
||||
* <script setup>
|
||||
* import { useClipboard } from '@KTXC/composables/useClipboard'
|
||||
*
|
||||
* const { copiedKey, copyToClipboard } = useClipboard()
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <v-btn
|
||||
* :icon="copiedKey === index ? 'mdi-check' : 'mdi-content-copy'"
|
||||
* :color="copiedKey === index ? 'success' : undefined"
|
||||
* @click="copyToClipboard(text, index)"
|
||||
* />
|
||||
* </template>
|
||||
* ```
|
||||
*/
|
||||
export function useClipboard<T = number>(timeout = 2000) {
|
||||
const copiedKey = ref<T | null>(null)
|
||||
|
||||
const copyToClipboard = async (text: string, key: T): Promise<boolean> => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
copiedKey.value = key
|
||||
setTimeout(() => { copiedKey.value = null }, timeout)
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('Failed to copy to clipboard:', err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
copiedKey,
|
||||
copyToClipboard
|
||||
}
|
||||
}
|
||||
38
core/src/composables/useSnackbar.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Simple snackbar/toast notification composable
|
||||
* Uses Vuetify's snackbar component
|
||||
*/
|
||||
import { ref } from 'vue'
|
||||
|
||||
export interface SnackbarOptions {
|
||||
message: string
|
||||
color?: 'success' | 'error' | 'warning' | 'info' | string
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
const snackbarVisible = ref(false)
|
||||
const snackbarMessage = ref('')
|
||||
const snackbarColor = ref<string>('info')
|
||||
const snackbarTimeout = ref(3000)
|
||||
|
||||
export function useSnackbar() {
|
||||
const showSnackbar = (options: SnackbarOptions) => {
|
||||
snackbarMessage.value = options.message
|
||||
snackbarColor.value = options.color || 'info'
|
||||
snackbarTimeout.value = options.timeout || 3000
|
||||
snackbarVisible.value = true
|
||||
}
|
||||
|
||||
const hideSnackbar = () => {
|
||||
snackbarVisible.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
snackbarVisible,
|
||||
snackbarMessage,
|
||||
snackbarColor,
|
||||
snackbarTimeout,
|
||||
showSnackbar,
|
||||
hideSnackbar,
|
||||
}
|
||||
}
|
||||
103
core/src/composables/useUser.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { computed } from 'vue';
|
||||
import { useUserStore } from '@KTXC/stores/userStore';
|
||||
|
||||
/**
|
||||
* Composable for accessing user authentication, profile, and settings
|
||||
* Provides a clean API for modules to access user data
|
||||
*/
|
||||
export function useUser() {
|
||||
const store = useUserStore();
|
||||
|
||||
// =========================================================================
|
||||
// Authentication
|
||||
// =========================================================================
|
||||
|
||||
const isAuthenticated = computed(() => store.isAuthenticated);
|
||||
const identifier = computed(() => store.identifier);
|
||||
const identity = computed(() => store.identity);
|
||||
const label = computed(() => store.label);
|
||||
const roles = computed(() => store.roles);
|
||||
const permissions = computed(() => store.permissions);
|
||||
|
||||
const hasPermission = (permission: string): boolean => {
|
||||
return store.hasPermission(permission);
|
||||
};
|
||||
|
||||
const hasAnyPermission = (perms: string[]): boolean => {
|
||||
return store.hasAnyPermission(perms);
|
||||
};
|
||||
|
||||
const hasAllPermissions = (perms: string[]): boolean => {
|
||||
return store.hasAllPermissions(perms);
|
||||
};
|
||||
|
||||
const hasRole = (role: string): boolean => {
|
||||
return store.hasRole(role);
|
||||
};
|
||||
|
||||
const logout = async (): Promise<void> => {
|
||||
await store.logout();
|
||||
};
|
||||
|
||||
// =========================================================================
|
||||
// Profile
|
||||
// =========================================================================
|
||||
|
||||
const profile = computed(() => store.profileFields);
|
||||
const editableProfile = computed(() => store.editableProfileFields);
|
||||
const managedProfile = computed(() => store.managedProfileFields);
|
||||
|
||||
const getProfile = (key: string): any => {
|
||||
return store.getProfileField(key);
|
||||
};
|
||||
|
||||
const setProfile = (key: string, value: any): boolean => {
|
||||
return store.setProfileField(key, value);
|
||||
};
|
||||
|
||||
const isProfileEditable = (key: string): boolean => {
|
||||
return store.isProfileFieldEditable(key);
|
||||
};
|
||||
|
||||
// =========================================================================
|
||||
// Settings
|
||||
// =========================================================================
|
||||
|
||||
const settings = computed(() => store.settings);
|
||||
|
||||
const getSetting = (key: string): any => {
|
||||
return store.getSetting(key);
|
||||
};
|
||||
|
||||
const setSetting = (key: string, value: any): void => {
|
||||
store.setSetting(key, value);
|
||||
};
|
||||
|
||||
return {
|
||||
// Auth
|
||||
isAuthenticated,
|
||||
identifier,
|
||||
identity,
|
||||
label,
|
||||
roles,
|
||||
permissions,
|
||||
hasPermission,
|
||||
hasAnyPermission,
|
||||
hasAllPermissions,
|
||||
hasRole,
|
||||
logout,
|
||||
|
||||
// Profile
|
||||
profile,
|
||||
editableProfile,
|
||||
managedProfile,
|
||||
getProfile,
|
||||
setProfile,
|
||||
isProfileEditable,
|
||||
|
||||
// Settings
|
||||
settings,
|
||||
getSetting,
|
||||
setSetting,
|
||||
};
|
||||
}
|
||||
15
core/src/config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export type ConfigProps = {
|
||||
Sidebar_drawer: boolean;
|
||||
mini_sidebar: boolean;
|
||||
actTheme: string;
|
||||
fontTheme: string;
|
||||
};
|
||||
|
||||
const config: ConfigProps = {
|
||||
Sidebar_drawer: true,
|
||||
mini_sidebar: false,
|
||||
actTheme: 'light',
|
||||
fontTheme: 'Public sans'
|
||||
};
|
||||
|
||||
export default config;
|
||||
9
core/src/layouts/blank/BlankLayout.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-app>
|
||||
<RouterView />
|
||||
</v-app>
|
||||
</template>
|
||||
29
core/src/layouts/footer/LayoutFooter.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { shallowRef } from 'vue';
|
||||
|
||||
const footerLink = shallowRef([
|
||||
{
|
||||
title: 'About us'
|
||||
},
|
||||
{
|
||||
title: 'Privacy'
|
||||
},
|
||||
{
|
||||
title: 'Terms'
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
<template>
|
||||
<v-footer class="px-0 footer">
|
||||
<v-row justify="center" no-gutters>
|
||||
<v-col cols="6">
|
||||
<p class="text-caption mb-0">© All rights reserved</p>
|
||||
</v-col>
|
||||
<v-col class="text-right" cols="6">
|
||||
<a v-for="(item, i) in footerLink" :key="i" class="mx-2 text-caption text-darkText" href="/">
|
||||
{{ item.title }}
|
||||
</a>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-footer>
|
||||
</template>
|
||||
105
core/src/layouts/header/LayoutHeader.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useUserStore } from '@KTXC/stores/userStore';
|
||||
import { useLayoutStore } from '@KTXC/stores/layoutStore';
|
||||
import LayoutUserMenu from '@KTXC/layouts/menus/LayoutUserMenu.vue';
|
||||
import NotificationDD from './NotificationDD.vue';
|
||||
import Searchbar from './SearchBarPanel.vue';
|
||||
import defaultAvatar from '@KTXC/assets/images/users/avatar-1.png';
|
||||
|
||||
const layoutStore = useLayoutStore();
|
||||
const userStore = useUserStore();
|
||||
const userAuth = computed(() => userStore.auth);
|
||||
const userAvatar = computed(() => userStore.getProfileField('avatar') || defaultAvatar);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-app-bar elevation="0" height="60">
|
||||
<v-btn
|
||||
class="hidden-md-and-down text-secondary mr-3"
|
||||
color="darkText"
|
||||
icon
|
||||
rounded="sm"
|
||||
variant="text"
|
||||
@click.stop="layoutStore.setMiniSidebar(!layoutStore.miniSidebar)"
|
||||
size="small"
|
||||
>
|
||||
<v-icon>mdi-menu-open</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
class="hidden-lg-and-up text-secondary ms-3"
|
||||
color="darkText"
|
||||
icon
|
||||
rounded="sm"
|
||||
variant="text"
|
||||
@click.stop="layoutStore.toggleSidebarDrawer()"
|
||||
size="small"
|
||||
>
|
||||
<v-icon>mdi-menu</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<!-- search mobile -->
|
||||
<v-menu :close-on-content-click="false" class="hidden-lg-and-up" offset="10, 0">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
class="hidden-lg-and-up text-secondary ml-1"
|
||||
color="lightsecondary"
|
||||
icon
|
||||
rounded="sm"
|
||||
variant="flat"
|
||||
size="small"
|
||||
v-bind="props"
|
||||
>
|
||||
<v-icon>mdi-magnify</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-sheet class="search-sheet v-col-12 pa-0" width="320">
|
||||
<v-text-field persistent-placeholder placeholder="Search here.." color="primary" variant="solo" hide-details>
|
||||
<template v-slot:prepend-inner>
|
||||
<v-icon>mdi-magnify</v-icon>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-sheet>
|
||||
</v-menu>
|
||||
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!-- Search part -->
|
||||
<!-- ---------------------------------------------- -->
|
||||
<v-sheet class="d-none d-lg-block" width="250">
|
||||
<Searchbar />
|
||||
</v-sheet>
|
||||
|
||||
<!---/Search part -->
|
||||
|
||||
<v-spacer />
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!---right part -->
|
||||
<!-- ---------------------------------------------- -->
|
||||
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!-- Notification -->
|
||||
<!-- ---------------------------------------------- -->
|
||||
<NotificationDD />
|
||||
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!-- User Profile -->
|
||||
<!-- ---------------------------------------------- -->
|
||||
<v-menu :close-on-content-click="false" offset="8, 0">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn class="profileBtn" variant="text" rounded="sm" v-bind="props">
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar class="mr-sm-2 mr-0" size="32">
|
||||
<v-img :src="userAvatar" :alt="userAuth?.label || 'User'" cover />
|
||||
</v-avatar>
|
||||
<h6 class="text-subtitle-1 mb-0 d-sm-block d-none">
|
||||
{{ userAuth?.label }}
|
||||
</h6>
|
||||
</div>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-sheet rounded="md" width="290">
|
||||
<LayoutUserMenu />
|
||||
</v-sheet>
|
||||
</v-menu>
|
||||
</v-app-bar>
|
||||
</template>
|
||||
119
core/src/layouts/header/NotificationDD.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const isActive = ref(true);
|
||||
|
||||
function deactivateItem() {
|
||||
isActive.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!-- notifications DD -->
|
||||
<!-- ---------------------------------------------- -->
|
||||
<v-menu :close-on-content-click="false" offset="6, 0">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn icon class="text-secondary ml-sm-2 ml-1" color="darkText" rounded="sm" size="small" v-bind="props">
|
||||
<v-badge :content="isActive ? '2' : '0'" color="primary" offset-x="-4" offset-y="-5">
|
||||
<v-icon>mdi-bell</v-icon>
|
||||
</v-badge>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-sheet rounded="md" width="387" class="notification-dropdown">
|
||||
<div class="pa-4">
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<h6 class="text-subtitle-1 mb-0">Notifications</h6>
|
||||
<v-btn
|
||||
variant="text"
|
||||
color="success"
|
||||
icon
|
||||
rounded
|
||||
size="small"
|
||||
@click="deactivateItem()"
|
||||
:class="isActive ? 'd-block' : 'd-none'"
|
||||
>
|
||||
<v-icon>mdi-check-circle</v-icon>
|
||||
<v-tooltip aria-label="tooltip" activator="parent" location="bottom" :content-class="isActive ? 'custom-tooltip' : 'd-none'">
|
||||
<span class="text-caption">Mark as all read</span>
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
<v-divider></v-divider>
|
||||
<perfect-scrollbar style="height: calc(100vh - 300px); max-height: 265px">
|
||||
<v-list class="py-0" lines="two" aria-label="notification list" aria-busy="true">
|
||||
<v-list-item value="1" color="secondary" class="no-spacer py-1" :active="isActive">
|
||||
<template v-slot:prepend>
|
||||
<v-avatar size="36" variant="flat" color="lightsuccess" class="mr-3 py-2 text-success">
|
||||
<v-icon>mdi-gift</v-icon>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<div class="d-inline-flex justify-space-between w-100">
|
||||
<h6 class="text-subtitle-1 font-weight-regular mb-0">
|
||||
It's <span style="font-weight: 600">Cristina danny's</span> birthday today.
|
||||
</h6>
|
||||
<span class="text-caption">3:00 AM</span>
|
||||
</div>
|
||||
|
||||
<p class="text-caption text-medium-emphasis my-0">2 min ago</p>
|
||||
</v-list-item>
|
||||
<v-divider></v-divider>
|
||||
<v-list-item value="2" color="secondary" class="no-spacer">
|
||||
<template v-slot:prepend>
|
||||
<v-avatar size="36" variant="flat" color="lightprimary" class="mr-3 py-2 text-primary">
|
||||
<v-icon>mdi-message</v-icon>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<div class="d-inline-flex justify-space-between w-100">
|
||||
<h6 class="text-subtitle-1 font-weight-regular mb-0"><span style="font-weight: 600">Aida Burg</span> commented your post.</h6>
|
||||
<span class="text-caption">6:00 PM</span>
|
||||
</div>
|
||||
|
||||
<p class="text-caption text-medium-emphasis my-0">5 August</p>
|
||||
</v-list-item>
|
||||
<v-divider></v-divider>
|
||||
<v-list-item value="3" color="secondary" class="no-spacer" :active="isActive">
|
||||
<template v-slot:prepend>
|
||||
<v-avatar size="36" variant="flat" color="lighterror" class="mr-3 py-2 text-error">
|
||||
<v-icon>mdi-cog</v-icon>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<div class="d-inline-flex justify-space-between w-100">
|
||||
<h6 class="text-subtitle-1 font-weight-regular mb-0">Your Profile is Complete <span style="font-weight: 600">60%</span></h6>
|
||||
<span class="text-caption">2:45 PM</span>
|
||||
</div>
|
||||
|
||||
<p class="text-caption text-medium-emphasis my-0">7 hours ago</p>
|
||||
</v-list-item>
|
||||
<v-divider></v-divider>
|
||||
<v-list-item value="4" color="secondary" class="no-spacer">
|
||||
<template v-slot:prepend>
|
||||
<v-avatar size="36" variant="flat" color="lightprimary" class="mr-3 py-2 text-primary"> C </v-avatar>
|
||||
</template>
|
||||
<div class="d-inline-flex justify-space-between w-100">
|
||||
<h6 class="text-subtitle-1 font-weight-regular mb-0">
|
||||
<span style="font-weight: 600">Cristina Danny</span> invited to join <span style="font-weight: 600">Metting.</span>
|
||||
</h6>
|
||||
<span class="text-caption">9:10 PM</span>
|
||||
</div>
|
||||
|
||||
<p class="text-caption text-medium-emphasis my-0">Daily scrum meeting time</p>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</perfect-scrollbar>
|
||||
<v-divider></v-divider>
|
||||
<div class="pa-2 text-center">
|
||||
<v-btn color="primary" variant="text">View All</v-btn>
|
||||
</div>
|
||||
</v-sheet>
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.v-tooltip {
|
||||
> .v-overlay__content.custom-tooltip {
|
||||
padding: 2px 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
13
core/src/layouts/header/SearchBarPanel.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!-- searchbar -->
|
||||
<!-- ---------------------------------------------- -->
|
||||
<v-text-field persistent-placeholder placeholder="Ctrl + k" color="primary" variant="outlined" hide-details density="compact" autocomplete="off">
|
||||
<template v-slot:prepend-inner>
|
||||
<v-icon size="small" color="lightText">mdi-magnify</v-icon>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</template>
|
||||
42
core/src/layouts/logo/LogoDark.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div class="logo">
|
||||
<RouterLink :to="{ name: 'home' }" aria-label="logo">
|
||||
<svg width="118" height="35" viewBox="0 0 118 35" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M4.63564 15.8644L6.94797 13.552L6.95038 13.5496H11.3006L9.56969 15.2806L9.12278 15.7275L7.35024 17.5L7.56977 17.7201L17.5 27.6498L27.6498 17.5L25.8766 15.7275L25.7518 15.602L23.6994 13.5496H28.0496L28.052 13.552L29.8644 15.3644L32 17.5L17.5 32L3 17.5L4.63564 15.8644ZM17.5 3L25.8784 11.3784H21.5282L17.5 7.35024L13.4718 11.3784H9.12158L17.5 3Z"
|
||||
:fill="darkprimary"
|
||||
></path>
|
||||
<path
|
||||
d="M7.35025 17.5L9.1228 15.7275L9.5697 15.2805L7.83937 13.5496H6.95039L6.94798 13.552L4.63564 15.8644L6.8551 18.073L7.35025 17.5Z"
|
||||
:fill="darkprimary"
|
||||
></path>
|
||||
<path
|
||||
d="M25.8767 15.7275L27.6498 17.5L27.4743 17.6755L27.4749 17.6761L29.8644 15.3644L28.0521 13.552L28.0497 13.5496H27.8736L25.7518 15.602L25.8767 15.7275Z"
|
||||
:fill="darkprimary"
|
||||
></path>
|
||||
<path d="M6.94549 13.5496L6.9479 13.552L9.12272 15.7275L17.4999 24.1041L28.0544 13.5496H6.94549Z" :fill="primary"></path>
|
||||
<path
|
||||
d="M46.5781 10V26H49.3594V14.9844H49.5078L53.9297 25.9531H56.0078L60.4297 15.0078H60.5781V26H63.3594V10H59.8125L55.0625 21.5937H54.875L50.125 10H46.5781ZM69.8438 26.2422C71.7266 26.2422 72.8516 25.3594 73.3672 24.3516H73.4609V26H76.1797V17.9687C76.1797 14.7969 73.5937 13.8438 71.3047 13.8438C68.7813 13.8438 66.8437 14.9687 66.2188 17.1562L68.8594 17.5312C69.1406 16.7109 69.9375 16.0078 71.3203 16.0078C72.6328 16.0078 73.3516 16.6797 73.3516 17.8594V17.9062C73.3516 18.7188 72.5 18.7578 70.3828 18.9844C68.0547 19.2344 65.8281 19.9297 65.8281 22.6328C65.8281 24.9922 67.5547 26.2422 69.8438 26.2422ZM70.5781 24.1641C69.3984 24.1641 68.5547 23.625 68.5547 22.5859C68.5547 21.5 69.5 21.0469 70.7656 20.8672C71.5078 20.7656 72.9922 20.5781 73.3594 20.2812V21.6953C73.3594 23.0312 72.2813 24.1641 70.5781 24.1641ZM81.8516 18.9687C81.8516 17.2344 82.8984 16.2344 84.3906 16.2344C85.8516 16.2344 86.7266 17.1953 86.7266 18.7969V26H89.5547V18.3594C89.5625 15.4844 87.9219 13.8438 85.4453 13.8438C83.6484 13.8438 82.4141 14.7031 81.8672 16.0391H81.7266V14H79.0234V26H81.8516V18.9687ZM98.4219 14H96.0547V11.125H93.2266V14H91.5234V16.1875H93.2266V22.8594C93.2109 25.1172 94.8516 26.2266 96.9766 26.1641C97.7813 26.1406 98.3359 25.9844 98.6406 25.8828L98.1641 23.6719C98.0078 23.7109 97.6875 23.7812 97.3359 23.7812C96.625 23.7812 96.0547 23.5312 96.0547 22.3906V16.1875H98.4219V14ZM100.787 26H103.615V14H100.787V26ZM102.209 12.2969C103.107 12.2969 103.842 11.6094 103.842 10.7656C103.842 9.91406 103.107 9.22656 102.209 9.22656C101.303 9.22656 100.568 9.91406 100.568 10.7656C100.568 11.6094 101.303 12.2969 102.209 12.2969ZM116.008 17.1719C115.617 15.1406 113.992 13.8438 111.18 13.8438C108.289 13.8438 106.32 15.2656 106.328 17.4844C106.32 19.2344 107.398 20.3906 109.703 20.8672L111.75 21.2969C112.852 21.5391 113.367 21.9844 113.367 22.6641C113.367 23.4844 112.477 24.1016 111.133 24.1016C109.836 24.1016 108.992 23.5391 108.75 22.4609L105.992 22.7266C106.344 24.9297 108.195 26.2344 111.141 26.2344C114.141 26.2344 116.258 24.6797 116.266 22.4062C116.258 20.6953 115.156 19.6484 112.891 19.1562L110.844 18.7188C109.625 18.4453 109.141 18.0234 109.148 17.3281C109.141 16.5156 110.039 15.9531 111.219 15.9531C112.523 15.9531 113.211 16.6641 113.43 17.4531L116.008 17.1719Z"
|
||||
fill="#000"
|
||||
fill-opacity="0.85"
|
||||
></path>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="8.62526" y1="14.0888" x2="5.56709" y2="17.1469" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="darkprimary"></stop>
|
||||
<stop offset="0.9637" stop-color="darkprimary" stop-opacity="0"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear" x1="26.2675" y1="14.1279" x2="28.7404" y2="16.938" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="darkprimary"></stop>
|
||||
<stop offset="1" stop-color="darkprimary" stop-opacity="0"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { RouterLink } from 'vue-router';
|
||||
const primary = ref('rgb(var(--v-theme-primary))');
|
||||
const darkprimary = ref('rgb(var(--v-theme-darkprimary))');
|
||||
</script>
|
||||
120
core/src/layouts/menus/LayoutSystemMenu.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useLayoutStore } from '@KTXC/stores/layoutStore';
|
||||
import { useIntegrationStore } from '@KTXC/stores/integrationStore';
|
||||
import Logo from '@KTXC/layouts/logo/LogoDark.vue';
|
||||
import SystemMenuGroupStatic from './LayoutSystemMenuGroupStatic.vue';
|
||||
import SystemMenuGroupDynamic from './LayoutSystemMenuGroupDynamic.vue';
|
||||
import SystemMenuItem from './LayoutSystemMenuItem.vue';
|
||||
|
||||
const layoutStore = useLayoutStore();
|
||||
const integrationStore = useIntegrationStore();
|
||||
|
||||
// Get all entries based on current menu mode
|
||||
const menuEntries = computed(() => {
|
||||
switch (layoutStore.menuMode) {
|
||||
case 'user-settings':
|
||||
return integrationStore.getPoint('user_settings_menu');
|
||||
case 'admin-settings':
|
||||
return integrationStore.getPoint('admin_settings_menu');
|
||||
case 'apps':
|
||||
default:
|
||||
return integrationStore.getPoint('app_menu');
|
||||
}
|
||||
});
|
||||
|
||||
// Menu mode display info
|
||||
const menuModeInfo = computed(() => {
|
||||
switch (layoutStore.menuMode) {
|
||||
case 'user-settings':
|
||||
return {
|
||||
icon: 'mdi-account-cog',
|
||||
label: 'Personal Settings',
|
||||
toggleLabel: 'Admin',
|
||||
toggleIcon: 'mdi-shield-crown',
|
||||
};
|
||||
case 'admin-settings':
|
||||
return {
|
||||
icon: 'mdi-shield-crown',
|
||||
label: 'Administration',
|
||||
toggleLabel: 'Apps',
|
||||
toggleIcon: 'mdi-view-dashboard',
|
||||
};
|
||||
case 'apps':
|
||||
default:
|
||||
return {
|
||||
icon: 'mdi-view-dashboard',
|
||||
label: 'Applications',
|
||||
toggleLabel: 'Settings',
|
||||
toggleIcon: 'mdi-account-cog',
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
methods: {
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-navigation-drawer
|
||||
left
|
||||
v-model="layoutStore.sidebarDrawer"
|
||||
elevation="0"
|
||||
rail-width="60"
|
||||
mobile-breakpoint="lg"
|
||||
app
|
||||
class="leftSidebar"
|
||||
:rail="layoutStore.miniSidebar"
|
||||
expand-on-hover
|
||||
>
|
||||
<div class="pa-5">
|
||||
<Logo />
|
||||
</div>
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!---Navigation -->
|
||||
<!-- ---------------------------------------------- -->
|
||||
<perfect-scrollbar class="scrollnavbar">
|
||||
<v-list aria-busy="true" aria-label="menu list">
|
||||
<!---Menu Loop -->
|
||||
<template v-for="entry in menuEntries" :key="entry.id">
|
||||
<!-- Group with dynamic style (collapsible) -->
|
||||
<SystemMenuGroupDynamic
|
||||
v-if="'items' in entry && entry.style === 'dynamic'"
|
||||
class="leftPadding"
|
||||
:group="entry"
|
||||
/>
|
||||
<!-- Group with static style (subheader) -->
|
||||
<SystemMenuGroupStatic
|
||||
v-else-if="'items' in entry"
|
||||
:group="entry"
|
||||
/>
|
||||
<!-- Single item (no group) -->
|
||||
<SystemMenuItem
|
||||
v-else
|
||||
:item="entry"
|
||||
/>
|
||||
</template>
|
||||
</v-list>
|
||||
</perfect-scrollbar>
|
||||
|
||||
<!-- Menu Mode Toggle -->
|
||||
<template v-slot:append>
|
||||
<v-divider />
|
||||
<v-list density="compact" class="pa-2">
|
||||
<v-list-item
|
||||
rounded
|
||||
color="primary"
|
||||
@click="layoutStore.toggleMenuMode()"
|
||||
:prepend-icon="menuModeInfo.toggleIcon"
|
||||
class="menu-mode-toggle"
|
||||
>
|
||||
<v-list-item-title class="text-body-2">{{ menuModeInfo.toggleLabel }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</template>
|
||||
</v-navigation-drawer>
|
||||
</template>
|
||||
42
core/src/layouts/menus/LayoutSystemMenuGroupDynamic.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import type { IntegrationGroup } from '@KTXC/types/integrationTypes';
|
||||
import NavItem from './LayoutSystemMenuItem.vue';
|
||||
|
||||
const props = defineProps<{ group: IntegrationGroup; level?: number }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!---Item Childern -->
|
||||
<!-- ---------------------------------------------- -->
|
||||
<v-list-group no-action>
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!---Dropdown -->
|
||||
<!-- ---------------------------------------------- -->
|
||||
<template v-slot:activator="{ props: activatorProps }">
|
||||
<v-list-item v-bind="activatorProps" :value="group.label" rounded class="mb-1" color="primary">
|
||||
<!---Icon -->
|
||||
<template v-slot:prepend>
|
||||
<v-icon v-if="group.icon" :icon="group.icon"></v-icon>
|
||||
</template>
|
||||
<!---Title -->
|
||||
<v-list-item-title class="mr-auto">{{ group.label }}</v-list-item-title>
|
||||
<!---If Caption-->
|
||||
<v-list-item-subtitle v-if="group.caption" class="text-caption mt-n1 hide-menu">
|
||||
{{ group.caption }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!---Sub Item-->
|
||||
<!-- ---------------------------------------------- -->
|
||||
<template v-for="(subitem, i) in group.items" :key="i">
|
||||
<NavItem :item="subitem" :level="(props.level ?? 0) + 1"></NavItem>
|
||||
</template>
|
||||
</v-list-group>
|
||||
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!---End Item Sub Header -->
|
||||
<!-- ---------------------------------------------- -->
|
||||
</template>
|
||||
|
||||
15
core/src/layouts/menus/LayoutSystemMenuGroupStatic.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { IntegrationGroup } from '@KTXC/types/integrationTypes';
|
||||
import LayoutSystemMenuItem from './LayoutSystemMenuItem.vue';
|
||||
|
||||
const props = defineProps<{ group: IntegrationGroup }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-list-subheader color="lightText" class="smallCap text-subtitle-2">{{ props.group.label }}</v-list-subheader>
|
||||
<LayoutSystemMenuItem
|
||||
v-for="(item, i) in props.group.items"
|
||||
:key="i"
|
||||
:item="item"
|
||||
/>
|
||||
</template>
|
||||
29
core/src/layouts/menus/LayoutSystemMenuItem.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import type { IntegrationItem } from '@KTXC/types/integrationTypes';
|
||||
|
||||
const props = defineProps<{ item: IntegrationItem; level?: number }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!---Single Item-->
|
||||
<v-list-item
|
||||
:to="item.toType === 'external' ? '' : item.to"
|
||||
:href="item.toType === 'external' ? item.to : ''"
|
||||
rounded
|
||||
class="mb-1"
|
||||
color="primary"
|
||||
:disabled="item.disabled"
|
||||
:target="item.toType === 'external' ? '_blank' : ''"
|
||||
@mouseenter="$emit && $emit('prefetch', item)"
|
||||
>
|
||||
<!---If icon-->
|
||||
<template v-slot:prepend>
|
||||
<v-icon v-if="props.item.icon" :icon="props.item.icon"></v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ item.label }}</v-list-item-title>
|
||||
<!---If Caption-->
|
||||
<v-list-item-subtitle v-if="item.caption" class="text-caption mt-n1 hide-menu">
|
||||
{{ item.caption }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</template>
|
||||
110
core/src/layouts/menus/LayoutUserMenu.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useTheme } from 'vuetify';
|
||||
import { useUserStore } from '@KTXC/stores/userStore';
|
||||
import { useIntegrationStore } from '@KTXC/stores/integrationStore';
|
||||
import { useLayoutStore } from '@KTXC/stores/layoutStore';
|
||||
import { useRouter } from 'vue-router';
|
||||
import defaultAvatar from '@KTXC/assets/images/users/avatar-1.png';
|
||||
|
||||
const theme = useTheme();
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const integrationStore = useIntegrationStore();
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
const userAuth = computed(() => userStore.auth);
|
||||
const profileMenuItems = computed(() => integrationStore.getItems('profile_menu'));
|
||||
const userAvatar = computed(() => userStore.getProfileField('avatar') || defaultAvatar);
|
||||
const userName = computed(() => {
|
||||
const given = userStore.getProfileField('name_given');
|
||||
const family = userStore.getProfileField('name_family');
|
||||
if (given && family) return `${given} ${family}`;
|
||||
return userAuth.value?.label || 'User';
|
||||
});
|
||||
const userEmail = computed(() => userStore.getProfileField('email') || '');
|
||||
|
||||
// Theme toggle
|
||||
const isDarkMode = computed(() => theme.global.name.value === 'dark');
|
||||
const toggleTheme = () => {
|
||||
const newTheme = theme.global.name.value === 'light' ? 'dark' : 'light';
|
||||
theme.global.name.value = newTheme;
|
||||
layoutStore.setTheme(newTheme);
|
||||
};
|
||||
|
||||
// Navigate to settings
|
||||
const goToSettings = () => {
|
||||
layoutStore.setMenuMode('settings');
|
||||
// Navigate to first settings item or a default settings route
|
||||
router.push('/modules'); // TODO: Make this dynamic based on first settings menu item
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!-- Profile Dropdown -->
|
||||
<!-- ---------------------------------------------- -->
|
||||
<div>
|
||||
<!-- User Info Header -->
|
||||
<div class="d-flex align-center pa-5 pb-4">
|
||||
<v-avatar size="40" class="mr-3">
|
||||
<v-img :src="userAvatar" :alt="userName" cover />
|
||||
</v-avatar>
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="text-h6 mb-0 font-weight-medium">
|
||||
{{ userName }}
|
||||
</h6>
|
||||
<p class="text-caption mb-0 text-medium-emphasis">{{ userEmail }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<v-divider />
|
||||
|
||||
<perfect-scrollbar style="max-height: 280px">
|
||||
<v-list class="py-0" aria-label="profile menu" aria-busy="true">
|
||||
<!-- Dynamic Profile Menu Items (from modules) -->
|
||||
<v-list-item
|
||||
v-for="item in profileMenuItems"
|
||||
:key="item.id"
|
||||
:to="item.toType === 'external' ? undefined : item.to"
|
||||
:href="item.toType === 'external' ? item.to : undefined"
|
||||
:target="item.toType === 'external' ? '_blank' : undefined"
|
||||
color="primary"
|
||||
rounded="0"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon v-if="item.icon">{{ item.icon }}</v-icon>
|
||||
</template>
|
||||
<v-list-item-title class="text-h6">{{ item.label }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider v-if="profileMenuItems.length" class="my-2" />
|
||||
|
||||
<!-- Theme Toggle -->
|
||||
<v-list-item @click="toggleTheme" color="primary" rounded="0">
|
||||
<template v-slot:prepend>
|
||||
<v-icon>{{ isDarkMode ? 'mdi-weather-sunny' : 'mdi-weather-night' }}</v-icon>
|
||||
</template>
|
||||
<v-list-item-title class="text-h6">{{ isDarkMode ? 'Light Mode' : 'Dark Mode' }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<!-- Go to Settings -->
|
||||
<v-list-item @click="goToSettings" color="primary" rounded="0">
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-cog-outline</v-icon>
|
||||
</template>
|
||||
<v-list-item-title class="text-h6">Settings</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<!-- Logout -->
|
||||
<v-list-item @click="userStore.logout()" color="secondary" rounded="0">
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-logout</v-icon>
|
||||
</template>
|
||||
<v-list-item-title class="text-h6">Logout</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</perfect-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
98
core/src/models/userProfile.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* User Profile Model
|
||||
*/
|
||||
|
||||
import type { ProfileFieldInterface, UserProfileInterface } from '@KTXC/types/user/userProfileTypes';
|
||||
|
||||
export class UserProfile {
|
||||
private _data: UserProfileInterface;
|
||||
|
||||
constructor(data: UserProfileInterface = {}) {
|
||||
this._data = data;
|
||||
}
|
||||
|
||||
fromJson(data: UserProfileInterface): UserProfile {
|
||||
this._data = data;
|
||||
return this;
|
||||
}
|
||||
|
||||
toJson(): UserProfileInterface {
|
||||
return { ...this._data };
|
||||
}
|
||||
|
||||
clone(): UserProfile {
|
||||
return new UserProfile(JSON.parse(JSON.stringify(this._data)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a profile field value
|
||||
*/
|
||||
get(key: string): any {
|
||||
return this._data[key]?.value ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a profile field value (only if editable)
|
||||
*/
|
||||
set(key: string, value: any): boolean {
|
||||
if (!this._data[key]) {
|
||||
console.warn(`Profile field "${key}" does not exist`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this._data[key].editable) {
|
||||
console.warn(`Profile field "${key}" is managed by ${this._data[key].provider} and cannot be edited`);
|
||||
return false;
|
||||
}
|
||||
|
||||
this._data[key].value = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a field is editable
|
||||
*/
|
||||
isEditable(key: string): boolean {
|
||||
return this._data[key]?.editable ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get field metadata
|
||||
*/
|
||||
getField(key: string): ProfileFieldInterface | null {
|
||||
return this._data[key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all fields
|
||||
*/
|
||||
get fields(): UserProfileInterface {
|
||||
return this._data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get only editable fields
|
||||
*/
|
||||
get editableFields(): UserProfileInterface {
|
||||
return Object.entries(this._data)
|
||||
.filter(([_, field]) => (field as ProfileFieldInterface).editable)
|
||||
.reduce((acc, [key, field]) => ({ ...acc, [key]: field }), {} as UserProfileInterface);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get only managed (non-editable) fields
|
||||
*/
|
||||
get managedFields(): UserProfileInterface {
|
||||
return Object.entries(this._data)
|
||||
.filter(([_, field]) => !(field as ProfileFieldInterface).editable)
|
||||
.reduce((acc, [key, field]) => ({ ...acc, [key]: field }), {} as UserProfileInterface);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get provider managing this profile
|
||||
*/
|
||||
get provider(): string | null {
|
||||
const managedField = Object.values(this._data).find(f => !(f as ProfileFieldInterface).editable);
|
||||
return (managedField as ProfileFieldInterface)?.provider ?? null;
|
||||
}
|
||||
}
|
||||
73
core/src/models/userSettings.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* User Settings Model
|
||||
*/
|
||||
|
||||
import type { UserSettingsInterface } from '@KTXC/types/user/userSettingsTypes';
|
||||
|
||||
export class UserSettings {
|
||||
private _data: UserSettingsInterface;
|
||||
|
||||
constructor(data: UserSettingsInterface = {}) {
|
||||
this._data = data;
|
||||
}
|
||||
|
||||
fromJson(data: UserSettingsInterface): UserSettings {
|
||||
this._data = data;
|
||||
return this;
|
||||
}
|
||||
|
||||
toJson(): UserSettingsInterface {
|
||||
return { ...this._data };
|
||||
}
|
||||
|
||||
clone(): UserSettings {
|
||||
return new UserSettings(JSON.parse(JSON.stringify(this._data)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a setting value
|
||||
*/
|
||||
get(key: string): any {
|
||||
return this._data[key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a setting value
|
||||
*/
|
||||
set(key: string, value: any): void {
|
||||
this._data[key] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a setting exists
|
||||
*/
|
||||
has(key: string): boolean {
|
||||
return key in this._data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all settings
|
||||
*/
|
||||
get all(): UserSettingsInterface {
|
||||
return this._data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get multiple settings
|
||||
*/
|
||||
getMany(keys: string[]): Record<string, any> {
|
||||
return keys.reduce((acc, key) => ({
|
||||
...acc,
|
||||
[key]: this._data[key] ?? null
|
||||
}), {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set multiple settings
|
||||
*/
|
||||
setMany(settings: Record<string, any>): void {
|
||||
Object.entries(settings).forEach(([key, value]) => {
|
||||
this._data[key] = value;
|
||||
});
|
||||
}
|
||||
}
|
||||
148
core/src/plugins/vuetify/defaults.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
export default {
|
||||
IconBtn: {
|
||||
icon: true,
|
||||
color: 'default',
|
||||
variant: 'text',
|
||||
},
|
||||
VAlert: {
|
||||
VBtn: {
|
||||
color: undefined,
|
||||
},
|
||||
},
|
||||
VAvatar: {
|
||||
// ℹ️ Remove after next release
|
||||
variant: 'flat',
|
||||
},
|
||||
VBadge: {
|
||||
// set v-badge default color to primary
|
||||
color: 'primary',
|
||||
},
|
||||
VBtn: {
|
||||
// set v-btn default color to primary
|
||||
color: 'primary',
|
||||
style: 'text-transform: none;',
|
||||
},
|
||||
VCard: {
|
||||
rounded: 'lg',
|
||||
elevation: 2,
|
||||
},
|
||||
VChip: {
|
||||
elevation: 0,
|
||||
},
|
||||
VMenu: {
|
||||
offset: '2px',
|
||||
},
|
||||
VPagination: {
|
||||
density: 'comfortable',
|
||||
showFirstLastPage: true,
|
||||
variant: 'tonal',
|
||||
},
|
||||
VTabs: {
|
||||
// set v-tabs default color to primary
|
||||
color: 'primary',
|
||||
VSlideGroup: {
|
||||
showArrows: true,
|
||||
},
|
||||
},
|
||||
VTooltip: {
|
||||
// set v-tooltip default location to top
|
||||
location: 'top',
|
||||
},
|
||||
VCheckboxBtn: {
|
||||
color: 'primary',
|
||||
},
|
||||
VCheckbox: {
|
||||
// set v-checkbox default color to primary
|
||||
color: 'primary',
|
||||
density: 'comfortable',
|
||||
hideDetails: 'auto',
|
||||
},
|
||||
VRadioGroup: {
|
||||
color: 'primary',
|
||||
density: 'comfortable',
|
||||
hideDetails: 'auto',
|
||||
},
|
||||
VRadio: {
|
||||
density: 'comfortable',
|
||||
hideDetails: 'auto',
|
||||
},
|
||||
VSelect: {
|
||||
variant: 'outlined',
|
||||
color: 'primary',
|
||||
hideDetails: 'auto',
|
||||
density: 'comfortable',
|
||||
},
|
||||
VRangeSlider: {
|
||||
// set v-range-slider default color to primary
|
||||
color: 'primary',
|
||||
thumbLabel: true,
|
||||
hideDetails: 'auto',
|
||||
trackSize: 6,
|
||||
thumbSize: 22,
|
||||
elevation: 4,
|
||||
},
|
||||
VRating: {
|
||||
// set v-rating default color to primary
|
||||
activeColor: 'warning',
|
||||
color: 'disabled',
|
||||
},
|
||||
VProgressCircular: {
|
||||
// set v-progress-circular default color to primary
|
||||
color: 'primary',
|
||||
},
|
||||
VProgressLinear: {
|
||||
color: 'primary',
|
||||
},
|
||||
VSlider: {
|
||||
// set v-slider default color to primary
|
||||
color: 'primary',
|
||||
trackSize: 6,
|
||||
hideDetails: 'auto',
|
||||
thumbSize: 22,
|
||||
elevation: 4,
|
||||
},
|
||||
VSnackbar: {
|
||||
VBtn: {
|
||||
size: 'small',
|
||||
},
|
||||
},
|
||||
VTextField: {
|
||||
variant: 'outlined',
|
||||
density: 'comfortable',
|
||||
color: 'primary',
|
||||
hideDetails: 'auto',
|
||||
},
|
||||
VAutocomplete: {
|
||||
variant: 'outlined',
|
||||
color: 'primary',
|
||||
density: 'comfortable',
|
||||
hideDetails: 'auto',
|
||||
},
|
||||
VCombobox: {
|
||||
variant: 'outlined',
|
||||
color: 'primary',
|
||||
hideDetails: 'auto',
|
||||
density: 'comfortable',
|
||||
},
|
||||
VFileInput: {
|
||||
variant: 'outlined',
|
||||
color: 'primary',
|
||||
hideDetails: 'auto',
|
||||
density: 'comfortable',
|
||||
},
|
||||
VTextarea: {
|
||||
variant: 'outlined',
|
||||
color: 'primary',
|
||||
hideDetails: 'auto',
|
||||
density: 'comfortable',
|
||||
},
|
||||
VSwitch: {
|
||||
// set v-switch default color to primary
|
||||
inset: true,
|
||||
color: 'primary',
|
||||
hideDetails: 'auto',
|
||||
},
|
||||
VNavigationDrawer: {
|
||||
touchless: true,
|
||||
},
|
||||
}
|
||||
14
core/src/plugins/vuetify/icons.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { IconAliases } from 'vuetify'
|
||||
import { aliases as mdiAliases, mdi } from 'vuetify/iconsets/mdi'
|
||||
|
||||
const aliases: Partial<IconAliases> = {
|
||||
...mdiAliases,
|
||||
}
|
||||
|
||||
export const icons = {
|
||||
defaultSet: 'mdi',
|
||||
aliases,
|
||||
sets: {
|
||||
mdi,
|
||||
},
|
||||
}
|
||||
24
core/src/plugins/vuetify/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { createVuetify } from 'vuetify'
|
||||
import { VBtn } from 'vuetify/components/VBtn'
|
||||
import * as components from 'vuetify/components'
|
||||
import * as directives from 'vuetify/directives'
|
||||
import defaults from './defaults'
|
||||
import { icons } from './icons'
|
||||
import { themes } from './theme'
|
||||
|
||||
// Styles
|
||||
import 'vuetify/styles'
|
||||
|
||||
export default createVuetify({
|
||||
components,
|
||||
directives,
|
||||
aliases: {
|
||||
IconBtn: VBtn,
|
||||
},
|
||||
defaults,
|
||||
icons,
|
||||
theme: {
|
||||
defaultTheme: 'light',
|
||||
themes,
|
||||
},
|
||||
})
|
||||
144
core/src/plugins/vuetify/theme.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import type { ThemeDefinition } from 'vuetify'
|
||||
|
||||
export const staticPrimaryColor = '#6366F1'
|
||||
export const staticPrimaryDarkenColor = '#4F46E5'
|
||||
|
||||
export const themes: Record<string, ThemeDefinition> = {
|
||||
light: {
|
||||
dark: false,
|
||||
colors: {
|
||||
// Primary brand colors - Modern indigo
|
||||
'primary': staticPrimaryColor,
|
||||
'on-primary': '#FFFFFF',
|
||||
'primary-darken-1': staticPrimaryDarkenColor,
|
||||
'primary-lighten-1': '#818CF8',
|
||||
|
||||
// Secondary - Purple accent
|
||||
'secondary': '#8B5CF6',
|
||||
'secondary-darken-1': '#7C3AED',
|
||||
'secondary-lighten-1': '#A78BFA',
|
||||
'on-secondary': '#FFFFFF',
|
||||
|
||||
// Semantic colors
|
||||
'success': '#10B981',
|
||||
'on-success': '#FFFFFF',
|
||||
'info': '#3B82F6',
|
||||
'on-info': '#FFFFFF',
|
||||
'warning': '#F59E0B',
|
||||
'on-warning': '#FFFFFF',
|
||||
'error': '#EF4444',
|
||||
'on-error': '#FFFFFF',
|
||||
|
||||
// Surface & backgrounds
|
||||
'background': '#F8FAFC',
|
||||
'on-background': '#0F172A',
|
||||
'surface': '#FFFFFF',
|
||||
'on-surface': '#0F172A',
|
||||
'surface-bright': '#FFFFFF',
|
||||
'surface-variant': '#F1F5F9',
|
||||
'on-surface-variant': '#64748B',
|
||||
|
||||
// Grey scale
|
||||
'grey': '#64748B',
|
||||
'grey-darken-1': '#475569',
|
||||
'grey-lighten-1': '#94A3B8',
|
||||
'grey-lighten-2': '#CBD5E1',
|
||||
'grey-lighten-3': '#E2E8F0',
|
||||
'grey-lighten-4': '#F1F5F9',
|
||||
'grey-lighten-5': '#F8FAFC',
|
||||
|
||||
// Component specific
|
||||
'perfect-scrollbar-thumb': '#CBD5E1',
|
||||
'track-bg': '#F1F5F9',
|
||||
},
|
||||
|
||||
variables: {
|
||||
'border-color': '#E2E8F0',
|
||||
'border-opacity': 0.12,
|
||||
'high-emphasis-opacity': 0.87,
|
||||
'medium-emphasis-opacity': 0.60,
|
||||
'disabled-opacity': 0.38,
|
||||
'idle-opacity': 0.04,
|
||||
'hover-opacity': 0.04,
|
||||
'focus-opacity': 0.12,
|
||||
'selected-opacity': 0.08,
|
||||
'activated-opacity': 0.12,
|
||||
'pressed-opacity': 0.12,
|
||||
'dragged-opacity': 0.08,
|
||||
'theme-kbd': '#212529',
|
||||
'theme-on-kbd': '#FFFFFF',
|
||||
'theme-code': '#F5F5F5',
|
||||
'theme-on-code': '#000000',
|
||||
},
|
||||
},
|
||||
|
||||
dark: {
|
||||
dark: true,
|
||||
colors: {
|
||||
// Primary brand colors - Lighter shades for dark mode
|
||||
'primary': '#818CF8',
|
||||
'on-primary': '#FFFFFF',
|
||||
'primary-darken-1': '#6366F1',
|
||||
'primary-lighten-1': '#A5B4FC',
|
||||
|
||||
// Secondary - Purple accent
|
||||
'secondary': '#A78BFA',
|
||||
'secondary-darken-1': '#8B5CF6',
|
||||
'secondary-lighten-1': '#C4B5FD',
|
||||
'on-secondary': '#FFFFFF',
|
||||
|
||||
// Semantic colors - Adjusted for dark mode
|
||||
'success': '#34D399',
|
||||
'on-success': '#FFFFFF',
|
||||
'info': '#60A5FA',
|
||||
'on-info': '#FFFFFF',
|
||||
'warning': '#FBBF24',
|
||||
'on-warning': '#FFFFFF',
|
||||
'error': '#F87171',
|
||||
'on-error': '#FFFFFF',
|
||||
|
||||
// Surface & backgrounds - Dark slate palette
|
||||
'background': '#0F172A',
|
||||
'on-background': '#F1F5F9',
|
||||
'surface': '#1E293B',
|
||||
'on-surface': '#F1F5F9',
|
||||
'surface-bright': '#334155',
|
||||
'surface-variant': '#1E293B',
|
||||
'on-surface-variant': '#94A3B8',
|
||||
|
||||
// Grey scale
|
||||
'grey': '#94A3B8',
|
||||
'grey-darken-1': '#CBD5E1',
|
||||
'grey-lighten-1': '#64748B',
|
||||
'grey-lighten-2': '#475569',
|
||||
'grey-lighten-3': '#334155',
|
||||
'grey-lighten-4': '#1E293B',
|
||||
'grey-lighten-5': '#0F172A',
|
||||
|
||||
// Component specific
|
||||
'perfect-scrollbar-thumb': '#475569',
|
||||
'track-bg': '#334155',
|
||||
},
|
||||
|
||||
variables: {
|
||||
'border-color': '#334155',
|
||||
'border-opacity': 0.12,
|
||||
'high-emphasis-opacity': 0.87,
|
||||
'medium-emphasis-opacity': 0.60,
|
||||
'disabled-opacity': 0.38,
|
||||
'idle-opacity': 0.10,
|
||||
'hover-opacity': 0.08,
|
||||
'focus-opacity': 0.12,
|
||||
'selected-opacity': 0.12,
|
||||
'activated-opacity': 0.16,
|
||||
'pressed-opacity': 0.14,
|
||||
'dragged-opacity': 0.10,
|
||||
'theme-kbd': '#212529',
|
||||
'theme-on-kbd': '#FFFFFF',
|
||||
'theme-code': '#343434',
|
||||
'theme-on-code': '#CCCCCC',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default themes
|
||||
23
core/src/private.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"vue": "/vendor/vue.mjs",
|
||||
"vue-router": "/vendor/vue-router.mjs",
|
||||
"pinia": "/vendor/pinia.mjs",
|
||||
"@KTXC/utils/helpers/fetch-wrapper-core": "/js/shared-utils.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>K-Trix</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./private.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
72
core/src/private.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import * as Vue from 'vue'
|
||||
import * as VueRouterLib from 'vue-router'
|
||||
import * as PiniaLib from 'pinia'
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { PerfectScrollbarPlugin } from 'vue3-perfect-scrollbar'
|
||||
import { useModuleStore } from '@KTXC/stores/moduleStore'
|
||||
import { useTenantStore } from '@KTXC/stores/tenantStore'
|
||||
import { useUserStore } from '@KTXC/stores/userStore'
|
||||
import { fetchWrapper } from '@KTXC/utils/helpers/fetch-wrapper'
|
||||
import { initializeModules } from '@KTXC/utils/modules'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import vuetify from './plugins/vuetify/index'
|
||||
import '@KTXC/scss/style.scss'
|
||||
|
||||
// Material Design Icons (Vuetify mdi icon set)
|
||||
import '@mdi/font/css/materialdesignicons.min.css'
|
||||
|
||||
// google-fonts
|
||||
import '@fontsource/public-sans/index.css'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
app.use(pinia)
|
||||
app.use(PerfectScrollbarPlugin)
|
||||
app.use(vuetify)
|
||||
|
||||
// Note: Router is registered AFTER modules are loaded to prevent premature route matching
|
||||
|
||||
const globalWindow = window as typeof window & {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
globalWindow.Vue = Vue
|
||||
globalWindow.vue = Vue
|
||||
globalWindow.VueRouter = VueRouterLib
|
||||
globalWindow.Pinia = PiniaLib as unknown
|
||||
|
||||
// Bootstrap initial private UI state (modules, tenant, user) before mounting
|
||||
(async () => {
|
||||
const moduleStore = useModuleStore();
|
||||
const tenantStore = useTenantStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
try {
|
||||
const payload = await fetchWrapper.get('/init');
|
||||
moduleStore.init(payload?.modules ?? {});
|
||||
tenantStore.init(payload?.tenant ?? null);
|
||||
userStore.init(payload?.user ?? {});
|
||||
|
||||
// Initialize registered modules (following reference app's bootstrap pattern)
|
||||
await initializeModules(app);
|
||||
|
||||
// Add 404 catch-all route AFTER all modules are loaded
|
||||
// This ensures module routes are registered before the catch-all
|
||||
router.addRoute({
|
||||
name: 'NotFound',
|
||||
path: '/:pathMatch(.*)*',
|
||||
component: () => import('@KTXC/views/pages/maintenance/error/Error404Page.vue')
|
||||
});
|
||||
|
||||
// Register router AFTER modules are loaded
|
||||
app.use(router);
|
||||
|
||||
await router.isReady();
|
||||
// Home redirect handled by router beforeEnter
|
||||
app.mount('#app');
|
||||
} catch (e) {
|
||||
console.error('Bootstrap failed:', e);
|
||||
}
|
||||
})();
|
||||
13
core/src/public.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Ktrix Cloud</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./public.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
39
core/src/public.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import App from './App.vue';
|
||||
import { router } from './router';
|
||||
import vuetify from './plugins/vuetify/index';
|
||||
import '@KTXC/scss/style.scss';
|
||||
|
||||
// Material Design Icons (Vuetify mdi icon set)
|
||||
import '@mdi/font/css/materialdesignicons.min.css';
|
||||
|
||||
// google-fonts
|
||||
import '@fontsource/public-sans/400.css';
|
||||
import '@fontsource/public-sans/500.css';
|
||||
import '@fontsource/public-sans/600.css';
|
||||
import '@fontsource/public-sans/700.css';
|
||||
|
||||
// The public app is served when the user has no valid server session.
|
||||
// Clear any stale identity data from localStorage to ensure the client
|
||||
// state matches the server's determination that the user is unauthenticated.
|
||||
//localStorage.removeItem('identityStore.self');
|
||||
|
||||
const app = createApp(App);
|
||||
const pinia = createPinia();
|
||||
app.use(pinia);
|
||||
app.use(router);
|
||||
app.use(vuetify);
|
||||
|
||||
// Wait for router to be ready, then ensure we're on a public route
|
||||
//router.isReady().then(() => {
|
||||
// If the current route requires auth, redirect to login
|
||||
// This handles the case where user navigates to / with an expired session
|
||||
//const currentRoute = router.currentRoute.value;
|
||||
//const requiresAuth = currentRoute.matched.some(record => record.meta?.requiresAuth);
|
||||
//if (requiresAuth || currentRoute.path === '/') {
|
||||
// router.replace('/login');
|
||||
//}
|
||||
//});
|
||||
|
||||
app.mount('#app');
|
||||
116
core/src/router/index.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';
|
||||
import { useUserStore } from '@KTXC/stores/userStore';
|
||||
import { useLayoutStore } from '@KTXC/stores/layoutStore';
|
||||
import BlankLayout from '@KTXC/layouts/blank/BlankLayout.vue';
|
||||
import PrivateLayout from '@KTXC/views/PrivateLayout.vue';
|
||||
import { useIntegrationStore } from '@KTXC/stores/integrationStore';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
// Public login route
|
||||
{
|
||||
name: 'login',
|
||||
path: '/login',
|
||||
meta: { requiresAuth: false },
|
||||
component: BlankLayout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: () => import('@KTXC/views/authentication/LoginPage.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
// Logout performs action then redirects
|
||||
{
|
||||
name: 'logout',
|
||||
path: '/logout',
|
||||
meta: { requiresAuth: true },
|
||||
component: BlankLayout,
|
||||
beforeEnter: async () => {
|
||||
const userStore = useUserStore();
|
||||
await userStore.logout();
|
||||
return false;
|
||||
}
|
||||
},
|
||||
// Private area (shell layout). Module routes under /m/{namespace} are added at runtime.
|
||||
{
|
||||
name: 'private',
|
||||
path: '/',
|
||||
component: PrivateLayout,
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
// Index redirects to the first available module route (if any)
|
||||
{
|
||||
name: 'home',
|
||||
path: '',
|
||||
meta: { requiresAuth: true },
|
||||
component: BlankLayout,
|
||||
beforeEnter: (to, from, next) => {
|
||||
const integrationStore = useIntegrationStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
// Treat preference as a route name (e.g., "samples.overview")
|
||||
const preferredRouteName = userStore.getSetting('default_module');
|
||||
if (preferredRouteName) {
|
||||
// If a route with this name exists, go there
|
||||
try {
|
||||
// using router variable at runtime:
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
const exists = router.getRoutes().some(r => r.name === preferredRouteName);
|
||||
if (exists) return next({ name: preferredRouteName });
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Get first available menu item from app_menu
|
||||
const entries = integrationStore.getPoint('app_menu');
|
||||
for (const entry of entries) {
|
||||
// Check if it's a group with items
|
||||
if ('items' in entry && entry.items.length > 0) {
|
||||
const first = entry.items[0]?.to;
|
||||
if (first) return next(first);
|
||||
}
|
||||
// Or a standalone item
|
||||
if ('to' in entry && entry.to) {
|
||||
return next(entry.to);
|
||||
}
|
||||
}
|
||||
return next();
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes
|
||||
});
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const userStore = useUserStore();
|
||||
|
||||
const authRequired = to.matched.some((record) => record.meta?.requiresAuth);
|
||||
|
||||
if (authRequired && !userStore.isAuthenticated && to.path !== '/login') {
|
||||
userStore.returnUrl = to.fullPath;
|
||||
return next('/login');
|
||||
}
|
||||
|
||||
if (userStore.isAuthenticated && to.path === '/login') {
|
||||
const dest = userStore.returnUrl && userStore.returnUrl !== '/' ? userStore.returnUrl : '/';
|
||||
return next(dest);
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
router.beforeEach(() => {
|
||||
const layoutStore = useLayoutStore();
|
||||
layoutStore.isLoading = true;
|
||||
});
|
||||
|
||||
router.afterEach(() => {
|
||||
const layoutStore = useLayoutStore();
|
||||
layoutStore.isLoading = false;
|
||||
});
|
||||
|
||||
export default router
|
||||
115
core/src/scss/_override.scss
Normal file
@@ -0,0 +1,115 @@
|
||||
html {
|
||||
.bg-success,
|
||||
.bg-info,
|
||||
.bg-warning {
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
.v-row + .v-row {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.v-divider {
|
||||
opacity: 1;
|
||||
border-color: rgb(var(--v-theme-borderLight));
|
||||
}
|
||||
|
||||
.v-table > .v-table__wrapper > table > thead > tr > th {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.border-blue-right {
|
||||
border-right: 1px solid rgba(var(--v-theme-borderLight), 0.36);
|
||||
}
|
||||
|
||||
.link-hover {
|
||||
text-decoration: unset;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.v-selection-control {
|
||||
flex: unset;
|
||||
}
|
||||
|
||||
.customizer-btn .icon {
|
||||
animation: progress-circular-rotate 1.4s linear infinite;
|
||||
transform-origin: center center;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.no-spacer {
|
||||
.v-list-item__spacer {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes progress-circular-rotate {
|
||||
100% {
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
&.v-toolbar--border {
|
||||
border-color: rgb(var(--v-theme-borderLight));
|
||||
}
|
||||
}
|
||||
|
||||
.v-toolbar {
|
||||
&.v-app-bar {
|
||||
border-bottom: 1px solid rgba(var(--v-theme-borderLight), 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.v-sheet--border {
|
||||
border: 1px solid rgba(var(--v-theme-borderLight), 0.8);
|
||||
}
|
||||
|
||||
// table css
|
||||
.v-table {
|
||||
&.v-table--hover {
|
||||
> .v-table__wrapper {
|
||||
> table {
|
||||
> tbody {
|
||||
> tr {
|
||||
&:hover {
|
||||
td {
|
||||
background: rgb(var(--v-theme-gray100));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// accordion page css
|
||||
.v-expansion-panel {
|
||||
border: 1px solid rgb(var(--v-theme-borderLight));
|
||||
&:not(:first-child) {
|
||||
margin-top: -1px;
|
||||
}
|
||||
.v-expansion-panel-text__wrapper {
|
||||
border-top: 1px solid rgb(var(--v-theme-borderLight));
|
||||
padding: 16px 24px;
|
||||
}
|
||||
&.v-expansion-panel--active {
|
||||
.v-expansion-panel-title--active {
|
||||
.v-expansion-panel-title__overlay {
|
||||
background-color: rgb(var(--v-theme-gray100));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.v-expansion-panel--active {
|
||||
> .v-expansion-panel-title {
|
||||
min-height: unset;
|
||||
}
|
||||
}
|
||||
.v-expansion-panel--disabled .v-expansion-panel-title {
|
||||
color: rgba(var(--v-theme-on-surface), 0.15);
|
||||
}
|
||||
140
core/src/scss/_variables.scss
Normal file
@@ -0,0 +1,140 @@
|
||||
@use 'sass:math';
|
||||
@use 'sass:map';
|
||||
@use 'sass:meta';
|
||||
@use 'vuetify/lib/styles/tools/functions' as *;
|
||||
|
||||
// This will false all colors which is not necessory for theme
|
||||
$color-pack: false;
|
||||
|
||||
// Global font size and border radius
|
||||
$font-size-root: 1rem;
|
||||
$border-radius-root: 4px;
|
||||
$body-font-family: 'Public sans', sans-serif !default;
|
||||
$heading-font-family: $body-font-family !default;
|
||||
$btn-font-weight: 400 !default;
|
||||
$btn-letter-spacing: 0 !default;
|
||||
|
||||
// Global Radius as per breakeven point
|
||||
$rounded: () !default;
|
||||
$rounded: map-deep-merge(
|
||||
(
|
||||
0: 0,
|
||||
'sm': $border-radius-root * 0.5,
|
||||
null: $border-radius-root,
|
||||
'md': $border-radius-root * 1,
|
||||
'lg': $border-radius-root * 2,
|
||||
'xl': $border-radius-root * 6,
|
||||
'pill': 9999px,
|
||||
'circle': 50%,
|
||||
'shaped': $border-radius-root * 6 0
|
||||
),
|
||||
$rounded
|
||||
);
|
||||
// Global Typography
|
||||
$typography: () !default;
|
||||
$typography: map-deep-merge(
|
||||
(
|
||||
'h1': (
|
||||
'size': 2.375rem,
|
||||
'weight': 600,
|
||||
'line-height': 1.21,
|
||||
'font-family': inherit
|
||||
),
|
||||
'h2': (
|
||||
'size': 1.875rem,
|
||||
'weight': 600,
|
||||
'line-height': 1.27,
|
||||
'font-family': inherit
|
||||
),
|
||||
'h3': (
|
||||
'size': 1.5rem,
|
||||
'weight': 600,
|
||||
'line-height': 1.33,
|
||||
'font-family': inherit
|
||||
),
|
||||
'h4': (
|
||||
'size': 1.25rem,
|
||||
'weight': 600,
|
||||
'line-height': 1.4,
|
||||
'font-family': inherit
|
||||
),
|
||||
'h5': (
|
||||
'size': 1rem,
|
||||
'weight': 600,
|
||||
'line-height': 1.5,
|
||||
'font-family': inherit
|
||||
),
|
||||
'h6': (
|
||||
'size': 0.875rem,
|
||||
'weight': 400,
|
||||
'line-height': 1.57,
|
||||
'font-family': inherit
|
||||
),
|
||||
'subtitle-1': (
|
||||
'size': 0.875rem,
|
||||
'weight': 600,
|
||||
'line-height': 1.57,
|
||||
'font-family': inherit
|
||||
),
|
||||
'subtitle-2': (
|
||||
'size': 0.75rem,
|
||||
'weight': 500,
|
||||
'line-height': 1.66,
|
||||
'font-family': inherit
|
||||
),
|
||||
'body-1': (
|
||||
'size': 0.875rem,
|
||||
'weight': 400,
|
||||
'line-height': 1.57,
|
||||
'font-family': inherit
|
||||
),
|
||||
'body-2': (
|
||||
'size': 0.75rem,
|
||||
'weight': 400,
|
||||
'line-height': 1.66,
|
||||
'font-family': inherit
|
||||
),
|
||||
'button': (
|
||||
'size': 0.875rem,
|
||||
'weight': 500,
|
||||
'font-family': inherit,
|
||||
'text-transform': uppercase
|
||||
),
|
||||
'caption': (
|
||||
'size': 0.75rem,
|
||||
'weight': 400,
|
||||
'letter-spacing': 0,
|
||||
'font-family': inherit
|
||||
),
|
||||
'overline': (
|
||||
'size': 0.75rem,
|
||||
'weight': 500,
|
||||
'font-family': inherit,
|
||||
'line-height': 1.67,
|
||||
'letter-spacing': 0,
|
||||
'text-transform': uppercase
|
||||
)
|
||||
),
|
||||
$typography
|
||||
);
|
||||
|
||||
// Custom Variables
|
||||
// colors
|
||||
$white: #fff !default;
|
||||
|
||||
// cards
|
||||
$card-item-spacer-xy: 20px !default;
|
||||
$card-text-spacer: 20px !default;
|
||||
$card-title-size: 16px !default;
|
||||
|
||||
// Global Shadow
|
||||
$box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.08);
|
||||
|
||||
$theme-colors: (
|
||||
primary: var(--v-theme-primary),
|
||||
secondary: var(--v-theme-secondary),
|
||||
success: var(--v-theme-success),
|
||||
info: var(--v-theme-info),
|
||||
warning: var(--v-theme-warning),
|
||||
error: var(--v-theme-error)
|
||||
);
|
||||
37
core/src/scss/components/_VAlert.scss
Normal file
@@ -0,0 +1,37 @@
|
||||
.single-line-alert {
|
||||
.v-alert__close,
|
||||
.v-alert__prepend {
|
||||
align-self: center !important;
|
||||
}
|
||||
}
|
||||
|
||||
.v-alert__prepend {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.v-alert--variant-tonal {
|
||||
&.with-border {
|
||||
@each $color, $value in $theme-colors {
|
||||
&.text-#{$color} {
|
||||
border: 1px solid rgba(#{$value}, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.single-line-alert {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.v-alert__append {
|
||||
margin-inline-start: 0px;
|
||||
}
|
||||
.v-alert__close {
|
||||
margin-left: auto;
|
||||
}
|
||||
.v-alert__content {
|
||||
width: 100%;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
core/src/scss/components/_VBadge.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
.v-badge__badge {
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
padding: 4px;
|
||||
}
|
||||
.v-badge--dot {
|
||||
.v-badge__badge {
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
}
|
||||
}
|
||||
32
core/src/scss/components/_VBreadcrumb.scss
Normal file
@@ -0,0 +1,32 @@
|
||||
.v-breadcrumbs-item--link {
|
||||
color: rgb(var(--v-theme-lightText));
|
||||
}
|
||||
.v-breadcrumbs {
|
||||
.v-breadcrumbs-item--disabled {
|
||||
--v-disabled-opacity: 1;
|
||||
.v-breadcrumbs-item--link {
|
||||
color: rgb(var(--v-theme-darkText));
|
||||
}
|
||||
}
|
||||
.v-breadcrumbs-divider {
|
||||
color: rgb(var(--v-theme-lightText));
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb-with-title {
|
||||
.v-toolbar__content {
|
||||
height: unset !important;
|
||||
padding: 20px 0;
|
||||
}
|
||||
.v-breadcrumbs__prepend {
|
||||
svg {
|
||||
vertical-align: -3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb-height {
|
||||
.v-toolbar__content {
|
||||
height: unset !important;
|
||||
}
|
||||
}
|
||||
68
core/src/scss/components/_VButtons.scss
Normal file
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// Light Buttons
|
||||
//
|
||||
|
||||
.v-btn {
|
||||
&.bg-lightprimary {
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
background-color: rgb(var(--v-theme-primary)) !important;
|
||||
color: $white !important;
|
||||
}
|
||||
}
|
||||
&.bg-lightsecondary {
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
background-color: rgb(var(--v-theme-secondary)) !important;
|
||||
color: $white !important;
|
||||
}
|
||||
}
|
||||
&.text-facebook {
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
background-color: rgb(var(--v-theme-facebook)) !important;
|
||||
color: $white !important;
|
||||
}
|
||||
}
|
||||
&.text-twitter {
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
background-color: rgb(var(--v-theme-twitter)) !important;
|
||||
color: $white !important;
|
||||
}
|
||||
}
|
||||
&.text-linkedin {
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
background-color: rgb(var(--v-theme-linkedin)) !important;
|
||||
color: $white !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-btn {
|
||||
text-transform: capitalize;
|
||||
letter-spacing: $btn-letter-spacing;
|
||||
font-weight: 400;
|
||||
}
|
||||
.v-btn--icon.v-btn--density-default {
|
||||
width: calc(var(--v-btn-height) + 6px);
|
||||
height: calc(var(--v-btn-height) + 6px);
|
||||
}
|
||||
|
||||
.v-btn-group .v-btn {
|
||||
height: inherit !important;
|
||||
}
|
||||
|
||||
.v-btn-group {
|
||||
border-color: rgba(var(--v-border-color), 1);
|
||||
}
|
||||
|
||||
.v-btn-group--divided .v-btn:not(:last-child) {
|
||||
border-inline-end-color: rgba(var(--v-border-color), 1);
|
||||
}
|
||||
40
core/src/scss/components/_VCard.scss
Normal file
@@ -0,0 +1,40 @@
|
||||
// Outline Card
|
||||
.v-card--variant-outlined {
|
||||
border-color: rgba(var(--v-theme-borderLight), 1);
|
||||
.v-divider {
|
||||
border-color: rgba(var(--v-theme-borderLight), 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.v-card-text {
|
||||
padding: $card-text-spacer;
|
||||
}
|
||||
|
||||
.v-card-actions {
|
||||
padding: 14px $card-text-spacer 14px;
|
||||
}
|
||||
|
||||
.v-card {
|
||||
overflow: visible;
|
||||
.v-card-title {
|
||||
&.text-h6 {
|
||||
font-weight: 600;
|
||||
line-height: 1.57;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-card-item {
|
||||
padding: $card-item-spacer-xy;
|
||||
}
|
||||
|
||||
.v-card-subtitle {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.title-card {
|
||||
.v-card-text {
|
||||
background-color: rgb(var(--v-theme-background));
|
||||
border: 1px solid rgba(var(--v-theme-borderLight), 1);
|
||||
}
|
||||
}
|
||||
9
core/src/scss/components/_VField.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
.v-field--variant-outlined .v-field__outline__start.v-locale--is-ltr,
|
||||
.v-locale--is-ltr .v-field--variant-outlined .v-field__outline__start {
|
||||
border-radius: $border-radius-root 0 0 $border-radius-root;
|
||||
}
|
||||
|
||||
.v-field--variant-outlined .v-field__outline__end.v-locale--is-ltr,
|
||||
.v-locale--is-ltr .v-field--variant-outlined .v-field__outline__end {
|
||||
border-radius: 0 $border-radius-root $border-radius-root 0;
|
||||
}
|
||||
55
core/src/scss/components/_VInput.scss
Normal file
@@ -0,0 +1,55 @@
|
||||
.v-input--density-default:not(.v-autocomplete--multiple),
|
||||
.v-field--variant-solo,
|
||||
.v-field--variant-filled {
|
||||
--v-input-control-height: 39px;
|
||||
--v-input-padding-top: 2px;
|
||||
input.v-field__input {
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
.v-field__input {
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
textarea {
|
||||
padding-top: 11px;
|
||||
}
|
||||
}
|
||||
.v-input--density-default {
|
||||
.v-field__input {
|
||||
min-height: 41px;
|
||||
}
|
||||
}
|
||||
.v-field--variant-outlined {
|
||||
&.v-field--focused {
|
||||
.v-field__outline {
|
||||
--v-field-border-width: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.v-input {
|
||||
.v-input__details {
|
||||
padding-inline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.v-input--density-comfortable {
|
||||
--v-input-control-height: 56px;
|
||||
--v-input-padding-top: 17px;
|
||||
}
|
||||
.v-label {
|
||||
font-size: 0.875rem;
|
||||
--v-medium-emphasis-opacity: 0.8;
|
||||
}
|
||||
.v-switch .v-label,
|
||||
.v-checkbox .v-label {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
textarea.v-field__input {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.textarea-input {
|
||||
.v-label {
|
||||
top: 15px;
|
||||
}
|
||||
}
|
||||
47
core/src/scss/components/_VList.scss
Normal file
@@ -0,0 +1,47 @@
|
||||
.v-list-item {
|
||||
&.v-list-item--border {
|
||||
border-color: rgb(var(--v-border-color));
|
||||
border-width: 0 0 1px 0;
|
||||
&:last-child {
|
||||
border-width: 0;
|
||||
}
|
||||
}
|
||||
&.v-list-item--variant-tonal {
|
||||
background: rgb(var(--v-theme-gray100));
|
||||
.v-list-item__underlay {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
&:last-child {
|
||||
.v-list-item__content {
|
||||
.v-divider--inset {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.v-list {
|
||||
&[aria-busy='true'] {
|
||||
cursor: context-menu;
|
||||
}
|
||||
}
|
||||
.v-list-group__items {
|
||||
.v-list-item {
|
||||
padding-inline-start: 40px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.v-list-item__content {
|
||||
.v-divider--inset:not(.v-divider--vertical) {
|
||||
max-width: 100%;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.v-list--border {
|
||||
.v-list-item {
|
||||
+ .v-list-item {
|
||||
border-top: 1px solid rgb(var(--v-theme-borderLight));
|
||||
}
|
||||
}
|
||||
}
|
||||
3
core/src/scss/components/_VNavigationDrawer.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
.v-navigation-drawer__scrim.fade-transition-leave-to {
|
||||
display: none;
|
||||
}
|
||||
20
core/src/scss/components/_VShadow.scss
Normal file
@@ -0,0 +1,20 @@
|
||||
.elevation-24 {
|
||||
box-shadow: $box-shadow !important;
|
||||
}
|
||||
|
||||
.v-menu {
|
||||
> .v-overlay__content {
|
||||
> .v-sheet {
|
||||
box-shadow: $box-shadow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@each $color, $value in $theme-colors {
|
||||
.#{$color}-shadow {
|
||||
box-shadow: 0 14px 12px rgba(#{$value}, 0.2);
|
||||
&:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
core/src/scss/components/_VTextField.scss
Normal file
@@ -0,0 +1,18 @@
|
||||
.v-text-field input {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.v-field__outline {
|
||||
color: rgb(var(--v-theme-inputBorder));
|
||||
}
|
||||
.inputWithbg {
|
||||
.v-field--variant-outlined {
|
||||
background-color: rgba(0, 0, 0, 0.025);
|
||||
}
|
||||
}
|
||||
|
||||
.v-select {
|
||||
.v-field {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
7
core/src/scss/components/_VTextarea.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
.v-textarea input {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
&::placeholder {
|
||||
color: rgba(0, 0, 0, 0.38);
|
||||
}
|
||||
}
|
||||
146
core/src/scss/layout/_container.scss
Normal file
@@ -0,0 +1,146 @@
|
||||
html {
|
||||
overflow-y: auto;
|
||||
}
|
||||
.horizontalLayout {
|
||||
.page-wrapper {
|
||||
.v-container {
|
||||
padding-top: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.spacer {
|
||||
padding: 100px 0;
|
||||
@media (max-width: 1264px) {
|
||||
padding: 72px 0;
|
||||
}
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
.spacer {
|
||||
padding: 40px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
.page-wrapper {
|
||||
background: rgb(var(--v-theme-containerBg));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 60px);
|
||||
overflow: hidden;
|
||||
|
||||
.page-content-container {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 15px;
|
||||
@media (max-width: 1550px) {
|
||||
max-width: 100%;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
padding-inline: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-footer-container {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.v-container {
|
||||
padding: 15px;
|
||||
@media (max-width: 1550px) {
|
||||
max-width: 100%;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
padding-inline: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.maxWidth {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
$sizes: (
|
||||
'display-1': 44px,
|
||||
'display-2': 40px,
|
||||
'display-3': 30px,
|
||||
'h1': 36px,
|
||||
'h2': 30px,
|
||||
'h3': 21px,
|
||||
'h4': 18px,
|
||||
'h5': 16px,
|
||||
'h6': 14px,
|
||||
'text-8': 8px,
|
||||
'text-10': 10px,
|
||||
'text-13': 13px,
|
||||
'text-18': 18px,
|
||||
'text-20': 20px,
|
||||
'text-24': 24px,
|
||||
'body-text-1': 10px
|
||||
);
|
||||
|
||||
@each $pixel, $size in $sizes {
|
||||
.#{$pixel} {
|
||||
font-size: $size;
|
||||
line-height: $size + 10;
|
||||
}
|
||||
}
|
||||
|
||||
.customizer-btn {
|
||||
.icon {
|
||||
animation: progress-circular-rotate 1.4s linear infinite;
|
||||
transform-origin: center center;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
.fixed-width {
|
||||
max-width: 1300px;
|
||||
}
|
||||
.ga-2 {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
// font family
|
||||
body {
|
||||
font-family: 'Public Sans', sans-serif;
|
||||
.Roboto {
|
||||
font-family: 'Roboto', sans-serif !important;
|
||||
}
|
||||
|
||||
.Poppins {
|
||||
font-family: 'Poppins', sans-serif !important;
|
||||
}
|
||||
|
||||
.Inter {
|
||||
font-family: 'Inter', sans-serif !important;
|
||||
}
|
||||
|
||||
.Public {
|
||||
font-family: 'Public sans', sans-serif !important;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideY {
|
||||
0%,
|
||||
50%,
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
25% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
75% {
|
||||
transform: translateY(10px);
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
color: rgb(var(--v-theme-lightText));
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
}
|
||||
25
core/src/scss/layout/_footer.scss
Normal file
@@ -0,0 +1,25 @@
|
||||
.v-footer {
|
||||
background: rgb(var(--v-theme-containerbg));
|
||||
padding: 24px 16px 0px;
|
||||
margin-top: auto;
|
||||
position: unset;
|
||||
a {
|
||||
text-decoration: unset;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 475px) {
|
||||
.footer {
|
||||
text-align: center;
|
||||
.v-col-6 {
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
&.text-right {
|
||||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
165
core/src/scss/layout/_sidebar.scss
Normal file
@@ -0,0 +1,165 @@
|
||||
/*This is for the logo*/
|
||||
.leftSidebar {
|
||||
border: 0px;
|
||||
box-shadow: none !important;
|
||||
border-right: 1px solid rgba(var(--v-theme-borderLight), 0.8);
|
||||
.logo {
|
||||
padding-left: 7px;
|
||||
}
|
||||
}
|
||||
/*This is for the Vertical sidebar*/
|
||||
.scrollnavbar {
|
||||
height: calc(100vh - 110px);
|
||||
.smallCap {
|
||||
padding: 0px 0 0 20px !important;
|
||||
}
|
||||
.v-list {
|
||||
color: rgb(var(--v-theme-lightText));
|
||||
padding: 0;
|
||||
.v-list-item--one-line {
|
||||
&.v-list-item--active {
|
||||
border-right: 2px solid rgb(var(--v-theme-primary));
|
||||
}
|
||||
}
|
||||
.v-list-group {
|
||||
.v-list-item--one-line {
|
||||
&.v-list-item--active.v-list-item--link {
|
||||
border-right: 2px solid rgb(var(--v-theme-primary));
|
||||
}
|
||||
&.v-list-item--active.v-list-group__header {
|
||||
border-right: none;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
.v-list-group__items {
|
||||
.v-list-item--link,
|
||||
.v-list-item {
|
||||
.v-list-item__prepend {
|
||||
margin-inline-end: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.v-list-item--variant-plain,
|
||||
.v-list-item--variant-outlined,
|
||||
.v-list-item--variant-text,
|
||||
.v-list-item--variant-tonal {
|
||||
color: rgb(var(--v-theme-darkText));
|
||||
}
|
||||
}
|
||||
/*General Menu css*/
|
||||
.v-list-group__items .v-list-item,
|
||||
.v-list-item {
|
||||
border-radius: 0;
|
||||
padding-inline-start: calc(20px + var(--indent-padding) / 2) !important;
|
||||
.v-list-item__prepend {
|
||||
margin-inline-end: 13px;
|
||||
}
|
||||
.v-list-item__append {
|
||||
font-size: 0.875rem;
|
||||
.v-icon {
|
||||
margin-inline-start: 13px;
|
||||
}
|
||||
> .v-icon {
|
||||
--v-medium-emphasis-opacity: 0.8;
|
||||
}
|
||||
}
|
||||
.v-list-item-title {
|
||||
font-size: 0.875rem;
|
||||
color: rgb(var(--v-theme-darkText));
|
||||
}
|
||||
&.v-list-item--active {
|
||||
.v-list-item-title {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
}
|
||||
}
|
||||
/*This is for the dropdown*/
|
||||
.v-list {
|
||||
.v-list-item--active {
|
||||
.v-list-item-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
.sidebarchip .v-icon {
|
||||
margin-inline-start: -3px;
|
||||
}
|
||||
.v-list-group {
|
||||
.v-list-item:focus-visible > .v-list-item__overlay {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
> .v-list-group {
|
||||
position: relative;
|
||||
> .v-list-item--active,
|
||||
> .v-list-item:hover {
|
||||
background: rgb(var(--v-theme-primary), 0.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.v-navigation-drawer--rail {
|
||||
.scrollnavbar .v-list .v-list-group__items,
|
||||
.hide-menu {
|
||||
opacity: 0;
|
||||
}
|
||||
.scrollnavbar {
|
||||
.v-list-item {
|
||||
.v-list-item__prepend {
|
||||
margin-left: 8px;
|
||||
.anticon {
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.v-list-group__items .v-list-item,
|
||||
.v-list-item {
|
||||
padding-inline-start: calc(12px + var(--indent-padding) / 2) !important;
|
||||
}
|
||||
.ExtraBox {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.sidebar-user {
|
||||
margin-left: -6px;
|
||||
}
|
||||
.leftPadding {
|
||||
margin-left: 0px;
|
||||
}
|
||||
&.leftSidebar {
|
||||
.v-list-subheader {
|
||||
display: none;
|
||||
}
|
||||
.v-navigation-drawer__content {
|
||||
.pa-5 {
|
||||
padding-left: 10px !important;
|
||||
.logo {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 1170px) {
|
||||
.mini-sidebar {
|
||||
.logo {
|
||||
width: 40px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.leftSidebar:hover {
|
||||
box-shadow: $box-shadow !important;
|
||||
}
|
||||
.v-navigation-drawer--expand-on-hover:hover {
|
||||
.logo {
|
||||
width: 100%;
|
||||
}
|
||||
.v-list .v-list-group__items,
|
||||
.hide-menu {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
41
core/src/scss/layout/_topbar.scss
Normal file
@@ -0,0 +1,41 @@
|
||||
.profileBtn {
|
||||
height: 44px !important;
|
||||
margin: 0 20px 0 10px !important;
|
||||
padding: 0 6px;
|
||||
.v-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.profileBtn {
|
||||
min-width: 42px;
|
||||
margin: 0 12px 0 0 !important;
|
||||
.v-avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 460px) {
|
||||
.notification-dropdown {
|
||||
width: 332px !important;
|
||||
.v-list-item__content {
|
||||
.d-inline-flex {
|
||||
.text-caption {
|
||||
min-width: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
core/src/scss/style.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
@import './variables';
|
||||
@import 'vuetify/styles/main.sass';
|
||||
@import './override';
|
||||
@import './layout/container';
|
||||
@import './layout/sidebar';
|
||||
@import './layout/footer';
|
||||
@import './layout/topbar';
|
||||
|
||||
@import 'vue3-perfect-scrollbar/style.css';
|
||||
100
core/src/services/authenticationService.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import type { ChallengeResponse, IdentifyResponse, RedirectResponse, SessionStatus, StartResponse, VerifyResponse } from '@KTXC/types/authenticationTypes';
|
||||
import { fetchWrapper } from '@KTXC/utils/helpers/fetch-wrapper';
|
||||
|
||||
export const authenticationService = {
|
||||
/**
|
||||
* Initialize authentication - get session and available methods
|
||||
*/
|
||||
async start(): Promise<StartResponse> {
|
||||
return fetchWrapper.get('/auth/start', undefined, { skipLogoutOnError: true });
|
||||
},
|
||||
|
||||
/**
|
||||
* Identify user - stores identity in session for identity-first flow
|
||||
* Returns tenant-wide methods (no user-specific filtering to prevent enumeration)
|
||||
*
|
||||
* @param session - Session ID from start
|
||||
* @param identity - User identity (email/username)
|
||||
*/
|
||||
async identify(session: string, identity: string): Promise<IdentifyResponse> {
|
||||
return fetchWrapper.post('/auth/identify', {
|
||||
session,
|
||||
identity,
|
||||
}, { skipLogoutOnError: true });
|
||||
},
|
||||
|
||||
/**
|
||||
* Verify a factor (primary or secondary)
|
||||
*
|
||||
* @param session - Session ID from init
|
||||
* @param method - Provider/method ID (e.g., 'default', 'totp')
|
||||
* @param response - User's response (password, code, etc.)
|
||||
* @param identity - User identity for credential-based auth (email/username)
|
||||
*/
|
||||
async verify(
|
||||
session: string,
|
||||
method: string,
|
||||
response: string,
|
||||
identity?: string
|
||||
): Promise<VerifyResponse> {
|
||||
return fetchWrapper.post('/auth/verify', {
|
||||
session,
|
||||
method,
|
||||
response,
|
||||
...(identity && { identity }),
|
||||
}, { autoRetry: false, skipLogoutOnError: true });
|
||||
},
|
||||
|
||||
/**
|
||||
* Begin redirect-based authentication (OIDC/SAML)
|
||||
*/
|
||||
async beginRedirect(
|
||||
session: string,
|
||||
method: string,
|
||||
returnUrl: string = '/'
|
||||
): Promise<RedirectResponse> {
|
||||
return fetchWrapper.post('/auth/redirect', {
|
||||
session,
|
||||
method,
|
||||
return_url: returnUrl,
|
||||
}, { skipLogoutOnError: true });
|
||||
},
|
||||
|
||||
/**
|
||||
* Start a challenge for methods that require it (SMS, email, TOTP)
|
||||
*/
|
||||
async beginChallenge(session: string, method: string): Promise<ChallengeResponse> {
|
||||
return fetchWrapper.post('/auth/challenge', {
|
||||
session,
|
||||
method,
|
||||
}, { skipLogoutOnError: true });
|
||||
},
|
||||
|
||||
/**
|
||||
* Get current session status
|
||||
*/
|
||||
async getStatus(session: string): Promise<SessionStatus> {
|
||||
return fetchWrapper.get(`/auth/status?session=${encodeURIComponent(session)}`, undefined, { skipLogoutOnError: true });
|
||||
},
|
||||
|
||||
/**
|
||||
* Cancel authentication session
|
||||
*/
|
||||
async cancelSession(session: string): Promise<void> {
|
||||
await fetchWrapper.delete(`/auth/session?session=${encodeURIComponent(session)}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Refresh access token
|
||||
*/
|
||||
async refresh(): Promise<void> {
|
||||
await fetchWrapper.post('/auth/refresh', {});
|
||||
},
|
||||
|
||||
/**
|
||||
* Logout
|
||||
*/
|
||||
async logout(): Promise<void> {
|
||||
await fetchWrapper.post('/auth/logout', {});
|
||||
},
|
||||
};
|
||||
1
core/src/services/user/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { userService } from './userService';
|
||||
153
core/src/services/user/userService.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { fetchWrapper } from '@KTXC/utils/helpers/fetch-wrapper';
|
||||
|
||||
/**
|
||||
* User Service
|
||||
* Provides methods for managing user profile and settings with batched updates
|
||||
*/
|
||||
|
||||
// Pending updates for profile and settings
|
||||
let pendingProfileUpdates: Record<string, any> = {};
|
||||
let pendingSettingsUpdates: Record<string, any> = {};
|
||||
let profileUpdateTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let settingsUpdateTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// Default debounce delay in milliseconds
|
||||
const DEBOUNCE_DELAY = 500;
|
||||
|
||||
export const userService = {
|
||||
/**
|
||||
* Update profile field(s) with debouncing
|
||||
* Multiple calls within the debounce window will be batched into a single request
|
||||
*/
|
||||
updateProfile(fields: Record<string, any>): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Merge new fields with pending updates
|
||||
Object.assign(pendingProfileUpdates, fields);
|
||||
|
||||
// Clear existing timer
|
||||
if (profileUpdateTimer) {
|
||||
clearTimeout(profileUpdateTimer);
|
||||
}
|
||||
|
||||
// Set new timer to batch updates
|
||||
profileUpdateTimer = setTimeout(async () => {
|
||||
const updates = { ...pendingProfileUpdates };
|
||||
pendingProfileUpdates = {};
|
||||
profileUpdateTimer = null;
|
||||
|
||||
try {
|
||||
await fetchWrapper.put('/user/profile', { data: updates });
|
||||
resolve();
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
}, DEBOUNCE_DELAY);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Update profile field(s) immediately without debouncing
|
||||
*/
|
||||
async updateProfileImmediate(fields: Record<string, any>): Promise<void> {
|
||||
// Cancel pending debounced update
|
||||
if (profileUpdateTimer) {
|
||||
clearTimeout(profileUpdateTimer);
|
||||
profileUpdateTimer = null;
|
||||
}
|
||||
|
||||
// Merge with pending updates and send immediately
|
||||
const updates = { ...pendingProfileUpdates, ...fields };
|
||||
pendingProfileUpdates = {};
|
||||
|
||||
return fetchWrapper.put('/user/profile', { data: updates });
|
||||
},
|
||||
|
||||
/**
|
||||
* Flush any pending profile updates immediately
|
||||
*/
|
||||
async flushProfileUpdates(): Promise<void> {
|
||||
if (profileUpdateTimer) {
|
||||
clearTimeout(profileUpdateTimer);
|
||||
profileUpdateTimer = null;
|
||||
}
|
||||
|
||||
if (Object.keys(pendingProfileUpdates).length > 0) {
|
||||
const updates = { ...pendingProfileUpdates };
|
||||
pendingProfileUpdates = {};
|
||||
return fetchWrapper.put('/user/profile', { data: updates });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update setting(s) with debouncing
|
||||
* Multiple calls within the debounce window will be batched into a single request
|
||||
*/
|
||||
updateSettings(settings: Record<string, any>): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Merge new settings with pending updates
|
||||
Object.assign(pendingSettingsUpdates, settings);
|
||||
|
||||
// Clear existing timer
|
||||
if (settingsUpdateTimer) {
|
||||
clearTimeout(settingsUpdateTimer);
|
||||
}
|
||||
|
||||
// Set new timer to batch updates
|
||||
settingsUpdateTimer = setTimeout(async () => {
|
||||
const updates = { ...pendingSettingsUpdates };
|
||||
pendingSettingsUpdates = {};
|
||||
settingsUpdateTimer = null;
|
||||
|
||||
try {
|
||||
await fetchWrapper.put('/user/settings', { data: updates });
|
||||
resolve();
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
}, DEBOUNCE_DELAY);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Update setting(s) immediately without debouncing
|
||||
*/
|
||||
async updateSettingsImmediate(settings: Record<string, any>): Promise<void> {
|
||||
// Cancel pending debounced update
|
||||
if (settingsUpdateTimer) {
|
||||
clearTimeout(settingsUpdateTimer);
|
||||
settingsUpdateTimer = null;
|
||||
}
|
||||
|
||||
// Merge with pending updates and send immediately
|
||||
const updates = { ...pendingSettingsUpdates, ...settings };
|
||||
pendingSettingsUpdates = {};
|
||||
|
||||
return fetchWrapper.put('/user/settings', { data: updates });
|
||||
},
|
||||
|
||||
/**
|
||||
* Flush any pending settings updates immediately
|
||||
*/
|
||||
async flushSettingsUpdates(): Promise<void> {
|
||||
if (settingsUpdateTimer) {
|
||||
clearTimeout(settingsUpdateTimer);
|
||||
settingsUpdateTimer = null;
|
||||
}
|
||||
|
||||
if (Object.keys(pendingSettingsUpdates).length > 0) {
|
||||
const updates = { ...pendingSettingsUpdates };
|
||||
pendingSettingsUpdates = {};
|
||||
return fetchWrapper.put('/user/settings', { data: updates });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Flush all pending updates (profile and settings)
|
||||
*/
|
||||
async flushAll(): Promise<void> {
|
||||
await Promise.all([
|
||||
this.flushProfileUpdates(),
|
||||
this.flushSettingsUpdates(),
|
||||
]);
|
||||
},
|
||||
};
|
||||
5
core/src/shims-vue.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue';
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
183
core/src/stores/integrationStore.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import type {
|
||||
IntegrationPointType,
|
||||
IntegrationPoint,
|
||||
IntegrationEntry,
|
||||
IntegrationItem,
|
||||
IntegrationGroup
|
||||
} from '@KTXC/types/integrationTypes';
|
||||
|
||||
export const useIntegrationStore = defineStore('integrationStore', {
|
||||
state: () => ({
|
||||
points: new Map<IntegrationPointType, IntegrationPoint>(),
|
||||
}),
|
||||
|
||||
actions: {
|
||||
// Ensure an integration point exists
|
||||
ensurePoint(pointType: IntegrationPointType): void {
|
||||
if (!this.points.has(pointType)) {
|
||||
this.points.set(pointType, { items: new Map() });
|
||||
}
|
||||
},
|
||||
|
||||
// Register a single item to an integration point
|
||||
registerItem(pointType: IntegrationPointType, item: IntegrationItem): void {
|
||||
this.ensurePoint(pointType);
|
||||
const point = this.points.get(pointType)!;
|
||||
point.items.set(item.id, item);
|
||||
},
|
||||
|
||||
// Register a group to an integration point
|
||||
registerGroup(pointType: IntegrationPointType, group: IntegrationGroup): void {
|
||||
this.ensurePoint(pointType);
|
||||
const point = this.points.get(pointType)!;
|
||||
point.items.set(group.id, group);
|
||||
},
|
||||
|
||||
// Bulk register from module integrations
|
||||
registerModuleIntegrations(
|
||||
moduleHandle: string,
|
||||
integrations: Record<string, any[]>
|
||||
): void {
|
||||
for (const [pointType, entries] of Object.entries(integrations)) {
|
||||
if (!entries || !Array.isArray(entries)) continue;
|
||||
|
||||
entries.forEach((entry: any) => {
|
||||
const prefixedEntry = this.prefixEntry(moduleHandle, entry);
|
||||
|
||||
if (entry.type === 'group' || ('items' in entry && Array.isArray(entry.items))) {
|
||||
this.registerGroup(pointType as IntegrationPointType, prefixedEntry as IntegrationGroup);
|
||||
} else {
|
||||
this.registerItem(pointType as IntegrationPointType, prefixedEntry as IntegrationItem);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Prefix IDs and paths with module handle
|
||||
prefixEntry(moduleHandle: string, entry: any): IntegrationEntry {
|
||||
const prefixed: any = {
|
||||
...entry,
|
||||
id: entry.id ? `${moduleHandle}.${entry.id}` : `${moduleHandle}.${this.randomID()}`,
|
||||
moduleHandle,
|
||||
};
|
||||
|
||||
// Remove 'type' field as it's only used for module-side disambiguation
|
||||
delete prefixed.type;
|
||||
|
||||
// Prefix internal paths
|
||||
if (entry.path) {
|
||||
prefixed.to = `/m/${moduleHandle}${entry.path}`;
|
||||
delete prefixed.path;
|
||||
} else if (entry.to && entry.toType !== 'external') {
|
||||
prefixed.to = `/m/${moduleHandle}${entry.to}`;
|
||||
}
|
||||
|
||||
// Recursively prefix items in groups
|
||||
if (entry.items && Array.isArray(entry.items)) {
|
||||
prefixed.items = entry.items.map((item: any) => this.prefixEntry(moduleHandle, item));
|
||||
}
|
||||
|
||||
return prefixed;
|
||||
},
|
||||
|
||||
// Update a specific item (useful for badges, visibility, etc.)
|
||||
updateItem(
|
||||
pointType: IntegrationPointType,
|
||||
itemId: string,
|
||||
updates: Partial<IntegrationItem>
|
||||
): void {
|
||||
const point = this.points.get(pointType);
|
||||
if (!point) return;
|
||||
|
||||
const item = point.items.get(itemId);
|
||||
if (item && !('items' in item)) {
|
||||
point.items.set(itemId, { ...item, ...updates });
|
||||
}
|
||||
},
|
||||
|
||||
// Update badge for notification icons
|
||||
updateBadge(
|
||||
pointType: IntegrationPointType,
|
||||
itemId: string,
|
||||
badge: string | number | null,
|
||||
badgeColor?: string
|
||||
): void {
|
||||
this.updateItem(pointType, itemId, { badge, badgeColor });
|
||||
},
|
||||
|
||||
// Toggle visibility
|
||||
setVisibility(
|
||||
pointType: IntegrationPointType,
|
||||
itemId: string,
|
||||
visible: boolean
|
||||
): void {
|
||||
this.updateItem(pointType, itemId, { visible });
|
||||
},
|
||||
|
||||
// Remove an item
|
||||
unregisterItem(pointType: IntegrationPointType, itemId: string): void {
|
||||
const point = this.points.get(pointType);
|
||||
if (point) {
|
||||
point.items.delete(itemId);
|
||||
}
|
||||
},
|
||||
|
||||
// Remove all items from a module
|
||||
unregisterModule(moduleHandle: string): void {
|
||||
this.points.forEach((point) => {
|
||||
point.items.forEach((entry, id) => {
|
||||
if (entry.moduleHandle === moduleHandle) {
|
||||
point.items.delete(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
reset(): void {
|
||||
this.points.clear();
|
||||
},
|
||||
|
||||
randomID(length = 8): string {
|
||||
return Math.random().toString(36).substring(2, 2 + length);
|
||||
},
|
||||
},
|
||||
|
||||
getters: {
|
||||
// Get all entries for an integration point, sorted by priority
|
||||
getPoint: (state) => (pointType: IntegrationPointType): IntegrationEntry[] => {
|
||||
const point = state.points.get(pointType);
|
||||
if (!point) return [];
|
||||
|
||||
return Array.from(point.items.values())
|
||||
.filter(entry => entry.visible !== false)
|
||||
.sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100));
|
||||
},
|
||||
|
||||
// Get items only (no groups)
|
||||
getItems: (state) => (pointType: IntegrationPointType): IntegrationItem[] => {
|
||||
const point = state.points.get(pointType);
|
||||
if (!point) return [];
|
||||
|
||||
return Array.from(point.items.values())
|
||||
.filter((entry): entry is IntegrationItem => !('items' in entry) && entry.visible !== false)
|
||||
.sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100));
|
||||
},
|
||||
|
||||
// Get groups only
|
||||
getGroups: (state) => (pointType: IntegrationPointType): IntegrationGroup[] => {
|
||||
const point = state.points.get(pointType);
|
||||
if (!point) return [];
|
||||
|
||||
return Array.from(point.items.values())
|
||||
.filter((entry): entry is IntegrationGroup => 'items' in entry && entry.visible !== false)
|
||||
.sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100));
|
||||
},
|
||||
|
||||
// Get a specific item by ID
|
||||
getItemById: (state) => (pointType: IntegrationPointType, itemId: string): IntegrationEntry | undefined => {
|
||||
const point = state.points.get(pointType);
|
||||
return point?.items.get(itemId);
|
||||
},
|
||||
},
|
||||
});
|
||||
85
core/src/stores/layoutStore.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { ref, watch } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import config from '@KTXC/config';
|
||||
import { useUserStore } from './userStore';
|
||||
|
||||
export type MenuMode = 'apps' | 'user-settings' | 'admin-settings';
|
||||
|
||||
export const useLayoutStore = defineStore('layout', () => {
|
||||
// Loading state
|
||||
const isLoading = ref(false);
|
||||
|
||||
// Sidebar state - initialize from settings or config
|
||||
const userStore = useUserStore();
|
||||
const sidebarDrawer = ref(userStore.getSetting('sidebar_drawer') ?? config.Sidebar_drawer);
|
||||
const miniSidebar = ref(userStore.getSetting('mini_sidebar') ?? config.mini_sidebar);
|
||||
const menuMode = ref<MenuMode>('apps');
|
||||
|
||||
// Theme state - initialize from settings or config
|
||||
const theme = ref(userStore.getSetting('theme') ?? config.actTheme);
|
||||
const font = ref(userStore.getSetting('font') ?? config.fontTheme);
|
||||
|
||||
// Watch and sync sidebar state to settings
|
||||
watch(sidebarDrawer, (value) => {
|
||||
userStore.setSetting('sidebar_drawer', value);
|
||||
});
|
||||
|
||||
watch(miniSidebar, (value) => {
|
||||
userStore.setSetting('mini_sidebar', value);
|
||||
});
|
||||
|
||||
watch(theme, (value) => {
|
||||
userStore.setSetting('theme', value);
|
||||
});
|
||||
|
||||
watch(font, (value) => {
|
||||
userStore.setSetting('font', value);
|
||||
});
|
||||
|
||||
// Actions
|
||||
function toggleSidebarDrawer() {
|
||||
sidebarDrawer.value = !sidebarDrawer.value;
|
||||
}
|
||||
|
||||
function setMiniSidebar(value: boolean) {
|
||||
miniSidebar.value = value;
|
||||
}
|
||||
|
||||
function setTheme(value: string) {
|
||||
theme.value = value;
|
||||
}
|
||||
|
||||
function setFont(value: string) {
|
||||
font.value = value;
|
||||
}
|
||||
|
||||
function setMenuMode(value: MenuMode) {
|
||||
menuMode.value = value;
|
||||
}
|
||||
|
||||
function toggleMenuMode() {
|
||||
// Cycle through: apps -> user-settings -> admin-settings -> apps
|
||||
const modes: MenuMode[] = ['apps', 'user-settings', 'admin-settings'];
|
||||
const currentIndex = modes.indexOf(menuMode.value);
|
||||
const nextIndex = (currentIndex + 1) % modes.length;
|
||||
menuMode.value = modes[nextIndex];
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
isLoading,
|
||||
sidebarDrawer,
|
||||
miniSidebar,
|
||||
menuMode,
|
||||
theme,
|
||||
font,
|
||||
|
||||
// Actions
|
||||
toggleSidebarDrawer,
|
||||
setMiniSidebar,
|
||||
setMenuMode,
|
||||
toggleMenuMode,
|
||||
setTheme,
|
||||
setFont
|
||||
};
|
||||
});
|
||||
34
core/src/stores/moduleStore.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import type { ModuleCollection, ModuleObject } from '@KTXC/types/moduleTypes';
|
||||
|
||||
export const useModuleStore = defineStore('moduleStore', {
|
||||
state: () => ({
|
||||
modules: {} as ModuleCollection,
|
||||
}),
|
||||
actions: {
|
||||
init(data: ModuleCollection) {
|
||||
this.modules = data ?? {};
|
||||
},
|
||||
markBooted(ns: string) {
|
||||
const targetNs = String(ns).toLowerCase();
|
||||
Object.keys(this.modules).forEach((key) => {
|
||||
const mod = this.modules[key] as ModuleObject;
|
||||
if (!mod) return;
|
||||
if (String(mod.namespace || mod.handle).toLowerCase() === targetNs) {
|
||||
mod.booted = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
reset() {
|
||||
this.modules = {};
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
has: (state) => (handleOrNamespace: string) => {
|
||||
const target = String(handleOrNamespace).toLowerCase();
|
||||
return Object.values(state.modules).some(
|
||||
(mod) => mod && (String(mod.handle).toLowerCase() === target || String(mod.namespace).toLowerCase() === target)
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
27
core/src/stores/tenantStore.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
export interface TenantState {
|
||||
id: string | null;
|
||||
domain: string | null;
|
||||
label: string | null;
|
||||
}
|
||||
|
||||
export const useTenantStore = defineStore('tenantStore', {
|
||||
state: () => ({
|
||||
tenant: null as TenantState | null,
|
||||
}),
|
||||
actions: {
|
||||
init(tenant: Partial<TenantState> | null) {
|
||||
this.tenant = tenant
|
||||
? {
|
||||
id: tenant.id ?? null,
|
||||
domain: tenant.domain ?? null,
|
||||
label: tenant.label ?? null,
|
||||
}
|
||||
: null;
|
||||
},
|
||||
reset() {
|
||||
this.tenant = null;
|
||||
},
|
||||
},
|
||||
});
|
||||
275
core/src/stores/userStore.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, computed } from 'vue';
|
||||
import { router } from '@KTXC/router';
|
||||
import { authenticationService } from '@KTXC/services/authenticationService';
|
||||
import { userService } from '@KTXC/services/user/userService';
|
||||
import type { AuthenticatedUser } from '@KTXC/types/authenticationTypes';
|
||||
import type { UserProfileInterface } from '@KTXC/types/user/userProfileTypes';
|
||||
import type { UserSettingsInterface } from '@KTXC/types/user/userSettingsTypes';
|
||||
import { UserProfile } from '@KTXC/models/userProfile';
|
||||
import { UserSettings } from '@KTXC/models/userSettings';
|
||||
|
||||
const STORAGE_KEY = 'userStore.auth';
|
||||
|
||||
// Flush pending updates before page unload
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('beforeunload', () => {
|
||||
userService.flushAll();
|
||||
});
|
||||
}
|
||||
|
||||
export const useUserStore = defineStore('userStore', () => {
|
||||
// =========================================================================
|
||||
// State
|
||||
// =========================================================================
|
||||
|
||||
const auth = ref<AuthenticatedUser | null>(
|
||||
localStorage.getItem(STORAGE_KEY)
|
||||
? (JSON.parse(localStorage.getItem(STORAGE_KEY)!) as AuthenticatedUser)
|
||||
: null
|
||||
);
|
||||
|
||||
const profile = ref<UserProfile>(new UserProfile());
|
||||
const settings = ref<UserSettings>(new UserSettings());
|
||||
const returnUrl = ref<string | null>(null);
|
||||
|
||||
// =========================================================================
|
||||
// Authentication Getters
|
||||
// =========================================================================
|
||||
|
||||
const isAuthenticated = computed(() => auth.value !== null);
|
||||
const identifier = computed(() => auth.value?.identifier ?? null);
|
||||
const identity = computed(() => auth.value?.identity ?? null);
|
||||
const label = computed(() => auth.value?.label ?? null);
|
||||
const roles = computed(() => auth.value?.roles ?? []);
|
||||
const permissions = computed(() => auth.value?.permissions ?? []);
|
||||
|
||||
// =========================================================================
|
||||
// Profile Getters
|
||||
// =========================================================================
|
||||
|
||||
const profileFields = computed(() => profile.value.fields);
|
||||
|
||||
const editableProfileFields = computed(() => profile.value.editableFields);
|
||||
|
||||
const managedProfileFields = computed(() => profile.value.managedFields);
|
||||
|
||||
// =========================================================================
|
||||
// Authentication Actions
|
||||
// =========================================================================
|
||||
|
||||
function setAuth(authUser: AuthenticatedUser): void {
|
||||
auth.value = authUser;
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(authUser));
|
||||
}
|
||||
|
||||
function clearAuth(): void {
|
||||
auth.value = null;
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
|
||||
async function logout(): Promise<void> {
|
||||
try {
|
||||
// Flush any pending profile/settings updates before logout
|
||||
await userService.flushAll();
|
||||
|
||||
await authenticationService.logout();
|
||||
} catch (error) {
|
||||
console.warn('Logout request failed, clearing local state:', error);
|
||||
} finally {
|
||||
clearAuth();
|
||||
clearProfile();
|
||||
clearSettings();
|
||||
router.push('/login');
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshToken(): Promise<boolean> {
|
||||
try {
|
||||
await authenticationService.refresh();
|
||||
return true;
|
||||
} catch (error) {
|
||||
await logout();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Profile Actions
|
||||
// =========================================================================
|
||||
|
||||
function initProfile(profileData: UserProfileInterface): void {
|
||||
profile.value = new UserProfile(profileData);
|
||||
}
|
||||
|
||||
function clearProfile(): void {
|
||||
profile.value = new UserProfile();
|
||||
}
|
||||
|
||||
function getProfileField(key: string): any {
|
||||
return profile.value.get(key);
|
||||
}
|
||||
|
||||
function setProfileField(key: string, value: any): boolean {
|
||||
const success = profile.value.set(key, value);
|
||||
if (success) {
|
||||
// Debounced update to backend
|
||||
userService.updateProfile({ [key]: value }).catch(error => {
|
||||
console.error('Failed to update profile:', error);
|
||||
});
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
function isProfileFieldEditable(key: string): boolean {
|
||||
return profile.value.isEditable(key);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Settings Actions
|
||||
// =========================================================================
|
||||
|
||||
function initSettings(settingsData: UserSettingsInterface): void {
|
||||
settings.value = new UserSettings(settingsData);
|
||||
}
|
||||
|
||||
function clearSettings(): void {
|
||||
settings.value = new UserSettings();
|
||||
}
|
||||
|
||||
function getSetting(key: string): any {
|
||||
return settings.value.get(key);
|
||||
}
|
||||
|
||||
function setSetting(key: string, value: any): void {
|
||||
settings.value.set(key, value);
|
||||
// Debounced update to backend
|
||||
userService.updateSettings({ [key]: value }).catch(error => {
|
||||
console.error('Failed to update setting:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Permission Checking
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Check if user has a specific permission
|
||||
* Supports wildcards: user_manager.users.* matches all user actions
|
||||
*/
|
||||
function hasPermission(permission: string): boolean {
|
||||
const userPermissions = permissions.value;
|
||||
|
||||
// Exact match
|
||||
if (userPermissions.includes(permission)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Wildcard match
|
||||
for (const userPerm of userPermissions) {
|
||||
if (userPerm.endsWith('.*')) {
|
||||
const prefix = userPerm.slice(0, -2);
|
||||
if (permission.startsWith(prefix + '.')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Full wildcard
|
||||
if (userPermissions.includes('*')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has ANY of the permissions (OR logic)
|
||||
*/
|
||||
function hasAnyPermission(perms: string[]): boolean {
|
||||
return perms.some(p => hasPermission(p));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has ALL permissions (AND logic)
|
||||
*/
|
||||
function hasAllPermissions(perms: string[]): boolean {
|
||||
return perms.every(p => hasPermission(p));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has a specific role
|
||||
*/
|
||||
function hasRole(role: string): boolean {
|
||||
return roles.value.includes(role);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Initialize from /init endpoint
|
||||
// =========================================================================
|
||||
|
||||
function init(userData: {
|
||||
auth?: AuthenticatedUser;
|
||||
profile?: UserProfileInterface;
|
||||
settings?: UserSettingsInterface
|
||||
}): void {
|
||||
if (userData.auth) {
|
||||
setAuth(userData.auth);
|
||||
}
|
||||
if (userData.profile) {
|
||||
initProfile(userData.profile);
|
||||
}
|
||||
if (userData.settings) {
|
||||
initSettings(userData.settings);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
auth,
|
||||
profile,
|
||||
settings,
|
||||
returnUrl,
|
||||
|
||||
// Auth getters
|
||||
isAuthenticated,
|
||||
identifier,
|
||||
identity,
|
||||
label,
|
||||
roles,
|
||||
permissions,
|
||||
|
||||
// Profile getters
|
||||
profileFields,
|
||||
editableProfileFields,
|
||||
managedProfileFields,
|
||||
|
||||
// Auth actions
|
||||
setAuth,
|
||||
clearAuth,
|
||||
logout,
|
||||
refreshToken,
|
||||
|
||||
// Profile actions
|
||||
initProfile,
|
||||
clearProfile,
|
||||
getProfileField,
|
||||
setProfileField,
|
||||
isProfileFieldEditable,
|
||||
|
||||
// Settings actions
|
||||
initSettings,
|
||||
clearSettings,
|
||||
getSetting,
|
||||
setSetting,
|
||||
|
||||
// Permission actions
|
||||
hasPermission,
|
||||
hasAnyPermission,
|
||||
hasAllPermissions,
|
||||
hasRole,
|
||||
|
||||
// Init
|
||||
init,
|
||||
};
|
||||
});
|
||||
87
core/src/types/authenticationTypes.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Authentication Method from provider
|
||||
*/
|
||||
export interface AuthenticationMethod {
|
||||
id: string;
|
||||
method: 'credential' | 'redirect' | 'challenge';
|
||||
label: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticated user info
|
||||
*/
|
||||
export interface AuthenticatedUser {
|
||||
identifier: string;
|
||||
identity: string;
|
||||
label: string;
|
||||
roles?: string[];
|
||||
permissions?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Start response from /auth/start
|
||||
*/
|
||||
export interface StartResponse {
|
||||
status: 'success';
|
||||
session: string;
|
||||
methods: AuthenticationMethod[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Identify response from /auth/identify
|
||||
*/
|
||||
export interface IdentifyResponse {
|
||||
status: 'success';
|
||||
session: string;
|
||||
state: string;
|
||||
methods: AuthenticationMethod[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify response from /auth/verify
|
||||
*/
|
||||
export interface VerifyResponse {
|
||||
status: 'success' | 'pending';
|
||||
user?: AuthenticatedUser;
|
||||
session?: string;
|
||||
methods?: AuthenticationMethod[];
|
||||
message?: string;
|
||||
error?: string;
|
||||
error_code?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect response from /auth/redirect
|
||||
*/
|
||||
export interface RedirectResponse {
|
||||
status: 'redirect';
|
||||
redirect_url: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Challenge response from /auth/challenge
|
||||
*/
|
||||
export interface ChallengeResponse {
|
||||
status: 'challenge';
|
||||
session: string;
|
||||
challenge?: {
|
||||
type?: string;
|
||||
message?: string;
|
||||
digits?: number;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Session status from /auth/status
|
||||
*/
|
||||
export interface SessionStatus {
|
||||
status: 'success';
|
||||
session: string;
|
||||
state: string;
|
||||
methods: AuthenticationMethod[];
|
||||
user?: {
|
||||
identity?: string;
|
||||
};
|
||||
}
|
||||
1
core/src/types/env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
45
core/src/types/integrationTypes.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// Integration point types - extensible via string
|
||||
export type IntegrationPointType =
|
||||
| 'app_menu' // Main applications menu (calendar, contacts, etc.)
|
||||
| 'admin_settings_menu' // Admin-only settings menu
|
||||
| 'user_settings_menu' // User's personal settings menu
|
||||
| 'profile_menu' // Top-right profile dropdown (quick actions)
|
||||
| string; // Allow custom integration points
|
||||
|
||||
export type IntegrationGroupStyle = 'none' | 'static' | 'dynamic' | null;
|
||||
|
||||
export interface IntegrationItem {
|
||||
id: string;
|
||||
moduleHandle: string;
|
||||
priority?: number;
|
||||
label?: string;
|
||||
caption?: string;
|
||||
icon?: string;
|
||||
to?: string;
|
||||
toType?: 'internal' | 'external';
|
||||
component?: () => Promise<any>;
|
||||
badge?: string | number | null;
|
||||
badgeColor?: string;
|
||||
visible?: boolean;
|
||||
disabled?: boolean;
|
||||
meta?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface IntegrationGroup {
|
||||
id: string;
|
||||
moduleHandle: string;
|
||||
priority?: number;
|
||||
label?: string;
|
||||
caption?: string;
|
||||
icon?: string;
|
||||
style?: IntegrationGroupStyle;
|
||||
items: IntegrationItem[];
|
||||
visible?: boolean;
|
||||
meta?: Record<string, any>;
|
||||
}
|
||||
|
||||
export type IntegrationEntry = IntegrationItem | IntegrationGroup;
|
||||
|
||||
export interface IntegrationPoint {
|
||||
items: Map<string, IntegrationEntry>;
|
||||
}
|
||||
30
core/src/types/layouts/layoutSystemMenu.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export interface LayoutSystemMenuItem {
|
||||
header?: string;
|
||||
title?: string;
|
||||
icon?: object;
|
||||
to?: string | object;
|
||||
href?: string;
|
||||
target?: string;
|
||||
divider?: boolean;
|
||||
children?: LayoutSystemMenuItem[];
|
||||
badgeContent?: string | number;
|
||||
badgeColor?: string;
|
||||
badgeVariant?: string;
|
||||
badgeIcon?: string;
|
||||
chip?: string | number;
|
||||
chipColor?: string;
|
||||
chipVariant?: string;
|
||||
chipIcon?: string;
|
||||
disabled?: boolean;
|
||||
type?: string;
|
||||
subCaption?: string;
|
||||
meta?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export interface LayoutSystemMenuGroup {
|
||||
handle: string;
|
||||
style: string;
|
||||
label: string;
|
||||
icon: object;
|
||||
items: LayoutSystemMenuItem[];
|
||||
}
|
||||
67
core/src/types/moduleTypes.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { IntegrationGroupStyle } from './integrationTypes';
|
||||
|
||||
export interface ModuleObject {
|
||||
handle: string;
|
||||
namespace: string;
|
||||
version: string;
|
||||
label: string;
|
||||
author?: string;
|
||||
description?: string;
|
||||
boot?: string; // relative path like "js/Dashboard.js"
|
||||
booted?: boolean; // set true once the module plugin is loaded
|
||||
}
|
||||
|
||||
export type ModuleCollection = Record<string, ModuleObject>;
|
||||
|
||||
// Module integration types
|
||||
|
||||
/**
|
||||
* Base interface for module integrations
|
||||
*/
|
||||
export interface ModuleIntegrationBase {
|
||||
id: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* UI Navigation integration item
|
||||
* Represents a single clickable entry in a menu or navigation structure
|
||||
*/
|
||||
export interface ModuleIntegrationItem extends ModuleIntegrationBase {
|
||||
type?: 'item';
|
||||
label: string;
|
||||
caption?: string;
|
||||
icon?: string;
|
||||
path?: string;
|
||||
to?: string;
|
||||
toType?: 'internal' | 'external';
|
||||
priority?: number;
|
||||
component?: () => Promise<any>;
|
||||
visible?: boolean;
|
||||
disabled?: boolean;
|
||||
meta?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* UI Navigation integration group
|
||||
* Represents a group of related navigation items, potentially with its own label and icon
|
||||
*/
|
||||
export interface ModuleIntegrationGroup extends ModuleIntegrationBase {
|
||||
type: 'group';
|
||||
label?: string;
|
||||
caption?: string;
|
||||
icon?: string;
|
||||
style?: IntegrationGroupStyle;
|
||||
priority?: number;
|
||||
items: ModuleIntegrationItem[];
|
||||
meta?: Record<string, any>;
|
||||
}
|
||||
|
||||
export type ModuleIntegrationEntry = ModuleIntegrationItem | ModuleIntegrationGroup;
|
||||
|
||||
export interface ModuleIntegrations {
|
||||
system_menu?: ModuleIntegrationEntry[];
|
||||
user_menu?: ModuleIntegrationEntry[];
|
||||
|
||||
[key: string]: ModuleIntegrationEntry[] | ModuleIntegrationBase[] | undefined;
|
||||
}
|
||||
13
core/src/types/user/userProfileTypes.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* User Profile Types
|
||||
*/
|
||||
|
||||
export interface ProfileFieldInterface {
|
||||
value: any;
|
||||
editable: boolean;
|
||||
provider: string | null;
|
||||
}
|
||||
|
||||
export interface UserProfileInterface {
|
||||
[key: string]: ProfileFieldInterface;
|
||||
}
|
||||
7
core/src/types/user/userSettingsTypes.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* User Settings Types
|
||||
*/
|
||||
|
||||
export interface UserSettingsInterface {
|
||||
[key: string]: any;
|
||||
}
|
||||
161
core/src/utils/helpers/fetch-wrapper-core.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Core fetch wrapper - reusable across modules
|
||||
* Does not depend on stores to avoid bundling issues in library builds
|
||||
*/
|
||||
|
||||
export interface FetchWrapperOptions {
|
||||
/**
|
||||
* Optional callback to handle logout on auth failure
|
||||
* If not provided, only logs error without redirecting
|
||||
*/
|
||||
onLogout?: () => void | Promise<void>;
|
||||
/**
|
||||
* Enable automatic retry of failed requests after token refresh
|
||||
* @default true
|
||||
*/
|
||||
autoRetry?: boolean;
|
||||
}
|
||||
|
||||
// Mutex to prevent multiple simultaneous refresh attempts
|
||||
class RefreshMutex {
|
||||
private promise: Promise<boolean> | null = null;
|
||||
|
||||
async acquire(): Promise<boolean> {
|
||||
if (this.promise) {
|
||||
return this.promise;
|
||||
}
|
||||
|
||||
this.promise = this.performRefresh();
|
||||
const result = await this.promise;
|
||||
this.promise = null;
|
||||
return result;
|
||||
}
|
||||
|
||||
private async performRefresh(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch('/security/refresh', {
|
||||
method: 'POST',
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error('Token refresh failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const tokenRefreshMutex = new RefreshMutex();
|
||||
|
||||
export interface RequestCallOptions {
|
||||
/**
|
||||
* Override autoRetry for this specific request
|
||||
* @default true
|
||||
*/
|
||||
autoRetry?: boolean;
|
||||
/**
|
||||
* Skip calling onLogout callback on 401/403 errors
|
||||
* Useful for authentication endpoints where 401 means invalid credentials, not session expiry
|
||||
* @default false
|
||||
*/
|
||||
skipLogoutOnError?: boolean;
|
||||
}
|
||||
|
||||
export function createFetchWrapper(options: FetchWrapperOptions = {}) {
|
||||
const { autoRetry: defaultAutoRetry = true } = options;
|
||||
|
||||
return {
|
||||
get: request('GET', options, defaultAutoRetry),
|
||||
post: request('POST', options, defaultAutoRetry),
|
||||
put: request('PUT', options, defaultAutoRetry),
|
||||
delete: request('DELETE', options, defaultAutoRetry)
|
||||
};
|
||||
}
|
||||
|
||||
interface RequestOptions {
|
||||
method: string;
|
||||
headers: Record<string, string>;
|
||||
body?: string;
|
||||
credentials: 'include';
|
||||
}
|
||||
|
||||
function request(method: string, options: FetchWrapperOptions, defaultAutoRetry: boolean) {
|
||||
return async (url: string, body?: object, callOptions?: RequestCallOptions): Promise<any> => {
|
||||
const autoRetry = callOptions?.autoRetry ?? defaultAutoRetry;
|
||||
|
||||
const requestOptions: RequestOptions = {
|
||||
method,
|
||||
headers: getHeaders(url),
|
||||
credentials: 'include'
|
||||
};
|
||||
|
||||
if (body) {
|
||||
requestOptions.headers['Content-Type'] = 'application/json';
|
||||
requestOptions.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, requestOptions);
|
||||
|
||||
if (response.status === 401 && autoRetry) {
|
||||
// Try to refresh the token
|
||||
const refreshSuccess = await tokenRefreshMutex.acquire();
|
||||
|
||||
if (refreshSuccess) {
|
||||
// Retry the original request with the new token
|
||||
const retryResponse = await fetch(url, requestOptions);
|
||||
return handleResponse(retryResponse, options, callOptions?.skipLogoutOnError);
|
||||
}
|
||||
}
|
||||
|
||||
return handleResponse(response, options, callOptions?.skipLogoutOnError);
|
||||
} catch (error) {
|
||||
console.error('API error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getHeaders(_url: string): Record<string, string> {
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
// Add CSRF token if available
|
||||
const csrfToken = getCsrfTokenFromCookie();
|
||||
if (csrfToken) {
|
||||
headers['X-CSRF-TOKEN'] = csrfToken;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
function getCsrfTokenFromCookie(): string | null {
|
||||
if (typeof document === 'undefined') return null;
|
||||
|
||||
const csrfCookie = document.cookie
|
||||
.split('; ')
|
||||
.find(row => row.startsWith('X-CSRF-TOKEN='));
|
||||
|
||||
return csrfCookie ? csrfCookie.split('=')[1] : null;
|
||||
}
|
||||
|
||||
async function handleResponse(response: Response, options: FetchWrapperOptions, skipLogoutOnError?: boolean): Promise<any> {
|
||||
const text = await response.text();
|
||||
const data = text && JSON.parse(text);
|
||||
|
||||
if (!response.ok) {
|
||||
if ([401, 403].includes(response.status) && !skipLogoutOnError) {
|
||||
// Call logout callback if provided
|
||||
if (options.onLogout) {
|
||||
await options.onLogout();
|
||||
} else {
|
||||
console.error('Authentication failed. Please log in again.');
|
||||
}
|
||||
}
|
||||
|
||||
const error: string = (data && data.message) || response.statusText;
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
10
core/src/utils/helpers/fetch-wrapper.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useUserStore } from '@KTXC/stores/userStore';
|
||||
import { createFetchWrapper } from './fetch-wrapper-core';
|
||||
|
||||
// Create fetch wrapper with user store logout callback
|
||||
export const fetchWrapper = createFetchWrapper({
|
||||
onLogout: () => {
|
||||
const { logout } = useUserStore();
|
||||
logout();
|
||||
}
|
||||
});
|
||||
6
core/src/utils/helpers/shared.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Shared utilities entry point for external modules
|
||||
* This file is built separately and exposed via import map
|
||||
*/
|
||||
|
||||
export { createFetchWrapper, type FetchWrapperOptions } from './fetch-wrapper-core';
|
||||
107
core/src/utils/modules.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import type { App } from 'vue';
|
||||
import { router } from '@KTXC/router';
|
||||
import { useModuleStore } from '@KTXC/stores/moduleStore';
|
||||
import { useIntegrationStore } from '@KTXC/stores/integrationStore';
|
||||
|
||||
function installModuleCSS(moduleHandle: string, cssPaths: string | string[]): void {
|
||||
const cssFiles = Array.isArray(cssPaths) ? cssPaths : [cssPaths];
|
||||
cssFiles.forEach((cssFile: string) => {
|
||||
const cssPath = `/modules/${moduleHandle}/${cssFile}`;
|
||||
const existingLink = document.querySelector(`link[href="${cssPath}"]`);
|
||||
if (!existingLink) {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = cssPath;
|
||||
link.onload = () => {
|
||||
console.log(`Module Loader - Loaded CSS for ${moduleHandle}: ${cssFile}`);
|
||||
};
|
||||
link.onerror = () => {
|
||||
console.error(`Module Loader - Failed to load CSS for ${moduleHandle}: ${cssPath}`);
|
||||
};
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function installModuleRoutes(moduleHandle: string, routes: any[]): void {
|
||||
routes.forEach((route: any) => {
|
||||
// Prefix route name with module handle for safety
|
||||
const prefixedRoute = {
|
||||
...route,
|
||||
path: `/m/${moduleHandle}${route.path}`
|
||||
};
|
||||
// Prefix the route name if it exists
|
||||
if (route.name) {
|
||||
prefixedRoute.name = `${moduleHandle}.${route.name}`;
|
||||
}
|
||||
// Recursively prefix child route names
|
||||
if (route.children && Array.isArray(route.children)) {
|
||||
prefixedRoute.children = route.children.map((child: any) => ({
|
||||
...child,
|
||||
name: child.name ? `${moduleHandle}.${child.name}` : undefined
|
||||
}));
|
||||
}
|
||||
|
||||
router.addRoute('private', prefixedRoute);
|
||||
});
|
||||
}
|
||||
|
||||
function installModuleIntegrations(
|
||||
moduleHandle: string,
|
||||
integrations: Record<string, any[]>
|
||||
): void {
|
||||
const integrationStore = useIntegrationStore();
|
||||
integrationStore.registerModuleIntegrations(moduleHandle, integrations);
|
||||
}
|
||||
|
||||
export async function initializeModules(app: App): Promise<void> {
|
||||
const moduleStore = useModuleStore();
|
||||
|
||||
// First, dynamically load modules based on moduleStore boot paths
|
||||
const availableModules = moduleStore.modules;
|
||||
const loadPromises: Promise<void>[] = [];
|
||||
|
||||
for (const [moduleId, moduleInfo] of Object.entries(availableModules)) {
|
||||
if (!moduleInfo) {
|
||||
console.warn(`Module ${moduleId} has no configuration, skipping`);
|
||||
continue;
|
||||
}
|
||||
if (moduleInfo.handle && moduleInfo.boot && !moduleInfo.booted) {
|
||||
const moduleHandle = moduleInfo.handle;
|
||||
const moduleUrl = `/modules/${moduleInfo.handle}/${moduleInfo.boot}`;
|
||||
console.log(`Module Loader - Loading ${moduleInfo.handle} from ${moduleUrl}`);
|
||||
|
||||
const loadPromise = import(/* @vite-ignore */ moduleUrl)
|
||||
.then((module) => {
|
||||
// Load CSS if module explicitly exports css path(s)
|
||||
if (module.css) {
|
||||
installModuleCSS(moduleInfo.handle, module.css);
|
||||
}
|
||||
// install module
|
||||
console.log(`Module Loader - Installing ${moduleInfo.handle}`);
|
||||
if (module.default && typeof module.default.install === 'function') {
|
||||
app.use(module.default);
|
||||
}
|
||||
// prefix routes with /m/{moduleHandle}
|
||||
console.log(`Module Loader - Installing Routes ${moduleInfo.handle}`);
|
||||
if (module.routes) {
|
||||
installModuleRoutes(moduleHandle, module.routes);
|
||||
}
|
||||
// register integrations
|
||||
console.log(`Module Loader - Installing Integrations ${moduleInfo.handle}`);
|
||||
if (module.integrations) {
|
||||
installModuleIntegrations(moduleHandle, module.integrations);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`Failed to load module ${moduleId} from ${moduleUrl}:`, error);
|
||||
});
|
||||
loadPromises.push(loadPromise);
|
||||
} else if (!moduleInfo.boot) {
|
||||
console.warn(`No boot path specified for module: ${moduleId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all dynamic loading to complete
|
||||
await Promise.all(loadPromises);
|
||||
}
|
||||
28
core/src/views/PrivateLayout.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router';
|
||||
import LayoutHeader from '@KTXC/layouts/header/LayoutHeader.vue';
|
||||
import { useLayoutStore } from '@KTXC/stores/layoutStore';
|
||||
import LayoutSystemMenu from '@KTXC/layouts/menus/LayoutSystemMenu.vue';
|
||||
|
||||
const layoutStore = useLayoutStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-locale-provider>
|
||||
<v-app :class="[layoutStore.miniSidebar ? 'mini-sidebar' : '']">
|
||||
<LayoutHeader />
|
||||
<LayoutSystemMenu />
|
||||
|
||||
<v-main class="page-wrapper">
|
||||
<RouterView />
|
||||
</v-main>
|
||||
</v-app>
|
||||
</v-locale-provider>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.v-main.page-wrapper {
|
||||
height: calc(100vh - 64px) !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
</style>
|
||||
26
core/src/views/PublicLayout.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router';
|
||||
import LayoutFooter from '@KTXC/layouts/footer/LayoutFooter.vue';
|
||||
import { useLayoutStore } from '@KTXC/stores/layoutStore';
|
||||
|
||||
const layoutStore = useLayoutStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-locale-provider>
|
||||
<v-app :class="[layoutStore.miniSidebar ? 'mini-sidebar' : '']">
|
||||
<v-main class="page-wrapper">
|
||||
<v-container fluid>
|
||||
<div>
|
||||
<RouterView />
|
||||
</div>
|
||||
</v-container>
|
||||
<v-container fluid class="pt-0">
|
||||
<div>
|
||||
<LayoutFooter />
|
||||
</div>
|
||||
</v-container>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</v-locale-provider>
|
||||
</template>
|
||||
38
core/src/views/authentication/AuthFooter.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { shallowRef } from 'vue';
|
||||
|
||||
const footerLink = shallowRef([
|
||||
{
|
||||
title: 'Terms and Conditions'
|
||||
},
|
||||
{
|
||||
title: 'Privacy Policy'
|
||||
},
|
||||
{
|
||||
title: 'CA Privacy Notice'
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
<template>
|
||||
<v-footer class="px-0 pt-2">
|
||||
<v-row justify="center" no-gutters>
|
||||
<v-col cols="12" md="6" class="text-md-left text-center">
|
||||
<p class="text-subtitle-2 text-lightText mb-md-0 mb-4">
|
||||
This site is protected by
|
||||
<a href="/" class="text-primary">Privacy Policy</a>
|
||||
</p>
|
||||
</v-col>
|
||||
<v-col class="d-flex flex-md-row flex-column justify-md-end align-center" cols="12" md="6">
|
||||
<a
|
||||
v-for="(item, i) in footerLink"
|
||||
:key="i"
|
||||
class="mx-md-3 mx-2 mb-md-0 mb-2 text-subtitle-2 text-lightText"
|
||||
href="https://codedthemes.com"
|
||||
target="_blank"
|
||||
>
|
||||
{{ item.title }}
|
||||
</a>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-footer>
|
||||
</template>
|
||||
586
core/src/views/authentication/AuthLogin.vue
Normal file
@@ -0,0 +1,586 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useUserStore } from '@KTXC/stores/userStore';
|
||||
import { authenticationService } from '@KTXC/services/authenticationService';
|
||||
import type { AuthenticationMethod, VerifyResponse } from '@KTXC/types/authenticationTypes';
|
||||
import { Form } from 'vee-validate';
|
||||
|
||||
const route = useRoute();
|
||||
const userStore = useUserStore();
|
||||
|
||||
// Login flow phases
|
||||
type LoginPhase = 'identity' | 'method' | 'mfa';
|
||||
|
||||
// Form state
|
||||
const identity = ref('');
|
||||
const authResponse = ref(''); // password, code, etc.
|
||||
const showPassword = ref(false);
|
||||
const rememberMe = ref(false);
|
||||
|
||||
// Auth state
|
||||
const session = ref<string | null>(null);
|
||||
const phase = ref<LoginPhase>('identity');
|
||||
const allMethods = ref<AuthenticationMethod[]>([]);
|
||||
const availableMethods = ref<AuthenticationMethod[]>([]);
|
||||
const selectedMethod = ref<AuthenticationMethod | null>(null);
|
||||
const loading = ref(true);
|
||||
const submitting = ref(false);
|
||||
const ssoLoading = ref<string | null>(null);
|
||||
const error = ref<string | null>(null);
|
||||
const challengeSent = ref(false);
|
||||
|
||||
// Separate methods by type
|
||||
const redirectMethods = computed(() =>
|
||||
allMethods.value.filter(m => m.method === 'redirect')
|
||||
);
|
||||
|
||||
const nonRedirectMethods = computed(() =>
|
||||
availableMethods.value.filter(m => m.method !== 'redirect')
|
||||
);
|
||||
|
||||
const hasNonRedirectMethods = computed(() =>
|
||||
allMethods.value.some(m => m.method !== 'redirect')
|
||||
);
|
||||
|
||||
// Computed UI states
|
||||
const showIdentityForm = computed(() => phase.value === 'identity');
|
||||
const showAuthForm = computed(() => phase.value === 'method' && selectedMethod.value !== null);
|
||||
const showMfaForm = computed(() => phase.value === 'mfa');
|
||||
const showSsoButtons = computed(() => redirectMethods.value.length > 0 && phase.value === 'identity');
|
||||
|
||||
// Title based on phase
|
||||
const pageTitle = computed(() => {
|
||||
switch (phase.value) {
|
||||
case 'identity': return 'Login';
|
||||
case 'method': return 'Verify Your Identity';
|
||||
case 'mfa': return 'Additional Verification';
|
||||
default: return 'Login';
|
||||
}
|
||||
});
|
||||
|
||||
// Input label/type based on selected method
|
||||
const authInputLabel = computed(() => {
|
||||
if (!selectedMethod.value) return 'Password';
|
||||
return selectedMethod.value.method === 'credential' ? 'Password' : 'Verification Code';
|
||||
});
|
||||
|
||||
const authInputType = computed(() => {
|
||||
if (!selectedMethod.value) return 'password';
|
||||
return selectedMethod.value.method === 'credential' ? 'password' : 'text';
|
||||
});
|
||||
|
||||
// Validation rules
|
||||
const identityRules = [
|
||||
(v: string) => !!v.trim() || 'Email is required',
|
||||
(v: string) => !/\s/.test(v.trim()) || 'Email must not contain spaces',
|
||||
(v: string) => /.+@.+\..+/.test(v.trim()) || 'Email must be valid'
|
||||
];
|
||||
|
||||
const authResponseRules = [
|
||||
(v: string) => !!v || 'This field is required',
|
||||
];
|
||||
|
||||
// Initialize authentication
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// Check for error from SSO callback
|
||||
const errorParam = route.query.error as string;
|
||||
if (errorParam) {
|
||||
error.value = decodeURIComponent(errorParam);
|
||||
}
|
||||
|
||||
// Check for MFA session from redirect
|
||||
const mfaSession = route.query.session as string;
|
||||
if (mfaSession) {
|
||||
session.value = mfaSession;
|
||||
await loadSessionStatus();
|
||||
} else {
|
||||
// Initialize new auth session
|
||||
await initSession();
|
||||
}
|
||||
|
||||
// Auto-trigger SSO if provider param present
|
||||
const providerParam = route.query.provider as string;
|
||||
if (providerParam && phase.value === 'identity') {
|
||||
const provider = allMethods.value.find(m => m.id === providerParam);
|
||||
if (provider && provider.method === 'redirect') {
|
||||
await initiateSsoLogin(provider.id);
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
error.value = e.message || 'Failed to initialize authentication';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Watch for method selection changes (for challenge-based methods)
|
||||
watch(selectedMethod, async (newMethod) => {
|
||||
if (newMethod && newMethod.method === 'challenge' && !challengeSent.value) {
|
||||
// Initiate challenge for methods that need it (SMS, email, TOTP)
|
||||
await initiateChallenge(newMethod.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize a new session
|
||||
async function initSession() {
|
||||
const result = await authenticationService.start();
|
||||
session.value = result.session;
|
||||
allMethods.value = result.methods;
|
||||
phase.value = 'identity';
|
||||
}
|
||||
|
||||
// Load existing session status (for MFA continuation)
|
||||
async function loadSessionStatus() {
|
||||
if (!session.value) return;
|
||||
|
||||
try {
|
||||
const status = await authenticationService.getStatus(session.value);
|
||||
|
||||
if (status.state === 'secondary_pending') {
|
||||
phase.value = 'mfa';
|
||||
availableMethods.value = status.methods;
|
||||
if (availableMethods.value.length > 0) {
|
||||
selectedMethod.value = availableMethods.value[0];
|
||||
}
|
||||
} else if (status.state === 'identified') {
|
||||
// Session has identity, show method selection
|
||||
phase.value = 'method';
|
||||
availableMethods.value = status.methods;
|
||||
autoSelectMethod();
|
||||
} else {
|
||||
// Session not in expected state, reinitialize
|
||||
await initSession();
|
||||
}
|
||||
} catch (e) {
|
||||
// Session expired or invalid, reinitialize
|
||||
await initSession();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle identity submission (step 1)
|
||||
async function handleIdentitySubmit(_values: any, { setErrors }: any) {
|
||||
if (!session.value) return;
|
||||
|
||||
error.value = null;
|
||||
submitting.value = true;
|
||||
|
||||
try {
|
||||
const result = await authenticationService.identify(session.value, identity.value.trim());
|
||||
|
||||
session.value = result.session;
|
||||
availableMethods.value = result.methods;
|
||||
phase.value = 'method';
|
||||
|
||||
autoSelectMethod();
|
||||
} catch (e: any) {
|
||||
const errorMessage = e.message || 'Failed to continue. Please try again.';
|
||||
setErrors({ apiError: errorMessage });
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-select method if only one available
|
||||
function autoSelectMethod() {
|
||||
const methods = nonRedirectMethods.value;
|
||||
if (methods.length === 1) {
|
||||
selectedMethod.value = methods[0];
|
||||
} else if (methods.length > 1) {
|
||||
// Default to credential if available
|
||||
const credMethod = methods.find(m => m.method === 'credential');
|
||||
selectedMethod.value = credMethod || methods[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Handle method selection
|
||||
function selectMethod(method: AuthenticationMethod) {
|
||||
selectedMethod.value = method;
|
||||
authResponse.value = '';
|
||||
challengeSent.value = false;
|
||||
}
|
||||
|
||||
// Initiate a challenge (for SMS, email, TOTP)
|
||||
async function initiateChallenge(methodId: string) {
|
||||
if (!session.value) return;
|
||||
|
||||
try {
|
||||
await authenticationService.beginChallenge(session.value, methodId);
|
||||
challengeSent.value = true;
|
||||
} catch (e: any) {
|
||||
// Challenge initiation failed - show generic error
|
||||
error.value = e.message || 'Failed to send verification code.';
|
||||
}
|
||||
}
|
||||
|
||||
// Handle authentication response submission (step 2)
|
||||
async function handleAuthSubmit(_values: any, { setErrors }: any) {
|
||||
if (!session.value || !selectedMethod.value) return;
|
||||
|
||||
error.value = null;
|
||||
submitting.value = true;
|
||||
|
||||
try {
|
||||
const result = await authenticationService.verify(
|
||||
session.value,
|
||||
selectedMethod.value.id,
|
||||
authResponse.value
|
||||
// identity is already in session from identify() step
|
||||
);
|
||||
|
||||
handleVerifyResponse(result);
|
||||
} catch (e: any) {
|
||||
const errorMessage = e.message || 'Verification failed. Please try again.';
|
||||
setErrors({ apiError: errorMessage });
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle MFA verification (step 3)
|
||||
async function handleMfaSubmit(_values: any, { setErrors }: any) {
|
||||
if (!session.value || !selectedMethod.value) return;
|
||||
|
||||
error.value = null;
|
||||
submitting.value = true;
|
||||
|
||||
try {
|
||||
const result = await authenticationService.verify(
|
||||
session.value,
|
||||
selectedMethod.value.id,
|
||||
authResponse.value
|
||||
);
|
||||
|
||||
handleVerifyResponse(result);
|
||||
} catch (e: any) {
|
||||
const errorMessage = e.message || 'Verification failed. Please try again.';
|
||||
setErrors({ apiError: errorMessage });
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle verify response
|
||||
function handleVerifyResponse(result: VerifyResponse) {
|
||||
if (result.status === 'success' && result.user) {
|
||||
// Authentication complete
|
||||
userStore.setAuth(result.user);
|
||||
window.location.replace('/');
|
||||
} else if (result.status === 'pending') {
|
||||
// MFA required
|
||||
phase.value = 'mfa';
|
||||
availableMethods.value = result.methods || [];
|
||||
if (result.session) {
|
||||
session.value = result.session;
|
||||
}
|
||||
if (availableMethods.value.length > 0) {
|
||||
selectedMethod.value = availableMethods.value[0];
|
||||
}
|
||||
authResponse.value = '';
|
||||
challengeSent.value = false;
|
||||
} else if (result.error) {
|
||||
error.value = result.error;
|
||||
}
|
||||
}
|
||||
|
||||
// Initiate SSO login
|
||||
async function initiateSsoLogin(methodId: string) {
|
||||
if (!session.value) return;
|
||||
|
||||
ssoLoading.value = methodId;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const result = await authenticationService.beginRedirect(session.value, methodId, '/');
|
||||
window.location.href = result.redirect_url;
|
||||
} catch (e: any) {
|
||||
error.value = e.message || 'Failed to initiate SSO login';
|
||||
ssoLoading.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Go back to identity phase
|
||||
function backToIdentity() {
|
||||
phase.value = 'identity';
|
||||
selectedMethod.value = null;
|
||||
authResponse.value = '';
|
||||
challengeSent.value = false;
|
||||
error.value = null;
|
||||
|
||||
// Reinitialize session
|
||||
initSession();
|
||||
}
|
||||
|
||||
function getMethodIcon(method: AuthenticationMethod): string {
|
||||
if (method.icon) return method.icon;
|
||||
switch (method.method) {
|
||||
case 'credential': return 'mdi-key';
|
||||
case 'challenge': return 'mdi-shield-check';
|
||||
case 'redirect': return 'mdi-login';
|
||||
default: return 'mdi-shield-check';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="d-flex justify-space-between align-center">
|
||||
<h3 class="text-h3 text-center mb-0">{{ pageTitle }}</h3>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" class="mt-7 text-center">
|
||||
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||
<p class="mt-2 text-medium-emphasis">Loading login options...</p>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- Error Alert -->
|
||||
<v-alert v-if="error" type="error" class="mt-4" closable @click:close="error = null">
|
||||
{{ error }}
|
||||
</v-alert>
|
||||
|
||||
<!-- Phase 1: Identity Input -->
|
||||
<template v-if="showIdentityForm">
|
||||
<!-- Identity Form -->
|
||||
<Form
|
||||
v-if="hasNonRedirectMethods"
|
||||
@submit="handleIdentitySubmit"
|
||||
class="loginForm"
|
||||
:class="{ 'mt-7': !showSsoButtons }"
|
||||
v-slot="{ errors, isSubmitting }"
|
||||
>
|
||||
<div class="mb-6">
|
||||
<v-label>Email Address</v-label>
|
||||
<v-text-field
|
||||
v-model="identity"
|
||||
:rules="identityRules"
|
||||
class="mt-2"
|
||||
required
|
||||
hide-details="auto"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
autocomplete="email"
|
||||
autofocus
|
||||
></v-text-field>
|
||||
</div>
|
||||
|
||||
<v-btn
|
||||
color="primary"
|
||||
:loading="isSubmitting || submitting"
|
||||
block
|
||||
class="mt-5"
|
||||
variant="flat"
|
||||
size="large"
|
||||
type="submit"
|
||||
>
|
||||
Continue
|
||||
</v-btn>
|
||||
|
||||
<!-- SSO Buttons -->
|
||||
<div v-if="showSsoButtons" class="mt-7">
|
||||
<v-btn
|
||||
v-for="method in redirectMethods"
|
||||
:key="method.id"
|
||||
color="secondary"
|
||||
:loading="ssoLoading === method.id"
|
||||
:disabled="ssoLoading !== null"
|
||||
block
|
||||
class="mb-3"
|
||||
variant="outlined"
|
||||
size="large"
|
||||
@click="initiateSsoLogin(method.id)"
|
||||
>
|
||||
<v-icon v-if="method.icon" start>{{ method.icon }}</v-icon>
|
||||
{{ method.label }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div v-if="errors.apiError" class="mt-2">
|
||||
<v-alert color="error">{{ errors.apiError }}</v-alert>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
<!-- No providers -->
|
||||
<div v-if="!hasNonRedirectMethods && !showSsoButtons" class="mt-7 text-center">
|
||||
<v-alert type="warning">
|
||||
No login methods are currently available. Please contact your administrator.
|
||||
</v-alert>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Phase 2: Method Selection & Authentication -->
|
||||
<template v-else-if="showAuthForm && !showMfaForm">
|
||||
<p class="text-body-1 text-medium-emphasis mt-4 mb-2">
|
||||
Signing in as <strong>{{ identity }}</strong>
|
||||
</p>
|
||||
|
||||
<!-- Method Selector (if multiple methods) -->
|
||||
<div v-if="nonRedirectMethods.length > 1" class="mb-4">
|
||||
<v-label class="mb-2">Choose verification method</v-label>
|
||||
<v-btn-toggle
|
||||
:model-value="selectedMethod?.id"
|
||||
mandatory
|
||||
variant="outlined"
|
||||
divided
|
||||
class="w-100"
|
||||
>
|
||||
<v-btn
|
||||
v-for="method in nonRedirectMethods"
|
||||
:key="method.id"
|
||||
:value="method.id"
|
||||
@click="selectMethod(method)"
|
||||
class="flex-grow-1"
|
||||
>
|
||||
<v-icon start>{{ getMethodIcon(method) }}</v-icon>
|
||||
{{ method.label }}
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</div>
|
||||
|
||||
<!-- Auth Response Form -->
|
||||
<Form
|
||||
@submit="handleAuthSubmit"
|
||||
class="loginForm mt-4"
|
||||
v-slot="{ errors, isSubmitting }"
|
||||
>
|
||||
<div class="mb-6">
|
||||
<v-label>{{ authInputLabel }}</v-label>
|
||||
<v-text-field
|
||||
v-model="authResponse"
|
||||
:rules="authResponseRules"
|
||||
:type="authInputType === 'password' && !showPassword ? 'password' : 'text'"
|
||||
class="mt-2"
|
||||
required
|
||||
hide-details="auto"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
:autocomplete="selectedMethod?.method === 'credential' ? 'current-password' : 'one-time-code'"
|
||||
:inputmode="selectedMethod?.method !== 'credential' ? 'numeric' : undefined"
|
||||
autofocus
|
||||
>
|
||||
<template v-if="authInputType === 'password'" v-slot:append-inner>
|
||||
<v-btn
|
||||
variant="text"
|
||||
density="compact"
|
||||
:icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
|
||||
@click="showPassword = !showPassword"
|
||||
></v-btn>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedMethod?.method === 'credential'" class="d-flex align-center mt-4 mb-7 mb-sm-0">
|
||||
<v-checkbox
|
||||
v-model="rememberMe"
|
||||
label="Keep me logged in"
|
||||
color="primary"
|
||||
hide-details
|
||||
></v-checkbox>
|
||||
<div class="ml-auto">
|
||||
<router-link to="/forgot-password" class="text-primary text-decoration-none">
|
||||
Forgot Password?
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-btn
|
||||
color="primary"
|
||||
:loading="isSubmitting || submitting"
|
||||
block
|
||||
class="mt-5"
|
||||
variant="flat"
|
||||
size="large"
|
||||
type="submit"
|
||||
>
|
||||
{{ selectedMethod?.method === 'credential' ? 'Login' : 'Verify' }}
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
variant="text"
|
||||
block
|
||||
class="mt-3"
|
||||
@click="backToIdentity"
|
||||
>
|
||||
Use different account
|
||||
</v-btn>
|
||||
|
||||
<div v-if="errors.apiError" class="mt-2">
|
||||
<v-alert color="error">{{ errors.apiError }}</v-alert>
|
||||
</div>
|
||||
</Form>
|
||||
</template>
|
||||
|
||||
<!-- Phase 3: MFA Verification -->
|
||||
<template v-if="showMfaForm">
|
||||
<p class="text-body-1 text-medium-emphasis mt-4 mb-6">
|
||||
Additional verification is required. Please enter the code from your {{ selectedMethod?.label || 'authenticator' }}.
|
||||
</p>
|
||||
|
||||
<!-- MFA Method Selector (if multiple) -->
|
||||
<v-select
|
||||
v-if="availableMethods.length > 1"
|
||||
v-model="selectedMethod"
|
||||
:items="availableMethods"
|
||||
item-title="label"
|
||||
item-value="id"
|
||||
return-object
|
||||
label="Verification Method"
|
||||
variant="outlined"
|
||||
class="mb-4"
|
||||
></v-select>
|
||||
|
||||
<Form @submit="handleMfaSubmit" v-slot="{ errors, isSubmitting }">
|
||||
<div class="mb-6">
|
||||
<v-label>Verification Code</v-label>
|
||||
<v-text-field
|
||||
v-model="authResponse"
|
||||
:rules="authResponseRules"
|
||||
type="text"
|
||||
class="mt-2"
|
||||
required
|
||||
hide-details="auto"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
autocomplete="one-time-code"
|
||||
inputmode="numeric"
|
||||
autofocus
|
||||
></v-text-field>
|
||||
</div>
|
||||
|
||||
<v-btn
|
||||
color="primary"
|
||||
:loading="isSubmitting || submitting"
|
||||
block
|
||||
class="mt-5"
|
||||
variant="flat"
|
||||
size="large"
|
||||
type="submit"
|
||||
>
|
||||
Verify
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
variant="text"
|
||||
block
|
||||
class="mt-3"
|
||||
@click="backToIdentity"
|
||||
>
|
||||
Back to Login
|
||||
</v-btn>
|
||||
|
||||
<div v-if="errors.apiError" class="mt-2">
|
||||
<v-alert color="error">{{ errors.apiError }}</v-alert>
|
||||
</div>
|
||||
</Form>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.loginForm {
|
||||
.v-text-field .v-field--active input {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
53
core/src/views/authentication/LoginPage.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import Logo from '@KTXC/layouts/logo/LogoDark.vue';
|
||||
import AuthLogin from './AuthLogin.vue';
|
||||
import AuthFooter from './AuthFooter.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-row class="bg-containerBg position-relative" no-gutters>
|
||||
<v-col cols="12">
|
||||
<div class="pt-6 pl-6">
|
||||
<Logo />
|
||||
</div>
|
||||
</v-col>
|
||||
<!---Login Part-->
|
||||
<v-col cols="12" lg="12" class="d-flex align-center">
|
||||
<v-container>
|
||||
<div class="d-flex align-center justify-center" style="min-height: calc(100vh - 148px)">
|
||||
<v-row justify="center">
|
||||
<v-col cols="12" md="12">
|
||||
<v-card elevation="0" class="loginBox">
|
||||
<v-card elevation="24">
|
||||
<v-card-text class="pa-sm-10 pa-6">
|
||||
<!---Login Form-->
|
||||
<AuthLogin />
|
||||
<!---Login Form-->
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-container>
|
||||
</v-col>
|
||||
<!---Login Part-->
|
||||
<v-col cols="12">
|
||||
<v-container class="pt-0 pb-6">
|
||||
<AuthFooter />
|
||||
</v-container>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.loginBox {
|
||||
max-width: 475px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.blur-logo {
|
||||
position: absolute;
|
||||
filter: blur(18px);
|
||||
bottom: 0;
|
||||
transform: inherit;
|
||||
}
|
||||
</style>
|
||||
50
core/src/views/pages/maintenance/error/Error404Page.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<v-row no-gutters class="overflow-hidden bg-containerBg" style="min-height: 100vh">
|
||||
<v-col class="d-flex align-center justify-center">
|
||||
<div class="text-center">
|
||||
<div class="CardMediaWrapper">
|
||||
<img src="@KTXC/assets/images/maintenance/Error404.png" alt="404" />
|
||||
<div class="CardMediaBuild">
|
||||
<img src="@KTXC/assets/images/maintenance/TwoCone.png" alt="grid" class="w-100" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="text-h1 mt-16">Page Not Found</h1>
|
||||
<p class="text-h6 text-lightText">The page you are looking was moved, removed, <br />renamed, or might never exist!</p>
|
||||
<v-btn variant="flat" color="primary" class="mt-2" to="/"> Back To Home</v-btn>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.CardMediaWrapper {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
> img {
|
||||
@media (min-width: 0px) {
|
||||
width: 250px;
|
||||
height: 130px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
width: 590px;
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.CardMediaBuild {
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
@media (min-width: 0px) {
|
||||
width: 130px;
|
||||
height: 115px;
|
||||
right: -14%;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
width: 390px;
|
||||
height: 330px;
|
||||
right: -60%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
28
core/src/views/pages/maintenance/error/Error500Page.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<v-row no-gutters class="overflow-hidden bg-containerBg" style="min-height: 100vh">
|
||||
<v-col class="d-flex align-center justify-center">
|
||||
<div class="text-center">
|
||||
<div class="errorMedia">
|
||||
<img src="@/assets/images/maintenance/Error500.png" alt="404" />
|
||||
</div>
|
||||
<h1 class="text-h1 mt-2 mb-1">Internal Server Error</h1>
|
||||
<p class="text-caption text-lightText">Server error 500. we fixing the problem. please try <br />again at a later stage.</p>
|
||||
<v-btn variant="flat" color="primary" class="mt-4" to="/"> Back To Home</v-btn>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.errorMedia {
|
||||
> img {
|
||||
@media (min-width: 0px) {
|
||||
width: 350px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
width: 396px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||