ENH - polish phone UI and repository hygiene

This commit is contained in:
Type
2026-08-06 20:58:55 +02:00
parent 48bcc6c551
commit dd2b5442b6
6 changed files with 227 additions and 436 deletions
+1
View File
@@ -1 +1,2 @@
/sky_phone/source/html
.pnpm-store/
+2
View File
@@ -1,4 +1,6 @@
node_modules/
dist/
.vite/
*.local
dev-server*.log
+1 -1
View File
@@ -5,7 +5,7 @@ import pluginVue from 'eslint-plugin-vue'
export default defineConfigWithVueTs(
{ files: ['**/*.{ts,mts,tsx,vue}'], name: 'app/files-to-lint' },
globalIgnores(['**/dist/**', 'testserver/**', 'build.cjs']),
globalIgnores(['**/.vite/**', '**/dist/**', 'testserver/**', 'build.cjs']),
pluginVue.configs['flat/essential'],
vueTsConfigs.recommended,
skipFormatting,
+61 -307
View File
@@ -1202,54 +1202,26 @@ button {
opacity: 0.2;
}
.banking-header {
.banking-navbar {
--k-safe-area-top: 46px;
position: absolute;
z-index: 5;
top: 0;
right: 0;
left: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 54px 22px 13px;
background: linear-gradient(to bottom, rgb(7 13 29 / 85%), transparent);
}
.banking-header > div {
min-width: 0;
}
.banking-header span {
display: block;
color: rgb(208 220 248 / 64%);
font-size: 11px;
letter-spacing: 0.03em;
}
.banking-header h1 {
overflow: hidden;
margin: 2px 0 0;
font-size: 17px;
font-weight: 650;
letter-spacing: -0.02em;
text-overflow: ellipsis;
white-space: nowrap;
color: #fff;
}
.banking-profile {
position: relative;
display: grid;
width: 42px;
height: 42px;
width: 36px;
height: 36px;
flex: 0 0 auto;
place-items: center;
border: 1px solid rgb(255 255 255 / 16%);
border-radius: 15px;
background: linear-gradient(145deg, rgb(65 130 255 / 42%), rgb(19 35 78 / 48%));
box-shadow: inset 0 1px rgb(255 255 255 / 18%), 0 9px 28px rgb(0 0 0 / 22%);
border-radius: 50%;
background: transparent;
color: #dce9ff;
backdrop-filter: blur(18px) saturate(145%);
-webkit-backdrop-filter: blur(18px) saturate(145%);
}
.banking-profile .banking-spin {
@@ -1297,30 +1269,8 @@ button {
}
.banking-balance {
position: relative;
overflow: hidden;
padding: 22px 20px 20px;
border: 1px solid rgb(135 178 255 / 22%);
border-radius: 27px;
background:
linear-gradient(145deg, rgb(50 106 218 / 38%), rgb(15 29 67 / 36%)),
rgb(255 255 255 / 3%);
box-shadow: inset 0 1px rgb(255 255 255 / 16%), 0 22px 52px rgb(0 0 0 / 26%);
backdrop-filter: blur(26px) saturate(150%);
-webkit-backdrop-filter: blur(26px) saturate(150%);
}
.banking-balance::after {
position: absolute;
width: 150px;
height: 150px;
top: -92px;
right: -24px;
border-radius: 50%;
background: var(--bank-blue);
box-shadow: 0 0 68px 25px rgb(45 118 255 / 48%);
content: '';
opacity: 0.25;
}
.banking-balance__label {
@@ -1361,47 +1311,67 @@ button {
.banking-actions {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-columns: repeat(2, 1fr);
gap: 9px;
margin: 13px 0 18px;
}
.banking-actions button {
.banking-action {
display: flex;
align-items: center;
flex-direction: column;
gap: 7px;
padding: 12px 5px 10px;
border: 1px solid rgb(255 255 255 / 10%);
justify-content: center;
gap: 8px;
min-height: 64px;
padding: 11px 12px;
border-radius: 19px;
background: rgb(255 255 255 / 5.5%);
box-shadow: inset 0 1px rgb(255 255 255 / 9%);
color: rgb(237 243 255 / 82%);
font-size: 10px;
backdrop-filter: blur(17px);
-webkit-backdrop-filter: blur(17px);
}
.banking-actions button span {
.banking-action__icon {
display: grid;
width: 38px;
height: 38px;
width: 34px;
height: 34px;
flex: 0 0 auto;
place-items: center;
border-radius: 13px;
border-radius: 11px;
background: linear-gradient(145deg, rgb(60 129 255 / 34%), rgb(25 54 115 / 25%));
color: #8fb8ff;
}
.banking-action b {
font-size: 11px;
font-weight: 610;
}
.banking-action--primary {
grid-column: 1 / -1;
justify-content: flex-start;
min-height: 58px;
padding: 10px 13px;
color: #fff;
}
.banking-action--primary .banking-action__icon {
background: rgb(255 255 255 / 15%);
color: #fff;
}
.banking-action--primary svg:last-child {
margin-left: auto;
opacity: 0.78;
}
.banking-action--secondary {
justify-content: flex-start;
}
.banking-card {
margin: 0 0 18px !important;
padding: 16px !important;
border: 1px solid rgb(255 255 255 / 10%) !important;
border-radius: 24px !important;
background: linear-gradient(145deg, rgb(255 255 255 / 8%), rgb(255 255 255 / 3%)) !important;
box-shadow: inset 0 1px rgb(255 255 255 / 9%), 0 18px 38px rgb(0 0 0 / 18%) !important;
color: #fff !important;
backdrop-filter: blur(24px) saturate(140%);
-webkit-backdrop-filter: blur(24px) saturate(140%);
}
.banking-transaction-card {
overflow: hidden;
}
.banking-section-title {
@@ -1425,140 +1395,22 @@ button {
font-size: 10px;
}
.banking-account-row {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border-radius: 17px;
.banking-account-list,
.banking-transaction-list {
margin: 0 !important;
}
.banking-account-row + .banking-account-row { margin-top: 7px; }
.banking-account-row--primary {
position: relative;
overflow: hidden;
border: 1px solid rgb(125 174 255 / 17%);
background: linear-gradient(120deg, rgb(37 103 230 / 38%), rgb(36 72 143 / 15%));
}
.banking-account-row > span:first-child {
display: grid;
width: 36px;
height: 36px;
flex: 0 0 auto;
place-items: center;
border-radius: 12px;
background: rgb(255 255 255 / 9%);
color: #9fc0ff;
}
.banking-account-row div {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
}
.banking-account-row small {
color: rgb(216 227 250 / 54%);
font-size: 9px;
}
.banking-account-row strong {
margin-top: 2px;
font-size: 16px;
font-weight: 620;
}
.banking-card-mark {
position: relative;
width: 32px !important;
background: transparent !important;
}
.banking-card-mark::before,
.banking-card-mark::after {
position: absolute;
width: 20px;
height: 20px;
top: 8px;
border-radius: 50%;
background: rgb(255 255 255 / 64%);
content: '';
}
.banking-card-mark::before { left: 0; }
.banking-card-mark::after { right: 0; background: rgb(120 164 255 / 65%); }
.banking-transactions { padding: 0 3px; }
.banking-transactions--all { margin-top: 18px; }
.banking-transaction-list {
overflow: hidden;
border: 1px solid rgb(255 255 255 / 8%);
border-radius: 22px;
background: rgb(255 255 255 / 3.5%);
backdrop-filter: blur(18px);
-webkit-backdrop-filter: blur(18px);
}
.banking-transaction {
display: flex;
align-items: center;
gap: 11px;
padding: 12px;
}
.banking-transaction + .banking-transaction {
border-top: 1px solid rgb(255 255 255 / 6%);
}
.banking-transaction > span {
display: grid;
width: 36px;
height: 36px;
flex: 0 0 auto;
place-items: center;
border-radius: 12px;
background: rgb(255 106 120 / 10%);
color: #ff8793;
}
.banking-transaction > span.is-incoming {
background: rgb(78 230 164 / 10%);
color: var(--bank-green);
}
.banking-transaction > div {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
}
.banking-transaction strong {
overflow: hidden;
font-size: 11px;
font-weight: 570;
text-overflow: ellipsis;
white-space: nowrap;
}
.banking-transaction small {
margin-top: 2px;
color: rgb(210 221 245 / 43%);
font-size: 8px;
}
.banking-transaction b {
.banking-transaction-list b {
flex: 0 0 auto;
color: #f2a0a8;
font-size: 11px;
font-weight: 620;
}
.banking-transaction b.is-incoming { color: var(--bank-green); }
.banking-transaction-list b.is-incoming { color: var(--bank-green); }
.banking-no-transactions {
padding: 24px 12px;
@@ -1570,39 +1422,6 @@ button {
text-align: center;
}
.banking-tabbar {
position: absolute;
z-index: 10;
right: 16px;
bottom: 22px;
left: 16px;
width: auto;
padding: 0;
}
.banking-tabbar__background {
border: 1px solid rgb(255 255 255 / 13%);
border-radius: 24px;
background: rgb(14 22 45 / 72%);
box-shadow: inset 0 1px rgb(255 255 255 / 12%), 0 16px 48px rgb(0 0 0 / 48%);
backdrop-filter: blur(30px) saturate(165%);
-webkit-backdrop-filter: blur(30px) saturate(165%);
}
.banking-tabbar__inner {
gap: 4px;
padding: 7px;
}
.banking-tabbar .k-link {
color: rgb(210 222 247 / 48%);
font-size: 10px;
}
.banking-tabbar .k-tabbar-link-active {
color: #93b9ff;
}
.banking-activity-hero {
display: flex;
align-items: center;
@@ -1693,30 +1512,10 @@ button {
font-size: 8px;
}
.banking-modal-backdrop {
position: absolute;
z-index: 30;
inset: 0;
display: flex;
align-items: flex-end;
padding: 14px 14px 20px;
background: rgb(0 3 12 / 58%);
backdrop-filter: blur(9px);
-webkit-backdrop-filter: blur(9px);
}
.banking-modal {
.banking-sheet__content {
position: relative;
width: 100%;
padding: 22px 18px 18px;
border: 1px solid rgb(255 255 255 / 14%);
border-radius: 29px;
background:
radial-gradient(circle at 16% 0%, rgb(47 117 255 / 24%), transparent 42%),
rgb(13 22 45 / 92%);
box-shadow: inset 0 1px rgb(255 255 255 / 14%), 0 24px 70px rgb(0 0 0 / 55%);
backdrop-filter: blur(34px) saturate(165%);
-webkit-backdrop-filter: blur(34px) saturate(165%);
}
.banking-modal__close {
@@ -1742,75 +1541,30 @@ button {
box-shadow: 0 12px 28px rgb(22 76 196 / 36%), inset 0 1px rgb(255 255 255 / 28%);
}
.banking-modal h2 {
.banking-sheet__content h2 {
margin: 14px 0 4px;
font-size: 20px;
letter-spacing: -0.035em;
}
.banking-modal > p {
.banking-sheet__content > p {
margin: 0 0 15px;
color: rgb(213 225 248 / 52%);
font-size: 10px;
line-height: 1.45;
}
.banking-modal label {
display: block;
margin-top: 11px;
}
.banking-form-list { margin: 12px 0 0 !important; }
.banking-modal label > span {
display: block;
margin: 0 0 6px 4px;
color: rgb(218 229 251 / 62%);
font-size: 9px;
}
.banking-modal input {
width: 100%;
height: 48px;
padding: 0 14px;
border: 1px solid rgb(255 255 255 / 10%);
border-radius: 15px;
outline: none;
background: rgb(255 255 255 / 5%);
color: #fff;
font-size: 14px;
}
.banking-modal input:focus {
border-color: rgb(72 137 255 / 62%);
box-shadow: 0 0 0 3px rgb(45 118 255 / 11%);
}
.banking-amount-input { position: relative; }
.banking-amount-input i {
position: absolute;
top: 50%;
left: 14px;
color: #7faaff;
font-size: 15px;
font-style: normal;
transform: translateY(-50%);
}
.banking-amount-input input { padding-left: 31px; }
.banking-modal .banking-form-error {
.banking-sheet__content .banking-form-error {
margin: 10px 3px 0;
color: #ff8b98;
font-size: 9px;
}
.banking-modal > button:last-child {
margin-top: 15px;
background: linear-gradient(135deg, #397fff, #1a55d1) !important;
box-shadow: 0 12px 28px rgb(25 78 190 / 30%);
}
.banking-sheet__content > button:last-child { margin-top: 15px; }
.banking-modal > button:last-child > span {
.banking-sheet__content > button:last-child > span {
display: flex;
align-items: center;
justify-content: center;
+160 -126
View File
@@ -2,10 +2,18 @@
import {
kButton,
kCard,
kGlass,
kIcon,
kList,
kListInput,
kListItem,
kNavbar,
kPage,
kPreloader,
kSheet,
kTabbar,
kTabbarLink,
kToolbarPane,
} from 'konsta/vue'
import {
ArrowDownLeft,
@@ -162,27 +170,29 @@ onMounted(() => void banking.load())
<template>
<k-page
component="main"
class="banking-app"
class="banking-app pb-safe-24"
:colors="{ bgIos: 'bg-transparent' }"
>
<div class="banking-app__aurora" aria-hidden="true"></div>
<header class="banking-header">
<div>
<span>{{ phone.t('Apps.banking.welcome') }}</span>
<h1>{{ banking.overview?.playerName ?? phone.t('Common.loading') }}</h1>
</div>
<button
type="button"
class="banking-profile"
:aria-label="phone.t('Apps.banking.refresh')"
:disabled="banking.isLoading"
@click="banking.load()"
>
<Landmark :size="20" />
<RefreshCw v-if="banking.isLoading" class="banking-spin" :size="10" />
</button>
</header>
<k-navbar
class="banking-navbar"
:subtitle="phone.t('Apps.banking.welcome')"
:title="banking.overview?.playerName ?? phone.t('Common.loading')"
>
<template #right>
<button
type="button"
class="banking-profile"
:aria-label="phone.t('Apps.banking.refresh')"
:disabled="banking.isLoading"
@click="banking.load()"
>
<Landmark :size="20" />
<RefreshCw v-if="banking.isLoading" class="banking-spin" :size="10" />
</button>
</template>
</k-navbar>
<div v-if="!banking.overview && banking.isLoading" class="banking-loading">
<k-preloader />
@@ -200,7 +210,7 @@ onMounted(() => void banking.load())
<div v-else class="banking-scroll">
<template v-if="activeTab === 'home'">
<section class="banking-balance">
<k-glass class="banking-balance">
<div class="banking-balance__label">
<span>{{ phone.t('Apps.banking.totalBalance') }}</span>
<small>#{{ banking.overview.playerId }}</small>
@@ -210,43 +220,57 @@ onMounted(() => void banking.load())
<span>{{ formatMoney(totals.incoming - totals.outgoing, true) }}</span>
{{ phone.t('Apps.banking.recentPeriod') }}
</div>
</section>
</k-glass>
<section class="banking-actions" :aria-label="phone.t('Apps.banking.actions')">
<button type="button" @click="openAction('transfer')">
<span><Send :size="20" /></span>
{{ phone.t('Apps.banking.send') }}
</button>
<button type="button" @click="openAction('deposit')">
<span><ArrowDownLeft :size="20" /></span>
{{ phone.t('Apps.banking.deposit') }}
</button>
<button type="button" @click="openAction('withdraw')">
<span><ArrowUpRight :size="20" /></span>
{{ phone.t('Apps.banking.withdraw') }}
</button>
<k-glass
component="button"
type="button"
class="banking-action banking-action--primary"
@click="openAction('transfer')"
>
<span class="banking-action__icon"><Send :size="20" /></span>
<b>{{ phone.t('Apps.banking.send') }}</b>
<ChevronRight :size="16" aria-hidden="true" />
</k-glass>
<k-glass
component="button"
type="button"
class="banking-action banking-action--secondary"
@click="openAction('deposit')"
>
<span class="banking-action__icon"><ArrowDownLeft :size="20" /></span>
<b>{{ phone.t('Apps.banking.deposit') }}</b>
</k-glass>
<k-glass
component="button"
type="button"
class="banking-action banking-action--secondary"
@click="openAction('withdraw')"
>
<span class="banking-action__icon"><ArrowUpRight :size="20" /></span>
<b>{{ phone.t('Apps.banking.withdraw') }}</b>
</k-glass>
</section>
<k-card :content-wrap="false" class="banking-card banking-accounts">
<k-card class="banking-card banking-accounts">
<div class="banking-section-title">
<h2>{{ phone.t('Apps.banking.accounts') }}</h2>
<ChevronRight :size="17" />
</div>
<div class="banking-account-row banking-account-row--primary">
<span><WalletCards :size="18" /></span>
<div>
<small>{{ phone.t('Apps.banking.bankAccount') }}</small>
<strong>{{ formatMoney(banking.overview.bank) }}</strong>
</div>
<span class="banking-card-mark" aria-hidden="true"></span>
</div>
<div class="banking-account-row">
<span><CircleDollarSign :size="18" /></span>
<div>
<small>{{ phone.t('Apps.banking.cash') }}</small>
<strong>{{ formatMoney(banking.overview.cash) }}</strong>
</div>
</div>
<k-list inset strong class="banking-account-list">
<k-list-item
:title="phone.t('Apps.banking.bankAccount')"
:after="formatMoney(banking.overview.bank)"
>
<template #media><WalletCards :size="18" /></template>
</k-list-item>
<k-list-item
:title="phone.t('Apps.banking.cash')"
:after="formatMoney(banking.overview.cash)"
>
<template #media><CircleDollarSign :size="18" /></template>
</k-list-item>
</k-list>
</k-card>
<section class="banking-transactions">
@@ -256,24 +280,29 @@ onMounted(() => void banking.load())
{{ phone.t('Apps.banking.viewAll') }}
</button>
</div>
<div v-if="banking.overview.transactions.length" class="banking-transaction-list">
<article
<k-card
v-if="banking.overview.transactions.length"
:content-wrap="false"
class="banking-card banking-transaction-card"
>
<k-list inset strong class="banking-transaction-list">
<k-list-item
v-for="transaction in banking.overview.transactions.slice(0, 5)"
:key="transaction.id"
class="banking-transaction"
>
<span :class="{ 'is-incoming': isIncoming(transaction.kind) }">
<component :is="transactionIcons[transaction.kind]" :size="17" />
</span>
<div>
<strong>{{ transactionTitle(transaction) }}</strong>
<small>{{ formatDate(transaction.createdAt) }}</small>
</div>
<b :class="{ 'is-incoming': isIncoming(transaction.kind) }">
{{ formatMoney(isIncoming(transaction.kind) ? transaction.amount : -transaction.amount, true) }}
</b>
</article>
</div>
:subtitle="formatDate(transaction.createdAt)"
:title="transactionTitle(transaction)"
>
<template #media>
<component :is="transactionIcons[transaction.kind]" :size="17" />
</template>
<template #after>
<b :class="{ 'is-incoming': isIncoming(transaction.kind) }">
{{ formatMoney(isIncoming(transaction.kind) ? transaction.amount : -transaction.amount, true) }}
</b>
</template>
</k-list-item>
</k-list>
</k-card>
<p v-else class="banking-no-transactions">
{{ phone.t('Apps.banking.noTransactions') }}
</p>
@@ -307,24 +336,25 @@ onMounted(() => void banking.load())
<div class="banking-section-title">
<h2>{{ phone.t('Apps.banking.allTransactions') }}</h2>
</div>
<div class="banking-transaction-list">
<article
v-for="transaction in banking.overview.transactions"
:key="transaction.id"
class="banking-transaction"
>
<span :class="{ 'is-incoming': isIncoming(transaction.kind) }">
<component :is="transactionIcons[transaction.kind]" :size="17" />
</span>
<div>
<strong>{{ transactionTitle(transaction) }}</strong>
<small>{{ formatDate(transaction.createdAt) }}</small>
</div>
<b :class="{ 'is-incoming': isIncoming(transaction.kind) }">
{{ formatMoney(isIncoming(transaction.kind) ? transaction.amount : -transaction.amount, true) }}
</b>
</article>
</div>
<k-card :content-wrap="false" class="banking-card banking-transaction-card">
<k-list inset strong class="banking-transaction-list">
<k-list-item
v-for="transaction in banking.overview.transactions"
:key="transaction.id"
:subtitle="formatDate(transaction.createdAt)"
:title="transactionTitle(transaction)"
>
<template #media>
<component :is="transactionIcons[transaction.kind]" :size="17" />
</template>
<template #after>
<b :class="{ 'is-incoming': isIncoming(transaction.kind) }">
{{ formatMoney(isIncoming(transaction.kind) ? transaction.amount : -transaction.amount, true) }}
</b>
</template>
</k-list-item>
</k-list>
</k-card>
</section>
</template>
</div>
@@ -334,33 +364,37 @@ onMounted(() => void banking.load())
component="nav"
icons
labels
class="banking-tabbar"
bg-class="banking-tabbar__background"
inner-class="banking-tabbar__inner"
class="bottom-0 left-0 fixed"
:aria-label="phone.t('Apps.banking.navigation')"
>
<k-tabbar-link
component="button"
:active="activeTab === 'home'"
:label="phone.t('Apps.banking.home')"
:link-props="{ type: 'button' }"
@click="activeTab = 'home'"
>
<template #icon><House :size="19" /></template>
</k-tabbar-link>
<k-tabbar-link
component="button"
:active="activeTab === 'activity'"
:label="phone.t('Apps.banking.activity')"
:link-props="{ type: 'button' }"
@click="activeTab = 'activity'"
>
<template #icon><BarChart3 :size="19" /></template>
</k-tabbar-link>
<k-toolbar-pane>
<k-tabbar-link
component="button"
:active="activeTab === 'home'"
:link-props="{ type: 'button' }"
@click="activeTab = 'home'"
>
<template #label>{{ phone.t('Apps.banking.home') }}</template>
<template #icon>
<k-icon><House class="w-7 h-7" /></k-icon>
</template>
</k-tabbar-link>
<k-tabbar-link
component="button"
:active="activeTab === 'activity'"
:link-props="{ type: 'button' }"
@click="activeTab = 'activity'"
>
<template #label>{{ phone.t('Apps.banking.activity') }}</template>
<template #icon>
<k-icon><BarChart3 class="w-7 h-7" /></k-icon>
</template>
</k-tabbar-link>
</k-toolbar-pane>
</k-tabbar>
<div v-if="action" class="banking-modal-backdrop" @click.self="closeAction">
<section class="banking-modal" role="dialog" aria-modal="true">
<k-sheet :opened="Boolean(action)" class="banking-sheet" @backdropclick="closeAction">
<section v-if="action" class="banking-sheet__content" role="dialog" aria-modal="true">
<button
type="button"
class="banking-modal__close"
@@ -376,30 +410,30 @@ onMounted(() => void banking.load())
</span>
<h2>{{ phone.t(`Apps.banking.forms.${action}.title`) }}</h2>
<p>{{ phone.t(`Apps.banking.forms.${action}.body`) }}</p>
<label v-if="action === 'transfer'">
<span>{{ phone.t('Apps.banking.playerId') }}</span>
<input
v-model="target"
<k-list inset strong class="banking-form-list">
<k-list-input
v-if="action === 'transfer'"
:label="phone.t('Apps.banking.playerId')"
inputmode="numeric"
min="1"
type="number"
outline
:placeholder="phone.t('Apps.banking.playerIdPlaceholder')"
type="number"
:value="target"
@input="target = $event"
/>
</label>
<label>
<span>{{ phone.t('Apps.banking.amount') }}</span>
<div class="banking-amount-input">
<i>{{ banking.overview?.currency }}</i>
<input
v-model="amount"
inputmode="numeric"
min="1"
type="number"
:placeholder="phone.t('Apps.banking.amountPlaceholder')"
@keydown.enter="submitAction"
/>
</div>
</label>
<k-list-input
:label="phone.t('Apps.banking.amount')"
inputmode="numeric"
min="1"
outline
:placeholder="phone.t('Apps.banking.amountPlaceholder')"
type="number"
:value="amount"
@input="amount = $event"
@keydown.enter="submitAction"
/>
</k-list>
<p v-if="formError" class="banking-form-error">{{ formError }}</p>
<k-button
large
@@ -414,6 +448,6 @@ onMounted(() => void banking.load())
</template>
</k-button>
</section>
</div>
</k-sheet>
</k-page>
</template>
+2 -2
View File
@@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sky Phone</title>
<script type="module" crossorigin src="./assets/sky-index-CsAxDiPY.js"></script>
<link rel="stylesheet" crossorigin href="./assets/sky-index-D9Q9SXH9.css">
<script type="module" crossorigin src="./assets/sky-index-i5PQ5yRP.js"></script>
<link rel="stylesheet" crossorigin href="./assets/sky-index-p1qSFvW3.css">
</head>
<body>
<div id="app"></div>