Initial Version

This commit is contained in:
root
2025-12-21 10:09:54 -05:00
commit 2fbddd7dbc
366 changed files with 41999 additions and 0 deletions

7
core/src/App.vue Normal file
View File

@@ -0,0 +1,7 @@
<template>
<RouterView></RouterView>
</template>
<script setup lang="ts">
import { RouterView } from 'vue-router';
</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 { usePreferences } from './usePreferences'

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,208 @@
import { computed, ref } from 'vue';
import { usePreferencesStore, type PreferencesState } from '@KTXC/stores/preferencesStore';
import { preferenceService } from '@KTXC/services/preferenceService';
/**
* Composable for managing user preferences
* Provides reactive access to preferences with automatic sync to server
*/
export function usePreferences() {
const store = usePreferencesStore();
const saving = ref(false);
const error = ref<string | null>(null);
/**
* Get all preferences
*/
const preferences = computed(() => store.preferences);
/**
* Get locked preference keys
*/
const locks = computed(() => store.locks);
/**
* Check if a preference is locked by tenant admin
*/
const isLocked = (key: keyof PreferencesState): boolean => {
return store.isLocked(key);
};
/**
* Get a single preference value
*/
const get = <K extends keyof PreferencesState>(key: K): PreferencesState[K] => {
return store.getPreference(key);
};
/**
* Set a single preference and sync to server
*/
const set = async <K extends keyof PreferencesState>(
key: K,
value: PreferencesState[K],
syncToServer = true
): Promise<boolean> => {
error.value = null;
// Update local state first
const success = store.setPreference(key, value);
if (!success) {
error.value = `Preference "${key}" is locked by administrator`;
return false;
}
// Sync to server if requested
if (syncToServer) {
saving.value = true;
try {
const response = await preferenceService.setPreference(key, value);
// Update store with server response to ensure consistency
store.setPreferences(response.effective);
store.setLocks(response.locks);
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to save preference';
return false;
} finally {
saving.value = false;
}
}
return true;
};
/**
* Update multiple preferences and sync to server
*/
const update = async (
prefs: Partial<PreferencesState>,
syncToServer = true
): Promise<{ success: boolean; rejected: string[] }> => {
error.value = null;
// Update local state
store.setPreferences(prefs);
if (syncToServer) {
saving.value = true;
try {
const response = await preferenceService.updatePreferences(prefs);
store.setPreferences(response.effective);
store.setLocks(response.locks);
return {
success: true,
rejected: response.rejectedKeys ?? [],
};
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to save preferences';
return { success: false, rejected: [] };
} finally {
saving.value = false;
}
}
return { success: true, rejected: [] };
};
/**
* Reset preferences to tenant defaults
*/
const reset = async (): Promise<boolean> => {
error.value = null;
saving.value = true;
try {
const response = await preferenceService.resetPreferences();
store.setPreferences(response.effective);
store.setLocks(response.locks);
return true;
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to reset preferences';
return false;
} finally {
saving.value = false;
}
};
/**
* Refresh preferences from server
*/
const refresh = async (): Promise<boolean> => {
error.value = null;
store.setLoading(true);
try {
const response = await preferenceService.getPreferences();
store.setPreferences(response.effective);
store.setLocks(response.locks);
return true;
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to load preferences';
return false;
} finally {
store.setLoading(false);
}
};
// Individual preference computed refs for convenience
const theme = computed({
get: () => store.preferences.theme,
set: (value: string) => set('theme', value),
});
const language = computed({
get: () => store.preferences.language,
set: (value: string) => set('language', value),
});
const timezone = computed({
get: () => store.preferences.timezone,
set: (value: string) => set('timezone', value),
});
const dateFormat = computed({
get: () => store.preferences.date_format,
set: (value: string) => set('date_format', value),
});
const timeFormat = computed({
get: () => store.preferences.time_format,
set: (value: string) => set('time_format', value),
});
const weekStart = computed({
get: () => store.preferences.week_start,
set: (value: string) => set('week_start', value),
});
const defaultModule = computed({
get: () => store.preferences.default_module ?? '',
set: (value: string) => set('default_module', value),
});
return {
// State
preferences,
locks,
saving,
error,
loading: computed(() => store.loading),
// Methods
get,
set,
update,
reset,
refresh,
isLocked,
// Individual preference refs
theme,
language,
timezone,
dateFormat,
timeFormat,
weekStart,
defaultModule,
};
}

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,103 @@
<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';
const layoutStore = useLayoutStore();
const userStore = useUserStore();
const identityData = computed(() => userStore.user);
</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 py-2">
<img src="@KTXC/assets/images/users/avatar-1.png" alt="Julia" />
</v-avatar>
<h6 class="text-subtitle-1 mb-0 d-sm-block d-none">
{{ identityData?.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">
<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,93 @@
<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(() => {
const pointType = layoutStore.menuMode === 'settings' ? 'admin_settings_menu' : 'app_menu';
return integrationStore.getPoint(pointType);
});
// Menu mode display info
const menuModeInfo = computed(() => ({
icon: layoutStore.menuMode === 'settings' ? 'mdi-cog' : 'mdi-view-dashboard',
label: layoutStore.menuMode === 'settings' ? 'Settings' : 'Applications',
toggleLabel: layoutStore.menuMode === 'settings' ? 'Switch to Apps' : 'Switch to Settings',
toggleIcon: layoutStore.menuMode === 'settings' ? 'mdi-view-dashboard' : 'mdi-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,101 @@
<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';
const theme = useTheme();
const router = useRouter();
const userStore = useUserStore();
const integrationStore = useIntegrationStore();
const layoutStore = useLayoutStore();
const identityData = computed(() => userStore.user);
const profileMenuItems = computed(() => integrationStore.getItems('profile_menu'));
// 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;
localStorage.setItem('theme', 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">
<v-avatar size="32" class="mr-2">
<img src="@KTXC/assets/images/users/avatar-1.png" width="32" />
</v-avatar>
<div>
<h6 class="text-h6 mb-0">
{{ identityData?.label || 'User Name' }}
</h6>
<p class="text-caption mb-0">{{ identityData?.email || '' }}</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,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

24
core/src/private.html Normal file
View File

@@ -0,0 +1,24 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css" />
<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>

67
core/src/private.ts Normal file
View File

@@ -0,0 +1,67 @@
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 { usePreferencesStore } from '@KTXC/stores/preferencesStore'
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'
// 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, preferences) before mounting
(async () => {
const moduleStore = useModuleStore();
const tenantStore = useTenantStore();
try {
const payload = await fetchWrapper.get('/init');
moduleStore.init(payload?.modules ?? {});
tenantStore.init(payload?.tenant ?? null);
// 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 now
app.mount('#app');
} catch (e) {
console.error('Bootstrap failed:', e);
}
})();

14
core/src/public.html Normal file
View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.svg" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css" />
<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>

36
core/src/public.ts Normal file
View File

@@ -0,0 +1,36 @@
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';
// 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');

117
core/src/router/index.ts Normal file
View File

@@ -0,0 +1,117 @@
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 { usePreferencesStore } from '@KTXC/stores/preferencesStore';
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 preferences = usePreferencesStore();
// Treat preference as a route name (e.g., "samples.overview")
const preferredRouteName = preferences.preferences.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.user && to.path !== '/login') {
userStore.returnUrl = to.fullPath;
return next('/login');
}
if (userStore.user && 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,82 @@
import { fetchWrapper } from '@KTXC/utils/helpers/fetch-wrapper';
import type { PreferencesState } from '@KTXC/stores/preferencesStore';
export interface PreferenceResponse {
effective: PreferencesState;
tenant: PreferencesState;
user: Partial<PreferencesState>;
locks: string[];
savedKeys?: string[];
rejectedKeys?: string[];
}
export interface TenantPreferenceResponse {
preferences: PreferencesState;
locks: string[];
defaults: PreferencesState;
}
export const preferenceService = {
/**
* Get effective preferences for current user
* Returns merged preferences with tenant defaults and user overrides resolved
*/
async getPreferences(): Promise<PreferenceResponse> {
return await fetchWrapper.get('/preferences');
},
/**
* Update multiple user preferences at once
* Locked preferences will be rejected and returned in rejectedKeys
*/
async updatePreferences(preferences: Partial<PreferencesState>): Promise<PreferenceResponse> {
return await fetchWrapper.put('/preferences', preferences);
},
/**
* Update a single user preference
*/
async setPreference<K extends keyof PreferencesState>(
key: K,
value: PreferencesState[K]
): Promise<PreferenceResponse> {
return await fetchWrapper.put(`/preferences/${key}`, { value });
},
/**
* Reset all user preferences to tenant defaults
*/
async resetPreferences(): Promise<PreferenceResponse> {
return await fetchWrapper.post('/preferences/reset', {});
},
// ============ Admin/Tenant Management ============
/**
* Get tenant preferences (admin only)
* Returns tenant defaults and locks
*/
async getTenantPreferences(): Promise<TenantPreferenceResponse> {
return await fetchWrapper.get('/preferences/tenant');
},
/**
* Update tenant preferences (admin only)
* @param preferences - The tenant-wide default preferences
* @param locks - Array of preference keys that users cannot override
*/
async updateTenantPreferences(
preferences: Partial<PreferencesState>,
locks: string[] = []
): Promise<TenantPreferenceResponse> {
return await fetchWrapper.put('/preferences/tenant', { preferences, locks });
},
/**
* Update tenant preference locks (admin only)
* @param locks - Array of preference keys that users cannot override
*/
async updateTenantLocks(locks: string[]): Promise<TenantPreferenceResponse> {
return await fetchWrapper.put('/preferences/tenant/locks', { locks });
},
};

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,62 @@
import { ref } from 'vue';
import { defineStore } from 'pinia';
import config from '@KTXC/config';
export type MenuMode = 'apps' | 'settings';
export const useLayoutStore = defineStore('layout', () => {
// Loading state
const isLoading = ref(false);
// Sidebar state
const sidebarDrawer = ref(config.Sidebar_drawer);
const miniSidebar = ref(config.mini_sidebar);
const menuMode = ref<MenuMode>('apps');
// Theme state
const theme = ref(config.actTheme);
const font = ref(config.fontTheme);
// 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() {
menuMode.value = menuMode.value === 'apps' ? 'settings' : 'apps';
}
return {
// State
isLoading,
sidebarDrawer,
miniSidebar,
menuMode,
theme,
font,
// Actions
toggleSidebarDrawer,
setMiniSidebar,
setMenuMode,
toggleMenuMode,
setTheme,
setFont
};
});

View File

@@ -0,0 +1,28 @@
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: {
},
});

View File

@@ -0,0 +1,114 @@
import { defineStore } from 'pinia';
export interface PreferencesState {
theme: string;
language: string;
timezone: string;
date_format: string;
time_format: string;
week_start: string;
default_module?: string; // preferred module handle like "dashboard"
}
const defaults: PreferencesState = {
theme: 'light',
language: 'en',
timezone: 'UTC',
date_format: 'Y-m-d',
time_format: 'H:i',
week_start: 'Monday',
default_module: ''
};
export const usePreferencesStore = defineStore('preferencesStore', {
state: () => ({
preferences: { ...defaults } as PreferencesState,
locks: [] as string[], // preference keys locked by tenant admin
loading: false,
error: null as string | null,
}),
getters: {
/**
* Check if a specific preference is locked by tenant admin
*/
isLocked: (state) => (key: string): boolean => {
return state.locks.includes(key);
},
/**
* Get a single preference value
*/
getPreference: (state) => <K extends keyof PreferencesState>(key: K): PreferencesState[K] => {
return state.preferences[key];
},
},
actions: {
/**
* Initialize preferences from server data (called on app bootstrap)
*/
init(prefs: Partial<PreferencesState> | undefined, locks?: string[]) {
this.preferences = { ...defaults, ...(prefs ?? {}) };
this.locks = locks ?? [];
},
/**
* Set multiple preferences at once (local state only)
*/
setPreferences(prefs: Partial<PreferencesState>) {
// Filter out locked preferences
const unlocked: Partial<PreferencesState> = {};
for (const [key, value] of Object.entries(prefs)) {
if (!this.locks.includes(key)) {
(unlocked as Record<string, unknown>)[key] = value;
}
}
this.preferences = { ...this.preferences, ...unlocked };
},
/**
* Set a single preference (local state only)
*/
setPreference<K extends keyof PreferencesState>(key: K, value: PreferencesState[K]) {
if (this.locks.includes(key)) {
console.warn(`Preference "${key}" is locked by administrator`);
return false;
}
this.preferences[key] = value;
return true;
},
/**
* Set default module preference
*/
setDefaultModule(handle: string) {
if (this.locks.includes('default_module')) {
console.warn('Default module preference is locked by administrator');
return false;
}
this.preferences.default_module = handle || '';
return true;
},
/**
* Update locks from server response
*/
setLocks(locks: string[]) {
this.locks = locks ?? [];
},
/**
* Reset preferences to defaults (local state only)
*/
reset() {
this.preferences = { ...defaults };
this.locks = [];
this.error = null;
},
/**
* Set loading state
*/
setLoading(loading: boolean) {
this.loading = loading;
},
/**
* Set error state
*/
setError(error: string | null) {
this.error = error;
},
},
});

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,69 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { router } from '@KTXC/router';
import { authenticationService } from '@KTXC/services/authenticationService';
import type { AuthenticatedUser } from '@KTXC/types/authenticationTypes';
const STORAGE_KEY = 'userStore.user';
export const useUserStore = defineStore('userStore', () => {
// Load user from localStorage on init
const user = ref<AuthenticatedUser | null>(
localStorage.getItem(STORAGE_KEY)
? (JSON.parse(localStorage.getItem(STORAGE_KEY)!) as AuthenticatedUser)
: null
);
const returnUrl = ref<string | null>(null);
/**
* Set user after successful authentication
*/
function setUser(authUser: AuthenticatedUser): void {
user.value = authUser;
localStorage.setItem(STORAGE_KEY, JSON.stringify(authUser));
}
/**
* Clear user state (on logout or auth failure)
*/
function clearUser(): void {
user.value = null;
localStorage.removeItem(STORAGE_KEY);
}
/**
* Logout and redirect to login
*/
async function logout(): Promise<void> {
try {
await authenticationService.logout();
} catch (error) {
console.warn('Logout request failed, clearing local state:', error);
} finally {
clearUser();
router.push('/login');
}
}
/**
* Refresh access token
*/
async function refreshToken(): Promise<boolean> {
try {
await authenticationService.refresh();
return true;
} catch (error) {
await logout();
return false;
}
}
return {
user,
returnUrl,
setUser,
clearUser,
logout,
refreshToken,
};
});

View File

@@ -0,0 +1,86 @@
/**
* 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;
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,51 @@
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-side integration types (before prefixing by the loader)
export interface ModuleIntegrationItem {
id: string;
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>;
}
export interface ModuleIntegrationGroup {
id: string;
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[] | undefined;
}

View File

@@ -0,0 +1,7 @@
export interface Identity {
identifier: string;
identity: string;
label: string;
email: string;
}

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

103
core/src/utils/modules.ts Normal file
View File

@@ -0,0 +1,103 @@
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.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,21 @@
<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>

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.setUser(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>