Initial Version

This commit is contained in:
root
2025-12-21 10:09:54 -05:00
commit 4ae6befc7b
422 changed files with 47225 additions and 0 deletions

20
core/src/App.vue Normal file
View 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>

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

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

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

View File

@@ -0,0 +1,6 @@
/**
* Core composables - reusable composition functions
*/
export { useClipboard } from './useClipboard'
export { useUser } from './useUser'

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

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

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

View File

@@ -0,0 +1,9 @@
<script setup lang="ts">
import { RouterView } from 'vue-router';
</script>
<template>
<v-app>
<RouterView />
</v-app>
</template>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View 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,
},
})

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

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

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

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

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

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

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

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

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

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

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

View File

@@ -0,0 +1,3 @@
.v-navigation-drawer__scrim.fade-transition-leave-to {
display: none;
}

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

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

View File

@@ -0,0 +1,7 @@
.v-textarea input {
font-size: 0.875rem;
font-weight: 500;
&::placeholder {
color: rgba(0, 0, 0, 0.38);
}
}

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

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

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

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

View 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', {});
},
};

View File

@@ -0,0 +1 @@
export { userService } from './userService';

View 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
View File

@@ -0,0 +1,5 @@
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}

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

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

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

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

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

View 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
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

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

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

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

View File

@@ -0,0 +1,13 @@
/**
* User Profile Types
*/
export interface ProfileFieldInterface {
value: any;
editable: boolean;
provider: string | null;
}
export interface UserProfileInterface {
[key: string]: ProfileFieldInterface;
}

View File

@@ -0,0 +1,7 @@
/**
* User Settings Types
*/
export interface UserSettingsInterface {
[key: string]: any;
}

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

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

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

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

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

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

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

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

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

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