mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 01:08:59 +00:00
97db163cd0
* FIX - repair inventory metadata and ESX contracts Use the stable core_inventory metadata setter and validate the complete inventory adapter contract after provider bridges load. Preserve the real ESX configuration error instead of cascading into a missing RegisterUsableItem failure. * FIX - clarify phone opening without SIM * FIX - harden inventory and device contracts Validate SIM number configuration before item handling, preserve QB metadata mutation contracts, and propagate phone-open failures to callers. Extend regression coverage for inventory bridges and device bootstrap errors. * FIX - resolve reported phone interaction issues Stop game-input passthrough while NUI text fields are focused and add an independent hold-to-look option. Propagate SIM number formatting to the frontend, migrate configured currency symbols to utf8mb4, and clarify SkyRide, VaultX, and DarkChat behavior with focused contract coverage. * FIX - detect missing packaged phone UI Validate the generated NUI entrypoint, its referenced assets, and required static media when the resource starts. Print an actionable server-console warning for incomplete source archives, package phone sounds through the manifest, and cover the detection contract with Lua regressions.
4656 lines
119 KiB
Vue
4656 lines
119 KiB
Vue
<script setup lang="ts">
|
||
import {
|
||
ArrowDownLeft,
|
||
ArrowUpRight,
|
||
BellRing,
|
||
ChartCandlestick,
|
||
ChartNoAxesCombined,
|
||
CheckCircle2,
|
||
ChevronRight,
|
||
Copy,
|
||
Eye,
|
||
EyeOff,
|
||
Fingerprint,
|
||
History,
|
||
KeyRound,
|
||
LockKeyhole,
|
||
LogOut,
|
||
Mail,
|
||
Settings2,
|
||
Send,
|
||
Share2,
|
||
ShieldCheck,
|
||
Sparkles,
|
||
UserRound,
|
||
WalletCards,
|
||
} from 'lucide-vue-next'
|
||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||
import cryptoHeaderLogo from '@/assets/img/app-icons/crypto-header-logo.webp'
|
||
import CryptoLogo from '@/components/crypto/CryptoLogo.vue'
|
||
import { useAccountStore } from '@/stores/account'
|
||
import { useCryptoStore } from '@/stores/crypto'
|
||
import { useEasyShareStore } from '@/stores/easyshare'
|
||
import { usePhoneStore } from '@/stores/phone'
|
||
import type {
|
||
CryptoActivity,
|
||
CryptoMarket,
|
||
CryptoRecipient,
|
||
CryptoSide,
|
||
} from '@/types/crypto'
|
||
import {
|
||
SkyAppPage,
|
||
SkyButton,
|
||
SkyCard,
|
||
SkyDialog,
|
||
SkyDialogButton,
|
||
SkyEmptyState,
|
||
SkyField,
|
||
SkyLink,
|
||
SkyNavbar,
|
||
SkyPillNavigation,
|
||
SkyScrollArea,
|
||
SkySegmented,
|
||
SkySegmentedButton,
|
||
SkySheet,
|
||
SkySpinner,
|
||
SkyStatusCard,
|
||
SkyToggle,
|
||
} from '@/ui'
|
||
import { copyText } from '@/utils/clipboard'
|
||
|
||
type Tab = 'portfolio' | 'markets' | 'activity' | 'profile'
|
||
type Sheet = 'trade' | 'deposit' | 'withdraw' | 'profile' | 'send' | null
|
||
type ChartPeriod = '1D' | '1W' | '1M' | '6M' | '1Y'
|
||
|
||
const CHART_PERIODS: ChartPeriod[] = ['1D', '1W', '1M', '6M', '1Y']
|
||
const CHART_PERIOD_CONFIG: Record<
|
||
ChartPeriod,
|
||
{ duration: number; samples: number }
|
||
> = {
|
||
'1D': { duration: 86_400_000, samples: 16 },
|
||
'1W': {
|
||
duration: 7 * 86_400_000,
|
||
samples: 24,
|
||
},
|
||
'1M': {
|
||
duration: 30 * 86_400_000,
|
||
samples: 32,
|
||
},
|
||
'6M': {
|
||
duration: 183 * 86_400_000,
|
||
samples: 40,
|
||
},
|
||
'1Y': {
|
||
duration: 365 * 86_400_000,
|
||
samples: 48,
|
||
},
|
||
}
|
||
const crypto = useCryptoStore()
|
||
const account = useAccountStore()
|
||
const easyShare = useEasyShareStore()
|
||
const phone = usePhoneStore()
|
||
let marketPreviewTimer: number | undefined
|
||
const tab = ref<Tab>('portfolio')
|
||
const authMode = ref<'login' | 'register'>('login')
|
||
const activityFilter = ref<'all' | 'trades' | 'wallet'>('all')
|
||
const detail = ref<CryptoMarket | null>(null)
|
||
const sheet = ref<Sheet>(null)
|
||
const selectedMarket = ref<CryptoMarket | null>(null)
|
||
const side = ref<CryptoSide>('buy')
|
||
const handle = ref('')
|
||
const password = ref('')
|
||
const amount = ref('')
|
||
const financialPassword = ref('')
|
||
const showPassword = ref(false)
|
||
const formError = ref('')
|
||
const profileHandle = ref('')
|
||
const profileCurrentPassword = ref('')
|
||
const profileNewPassword = ref('')
|
||
const priceAlerts = ref(true)
|
||
const confirmations = ref(true)
|
||
const hideBalances = ref(false)
|
||
const saved = ref(false)
|
||
const period = ref<ChartPeriod>('1D')
|
||
const transferWalletKey = ref('')
|
||
const transferMarketId = ref('')
|
||
const transferQuantity = ref('')
|
||
const transferPassword = ref('')
|
||
const transferRecipient = ref<CryptoRecipient | null>(null)
|
||
const walletKeyCopied = ref(false)
|
||
const logoutDialogOpen = ref(false)
|
||
const logoutSubmitting = ref(false)
|
||
|
||
const locale = computed(() => phone.lang || 'de')
|
||
const authenticated = computed(() => crypto.data?.authenticated === true)
|
||
const profile = computed(() => crypto.data?.profile ?? null)
|
||
const markets = computed(() => crypto.data?.markets ?? [])
|
||
const holdings = computed(() => crypto.data?.holdings ?? [])
|
||
const activities = computed(() =>
|
||
(crypto.data?.activity ?? []).filter(
|
||
(item) =>
|
||
activityFilter.value === 'all' ||
|
||
(activityFilter.value === 'trades'
|
||
? ['buy', 'sell'].includes(item.type)
|
||
: ['deposit', 'withdrawal', 'transfer_in', 'transfer_out'].includes(
|
||
item.type,
|
||
)),
|
||
),
|
||
)
|
||
const transferMarket = computed(() => market(transferMarketId.value))
|
||
const transferHolding = computed(() =>
|
||
holdings.value.find((item) => item.assetId === transferMarketId.value),
|
||
)
|
||
const investedValue = computed(() =>
|
||
Math.max(
|
||
0,
|
||
Number(crypto.data?.portfolioValue ?? 0) -
|
||
Number(crypto.data?.cashBalance ?? 0),
|
||
),
|
||
)
|
||
const portfolioCostBasis = computed(() =>
|
||
holdings.value.reduce(
|
||
(sum, holding) =>
|
||
sum + Number(holding.quantity) * Number(holding.averagePrice),
|
||
0,
|
||
),
|
||
)
|
||
const portfolioProfitLoss = computed(
|
||
() => investedValue.value - portfolioCostBasis.value,
|
||
)
|
||
const portfolioProfitPercent = computed(() =>
|
||
portfolioCostBasis.value > 0
|
||
? (portfolioProfitLoss.value / portfolioCostBasis.value) * 100
|
||
: 0,
|
||
)
|
||
const portfolioChart = computed(() => {
|
||
const width = 320
|
||
const height = 126
|
||
const startX = 18
|
||
const currentX = 224
|
||
const forecastEndX = 306
|
||
const sampleCount = 10
|
||
const weightedMarkets = holdings.value
|
||
.map((holding) => ({
|
||
cost: Number(holding.quantity) * Number(holding.averagePrice),
|
||
market: markets.value.find((item) => item.id === holding.assetId),
|
||
}))
|
||
.filter((item): item is { cost: number; market: CryptoMarket } =>
|
||
Boolean(item.market?.sparkline.length),
|
||
)
|
||
const totalWeight =
|
||
weightedMarkets.reduce((sum, item) => sum + item.cost, 0) || 1
|
||
const reference = Array.from({ length: sampleCount }, (_, index) =>
|
||
weightedMarkets.reduce((sum, item) => {
|
||
const sourceIndex = Math.round(
|
||
(index / (sampleCount - 1)) * (item.market.sparkline.length - 1),
|
||
)
|
||
return (
|
||
sum + item.market.sparkline[sourceIndex] * (item.cost / totalWeight)
|
||
)
|
||
}, 0),
|
||
)
|
||
const currentProfit = portfolioProfitLoss.value
|
||
const referenceEnd = reference[reference.length - 1] ?? 0.5
|
||
const amplitude = Math.max(
|
||
portfolioCostBasis.value * 0.045,
|
||
Math.abs(currentProfit) * 0.38,
|
||
40,
|
||
)
|
||
const historyValues = reference.map(
|
||
(value) => currentProfit + (value - referenceEnd) * amplitude,
|
||
)
|
||
historyValues[historyValues.length - 1] = currentProfit
|
||
|
||
const recentMomentum =
|
||
currentProfit - (historyValues[historyValues.length - 3] ?? currentProfit)
|
||
const forecastValues = [
|
||
currentProfit,
|
||
currentProfit + recentMomentum * 0.42,
|
||
currentProfit + recentMomentum * 0.58 + amplitude * 0.035,
|
||
currentProfit + recentMomentum * 0.48 - amplitude * 0.018,
|
||
]
|
||
const extent = Math.max(
|
||
50,
|
||
...historyValues.map((value) => Math.abs(value)),
|
||
...forecastValues.map((value) => Math.abs(value)),
|
||
)
|
||
const scale = Math.ceil((extent * 1.18) / 50) * 50
|
||
const chartTop = 15
|
||
const chartBottom = height - 16
|
||
const chartRange = chartBottom - chartTop
|
||
const y = (value: number) =>
|
||
chartTop + ((scale - value) / (scale * 2)) * chartRange
|
||
const historyPoints = historyValues
|
||
.map(
|
||
(value, index) =>
|
||
`${
|
||
startX + index * ((currentX - startX) / (historyValues.length - 1))
|
||
},${y(value)}`,
|
||
)
|
||
.join(' ')
|
||
const forecastPoints = forecastValues
|
||
.map(
|
||
(value, index) =>
|
||
`${
|
||
currentX +
|
||
index * ((forecastEndX - currentX) / (forecastValues.length - 1))
|
||
},${y(value)}`,
|
||
)
|
||
.join(' ')
|
||
|
||
return {
|
||
currentX,
|
||
currentY: y(currentProfit),
|
||
forecastPoints,
|
||
height,
|
||
historyPoints,
|
||
labelY: Math.max(3, Math.min(height - 29, y(currentProfit) - 27)),
|
||
scale,
|
||
width,
|
||
zeroY: y(0),
|
||
}
|
||
})
|
||
const cashShare = computed(() => {
|
||
const total = Number(crypto.data?.portfolioValue ?? 0)
|
||
return total > 0
|
||
? Math.min(100, (Number(crypto.data?.cashBalance ?? 0) / total) * 100)
|
||
: 0
|
||
})
|
||
const topMover = computed(
|
||
() =>
|
||
[...markets.value].sort(
|
||
(first, second) => second.changePercent - first.changePercent,
|
||
)[0],
|
||
)
|
||
const detailHolding = computed(() =>
|
||
holdings.value.find((item) => item.assetId === detail.value?.id),
|
||
)
|
||
const selectedHolding = computed(() =>
|
||
holdings.value.find((item) => item.assetId === selectedMarket.value?.id),
|
||
)
|
||
const detailChart = computed(() => {
|
||
const left = 52
|
||
const right = 307
|
||
const top = 14
|
||
const bottom = 137
|
||
const config = CHART_PERIOD_CONFIG[period.value]
|
||
const selected = detail.value
|
||
|
||
if (!selected) {
|
||
return {
|
||
areaPoints: '',
|
||
changePercent: 0,
|
||
marker: { labelY: top, x: right, y: bottom },
|
||
points: '',
|
||
xTicks: [],
|
||
yTicks: [],
|
||
}
|
||
}
|
||
|
||
const currentPrice = Number(selected.price)
|
||
const history = (selected.priceHistory ?? [])
|
||
.map(Number)
|
||
.filter((value) => Number.isFinite(value) && value > 0)
|
||
const values = (
|
||
history.length ? history : [currentPrice, currentPrice]
|
||
).slice(-config.samples)
|
||
if (values.length === 1) values.unshift(values[0])
|
||
const startPrice = values[0] ?? currentPrice
|
||
values[values.length - 1] = currentPrice
|
||
|
||
const minimum = Math.min(...values)
|
||
const maximum = Math.max(...values)
|
||
const padding = Math.max((maximum - minimum) * 0.16, currentPrice * 0.012)
|
||
const axisMinimum = Math.max(0, minimum - padding)
|
||
const axisMaximum = maximum + padding
|
||
const axisRange = Math.max(axisMaximum - axisMinimum, currentPrice * 0.02)
|
||
const x = (ratio: number) => left + ratio * (right - left)
|
||
const y = (value: number) =>
|
||
top + ((axisMaximum - value) / axisRange) * (bottom - top)
|
||
const linePoints = values
|
||
.map((value, index) => `${x(index / (values.length - 1))},${y(value)}`)
|
||
.join(' ')
|
||
const markerY = y(currentPrice)
|
||
|
||
return {
|
||
areaPoints: `${left},${bottom} ${linePoints} ${right},${bottom}`,
|
||
changePercent: ((currentPrice - startPrice) / startPrice) * 100,
|
||
marker: {
|
||
labelY: markerY < 31 ? markerY + 19 : markerY - 10,
|
||
x: right,
|
||
y: markerY,
|
||
},
|
||
points: linePoints,
|
||
xTicks: Array.from({ length: 5 }, (_, index) => {
|
||
const ratio = index / 4
|
||
return {
|
||
label: chartTimeLabel(period.value, ratio, config.duration),
|
||
x: x(ratio),
|
||
}
|
||
}),
|
||
yTicks: Array.from({ length: 4 }, (_, index) => {
|
||
const ratio = index / 3
|
||
const value = axisMaximum - axisRange * ratio
|
||
return { label: chartMoney(value), value, y: y(value) }
|
||
}),
|
||
}
|
||
})
|
||
const estimatedTradeValue = computed(
|
||
() => Number(amount.value || 0) * Number(selectedMarket.value?.price || 0),
|
||
)
|
||
|
||
function t(key: string) {
|
||
return phone.t(`Apps.crypto.${key}`)
|
||
}
|
||
function money(value: string | number) {
|
||
const numericValue = Number(value) || 0
|
||
const absolute = Math.abs(numericValue)
|
||
return new Intl.NumberFormat(locale.value, {
|
||
currency: 'USD',
|
||
maximumFractionDigits: absolute > 0 && absolute < 1 ? 4 : 2,
|
||
minimumFractionDigits: 2,
|
||
style: 'currency',
|
||
}).format(numericValue)
|
||
}
|
||
function signedMoney(value: number) {
|
||
const formatted = money(Math.abs(value))
|
||
if (value > 0) return `+${formatted}`
|
||
if (value < 0) return `−${formatted}`
|
||
return formatted
|
||
}
|
||
function privateMoney(value: string | number) {
|
||
return profile.value?.hideBalances ? '••••••' : money(value)
|
||
}
|
||
function privateSignedMoney(value: number) {
|
||
return profile.value?.hideBalances ? '••••••' : signedMoney(value)
|
||
}
|
||
function quantity(value: string) {
|
||
return new Intl.NumberFormat(locale.value, {
|
||
maximumFractionDigits: 6,
|
||
}).format(Number(value) || 0)
|
||
}
|
||
function compact(value: string | number) {
|
||
return new Intl.NumberFormat(locale.value, {
|
||
maximumFractionDigits: 2,
|
||
notation: 'compact',
|
||
}).format(Number(value) || 0)
|
||
}
|
||
function chartMoney(value: number) {
|
||
const absolute = Math.abs(value)
|
||
return new Intl.NumberFormat(locale.value, {
|
||
currency: 'USD',
|
||
maximumFractionDigits:
|
||
absolute >= 1_000 ? 0 : absolute >= 1 ? 2 : absolute >= 0.01 ? 3 : 5,
|
||
notation: absolute >= 1_000_000 ? 'compact' : 'standard',
|
||
style: 'currency',
|
||
}).format(value)
|
||
}
|
||
function chartTimeLabel(
|
||
selectedPeriod: ChartPeriod,
|
||
ratio: number,
|
||
duration: number,
|
||
) {
|
||
const date = new Date(Date.now() - duration * (1 - ratio))
|
||
const options: Intl.DateTimeFormatOptions =
|
||
selectedPeriod === '1D'
|
||
? { hour: '2-digit', minute: '2-digit' }
|
||
: selectedPeriod === '1W'
|
||
? { weekday: 'short' }
|
||
: selectedPeriod === '1M'
|
||
? { day: '2-digit', month: '2-digit' }
|
||
: { month: 'short' }
|
||
|
||
return new Intl.DateTimeFormat(locale.value, options).format(date)
|
||
}
|
||
function points(values: number[], width = 320, height = 110) {
|
||
return values
|
||
.map(
|
||
(value, index) =>
|
||
`${index * (width / Math.max(1, values.length - 1))},${height - 7 - value * (height - 14)}`,
|
||
)
|
||
.join(' ')
|
||
}
|
||
function market(id?: string) {
|
||
return markets.value.find((item) => item.id === id)
|
||
}
|
||
function allocation(value: string) {
|
||
const total = Number(crypto.data?.portfolioValue ?? 0)
|
||
return total > 0 ? Math.min(100, (Number(value) / total) * 100) : 0
|
||
}
|
||
function errorText(code: string) {
|
||
const value = t(`errors.${code}`)
|
||
return value.startsWith('Apps.crypto.') ? t('errors.default') : value
|
||
}
|
||
function setTab(next: Tab) {
|
||
detail.value = null
|
||
tab.value = next
|
||
}
|
||
function openMarket(next: CryptoMarket) {
|
||
detail.value = next
|
||
period.value = '1D'
|
||
}
|
||
function openTrade(next: CryptoMarket, nextSide: CryptoSide = 'buy') {
|
||
selectedMarket.value = next
|
||
side.value = nextSide
|
||
amount.value = ''
|
||
formError.value = ''
|
||
crypto.pendingQuote = null
|
||
sheet.value = 'trade'
|
||
}
|
||
function selectTradeSide(next: CryptoSide) {
|
||
if (side.value === next) return
|
||
side.value = next
|
||
crypto.pendingQuote = null
|
||
formError.value = ''
|
||
}
|
||
function openSettlement(next: 'deposit' | 'withdraw') {
|
||
sheet.value = next
|
||
amount.value = ''
|
||
financialPassword.value = ''
|
||
formError.value = ''
|
||
}
|
||
function openSend() {
|
||
transferWalletKey.value = ''
|
||
transferMarketId.value = holdings.value[0]?.assetId ?? ''
|
||
transferQuantity.value = ''
|
||
transferPassword.value = ''
|
||
transferRecipient.value = null
|
||
formError.value = ''
|
||
sheet.value = 'send'
|
||
}
|
||
function openProfileEditor() {
|
||
profileHandle.value = profile.value?.handle ?? ''
|
||
profileCurrentPassword.value = ''
|
||
profileNewPassword.value = ''
|
||
formError.value = ''
|
||
saved.value = false
|
||
sheet.value = 'profile'
|
||
}
|
||
function closeSheet() {
|
||
sheet.value = null
|
||
crypto.pendingQuote = null
|
||
profileCurrentPassword.value = ''
|
||
profileNewPassword.value = ''
|
||
transferRecipient.value = null
|
||
transferPassword.value = ''
|
||
}
|
||
|
||
async function submitAuth() {
|
||
formError.value = ''
|
||
const success =
|
||
authMode.value === 'register'
|
||
? await crypto.register(handle.value.trim(), password.value)
|
||
: await crypto.login(password.value)
|
||
if (!success) formError.value = errorText(crypto.error)
|
||
else password.value = ''
|
||
}
|
||
async function resolveTransferRecipient() {
|
||
formError.value = ''
|
||
transferRecipient.value = null
|
||
const recipient = await crypto.resolveRecipient(transferWalletKey.value)
|
||
if (!recipient) formError.value = errorText(crypto.error)
|
||
else {
|
||
transferWalletKey.value = recipient.walletKey
|
||
transferRecipient.value = recipient
|
||
}
|
||
}
|
||
async function submitTransfer() {
|
||
formError.value = ''
|
||
if (
|
||
!transferRecipient.value ||
|
||
transferRecipient.value.walletKey !== transferWalletKey.value ||
|
||
!transferMarket.value ||
|
||
!/^\d+(?:\.\d{1,6})?$/.test(transferQuantity.value)
|
||
) {
|
||
formError.value = t('errors.invalid_transfer')
|
||
return
|
||
}
|
||
const success = await crypto.transfer({
|
||
marketId: transferMarket.value.id,
|
||
password: transferPassword.value,
|
||
quantity: transferQuantity.value,
|
||
walletKey: transferRecipient.value.walletKey,
|
||
})
|
||
if (!success) formError.value = errorText(crypto.error)
|
||
else closeSheet()
|
||
}
|
||
function copyWalletKey() {
|
||
if (!profile.value?.walletKey) return
|
||
walletKeyCopied.value = copyText(profile.value.walletKey)
|
||
if (walletKeyCopied.value) {
|
||
globalThis.setTimeout(() => (walletKeyCopied.value = false), 1800)
|
||
}
|
||
}
|
||
function shareWalletKey() {
|
||
if (!profile.value?.walletKey) return
|
||
easyShare.open({
|
||
appId: 'crypto',
|
||
copyText: `${t('profile.shareText')}\n${profile.value.walletKey}`,
|
||
id: profile.value.walletKey,
|
||
kind: 'text',
|
||
subtitle: `@${profile.value.handle}`,
|
||
title: t('profile.shareTitle'),
|
||
})
|
||
}
|
||
function activityValue(item: CryptoActivity) {
|
||
if (item.type === 'transfer_in' || item.type === 'transfer_out') {
|
||
return `${quantity(item.quantity ?? '0')} ${market(item.marketId)?.symbol ?? ''}`
|
||
}
|
||
return privateMoney(item.amount)
|
||
}
|
||
function activityIsDebit(item: CryptoActivity) {
|
||
return ['buy', 'withdrawal', 'transfer_out'].includes(item.type)
|
||
}
|
||
async function submitSettlement() {
|
||
if (sheet.value !== 'deposit' && sheet.value !== 'withdraw') return
|
||
if (!/^\d+$/.test(amount.value) || Number(amount.value) <= 0) {
|
||
formError.value = t('errors.invalid_amount')
|
||
return
|
||
}
|
||
if (
|
||
!(await crypto.settle(sheet.value, amount.value, financialPassword.value))
|
||
)
|
||
formError.value = errorText(crypto.error)
|
||
else closeSheet()
|
||
}
|
||
async function requestQuote() {
|
||
if (!selectedMarket.value || !/^\d+(?:\.\d{1,6})?$/.test(amount.value)) {
|
||
formError.value = t('errors.invalid_quantity')
|
||
return
|
||
}
|
||
if (
|
||
!(await crypto.quote(selectedMarket.value.id, side.value, amount.value))
|
||
.success
|
||
)
|
||
formError.value = errorText(crypto.error)
|
||
}
|
||
async function executeQuote() {
|
||
if (!(await crypto.executeQuote())) formError.value = errorText(crypto.error)
|
||
else closeSheet()
|
||
}
|
||
async function saveProfile() {
|
||
saved.value = false
|
||
formError.value = ''
|
||
const success = await crypto.updateProfile({
|
||
handle: profileHandle.value.trim(),
|
||
hideBalances: hideBalances.value,
|
||
currentPassword: profileCurrentPassword.value,
|
||
newPassword: profileNewPassword.value,
|
||
priceAlerts: priceAlerts.value,
|
||
tradeConfirmations: confirmations.value,
|
||
})
|
||
if (!success) formError.value = errorText(crypto.error)
|
||
else {
|
||
closeSheet()
|
||
saved.value = true
|
||
}
|
||
}
|
||
async function savePreferences() {
|
||
saved.value = false
|
||
formError.value = ''
|
||
const success = await crypto.updateProfile({
|
||
handle: profile.value?.handle ?? '',
|
||
hideBalances: hideBalances.value,
|
||
currentPassword: '',
|
||
newPassword: '',
|
||
priceAlerts: priceAlerts.value,
|
||
tradeConfirmations: confirmations.value,
|
||
})
|
||
if (!success) formError.value = errorText(crypto.error)
|
||
else saved.value = true
|
||
}
|
||
async function signOut() {
|
||
if (logoutSubmitting.value) return
|
||
logoutSubmitting.value = true
|
||
closeSheet()
|
||
try {
|
||
await crypto.logout()
|
||
if (!authenticated.value) {
|
||
logoutDialogOpen.value = false
|
||
tab.value = 'portfolio'
|
||
}
|
||
} finally {
|
||
logoutSubmitting.value = false
|
||
}
|
||
}
|
||
|
||
watch(
|
||
() => account.email,
|
||
(email) => {
|
||
if (!email || handle.value) return
|
||
handle.value = (email.split('@')[0] ?? '')
|
||
.replace(/[^a-zA-Z0-9._]/g, '')
|
||
.slice(0, 20)
|
||
},
|
||
{ immediate: true },
|
||
)
|
||
watch(
|
||
profile,
|
||
(value) => {
|
||
if (!value) return
|
||
profileHandle.value = value.handle
|
||
priceAlerts.value = value.priceAlerts
|
||
confirmations.value = value.tradeConfirmations
|
||
hideBalances.value = value.hideBalances
|
||
},
|
||
{ immediate: true },
|
||
)
|
||
watch(amount, () => {
|
||
if (sheet.value === 'trade' && crypto.pendingQuote) {
|
||
crypto.pendingQuote = null
|
||
formError.value = ''
|
||
}
|
||
})
|
||
watch(transferWalletKey, () => {
|
||
if (
|
||
transferRecipient.value?.walletKey !== transferWalletKey.value.toUpperCase()
|
||
) {
|
||
transferRecipient.value = null
|
||
}
|
||
})
|
||
watch(
|
||
() => crypto.data?.registered,
|
||
(registered) => {
|
||
if (!authenticated.value) authMode.value = registered ? 'login' : 'register'
|
||
},
|
||
{ immediate: true },
|
||
)
|
||
watch(markets, (value) => {
|
||
if (detail.value) {
|
||
detail.value =
|
||
value.find((market) => market.id === detail.value?.id) ?? null
|
||
}
|
||
if (selectedMarket.value) {
|
||
selectedMarket.value =
|
||
value.find((market) => market.id === selectedMarket.value?.id) ?? null
|
||
}
|
||
})
|
||
function scheduleDevelopmentMarketTick() {
|
||
if (!import.meta.env.DEV) return
|
||
marketPreviewTimer = window.setTimeout(
|
||
async () => {
|
||
await crypto.previewMarketTick()
|
||
scheduleDevelopmentMarketTick()
|
||
},
|
||
4500 + Math.random() * 2500,
|
||
)
|
||
}
|
||
onMounted(async () => {
|
||
await crypto.load()
|
||
scheduleDevelopmentMarketTick()
|
||
})
|
||
onUnmounted(() => {
|
||
if (marketPreviewTimer !== undefined) window.clearTimeout(marketPreviewTimer)
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<SkyAppPage
|
||
class="crypto-app"
|
||
accent="#31d6aa"
|
||
accent-soft="rgba(49,214,170,.16)"
|
||
dark
|
||
>
|
||
<SkyNavbar
|
||
v-if="authenticated"
|
||
:key="
|
||
detail ? `detail-${detail.id}` : `page-${authenticated ? tab : 'auth'}`
|
||
"
|
||
class="vault-navbar"
|
||
:class="{
|
||
'vault-navbar--profile': authenticated && tab === 'profile' && !detail,
|
||
}"
|
||
:title="detail?.symbol ?? (authenticated ? t(`tabs.${tab}`) : t('name'))"
|
||
:show-back="Boolean(detail)"
|
||
back-appearance="surface"
|
||
:back-label="t('marketDetail.back')"
|
||
@back="detail = null"
|
||
>
|
||
<template #title>
|
||
<span
|
||
v-if="!detail && (!authenticated || tab === 'portfolio')"
|
||
class="vault-header-brand"
|
||
>
|
||
<img class="vault-header-logo" :src="cryptoHeaderLogo" alt="" />
|
||
<span>{{ t('name') }}</span>
|
||
</span>
|
||
<span v-else-if="detail" class="vault-detail-title">
|
||
<CryptoLogo class="vault-detail-logo" :market="detail" />
|
||
<span>
|
||
<b>{{ detail.symbol }}</b>
|
||
<small>{{ detail.name }}</small>
|
||
</span>
|
||
</span>
|
||
<strong v-else class="vault-section-title">
|
||
{{ t(`tabs.${tab}`) }}
|
||
</strong>
|
||
</template>
|
||
<template v-if="authenticated && tab === 'profile' && !detail" #right>
|
||
<SkyLink
|
||
component="button"
|
||
class="profile-signout"
|
||
type="button"
|
||
:aria-label="t('logout')"
|
||
:title="t('logout')"
|
||
@click="logoutDialogOpen = true"
|
||
>
|
||
<LogOut :size="14" />
|
||
<span>{{ t('logout') }}</span>
|
||
</SkyLink>
|
||
</template>
|
||
</SkyNavbar>
|
||
|
||
<SkyScrollArea
|
||
v-if="crypto.isLoading && !crypto.data"
|
||
key="loading"
|
||
class="state vault-view"
|
||
padded
|
||
><SkySpinner />
|
||
<p>{{ t('loading') }}</p></SkyScrollArea
|
||
>
|
||
<template v-else-if="!authenticated">
|
||
<SkyScrollArea key="auth" class="auth vault-view" padded>
|
||
<div class="auth-shell">
|
||
<div class="auth-hero">
|
||
<span class="auth-hero__mark">
|
||
<img :src="cryptoHeaderLogo" alt="" />
|
||
</span>
|
||
<h2>
|
||
{{
|
||
t(
|
||
authMode === 'login'
|
||
? 'auth.loginTitle'
|
||
: 'auth.registerTitle',
|
||
)
|
||
}}
|
||
</h2>
|
||
</div>
|
||
<section class="auth-panel">
|
||
<SkySegmented
|
||
class="auth-mode"
|
||
strong
|
||
:active-index="authMode === 'login' ? 0 : 1"
|
||
:item-count="2"
|
||
><SkySegmentedButton
|
||
:active="authMode === 'login'"
|
||
@click="authMode = 'login'"
|
||
>{{ t('auth.login') }}</SkySegmentedButton
|
||
><SkySegmentedButton
|
||
:active="authMode === 'register'"
|
||
@click="authMode = 'register'"
|
||
>{{ t('auth.register') }}</SkySegmentedButton
|
||
></SkySegmented
|
||
>
|
||
<div class="auth-account">
|
||
<span><Mail :size="20" /></span>
|
||
<div>
|
||
<small>{{ t('auth.accountEmail') }}</small>
|
||
<strong>{{ account.email || t('auth.accountMissing') }}</strong>
|
||
</div>
|
||
<ShieldCheck :size="17" />
|
||
</div>
|
||
<form class="form auth-form" @submit.prevent="submitAuth">
|
||
<SkyField
|
||
v-if="authMode === 'register'"
|
||
v-model="handle"
|
||
class="auth-field"
|
||
:label="t('auth.handle')"
|
||
:placeholder="t('auth.handlePlaceholder')"
|
||
autocomplete="username"
|
||
maxlength="20"
|
||
outline
|
||
><template #leading><UserRound :size="18" /></template
|
||
></SkyField>
|
||
<SkyField
|
||
v-model="password"
|
||
class="auth-field"
|
||
:label="t('auth.password')"
|
||
:type="showPassword ? 'text' : 'password'"
|
||
:placeholder="t('auth.passwordPlaceholder')"
|
||
:autocomplete="
|
||
authMode === 'login' ? 'current-password' : 'new-password'
|
||
"
|
||
maxlength="72"
|
||
outline
|
||
><template #leading><LockKeyhole :size="18" /></template
|
||
><template #trailing
|
||
><button
|
||
class="visibility"
|
||
type="button"
|
||
@click="showPassword = !showPassword"
|
||
>
|
||
<EyeOff v-if="showPassword" :size="18" /><Eye
|
||
v-else
|
||
:size="18"
|
||
/></button></template
|
||
></SkyField>
|
||
<p v-if="authMode === 'register'" class="auth-password-hint">
|
||
{{ t('auth.ruleLength') }} · {{ t('auth.ruleMixed') }} ·
|
||
{{ t('auth.ruleNumber') }} · {{ t('auth.ruleSpecial') }}
|
||
</p>
|
||
<p v-if="formError" class="error">{{ formError }}</p>
|
||
<SkyButton block large class="auth-submit" type="submit">{{
|
||
t(
|
||
authMode === 'login'
|
||
? 'auth.loginAction'
|
||
: 'auth.registerAction',
|
||
)
|
||
}}</SkyButton>
|
||
</form>
|
||
</section>
|
||
</div>
|
||
</SkyScrollArea>
|
||
</template>
|
||
|
||
<SkyScrollArea
|
||
v-else-if="detail"
|
||
:key="`detail-${detail.id}`"
|
||
class="vault-view vault-detail-scroll"
|
||
with-tabbar
|
||
padded
|
||
>
|
||
<section class="detail-head">
|
||
<CryptoLogo class="coin large" :market="detail" />
|
||
<p>{{ detail.symbol }} · {{ detail.name }}</p>
|
||
<strong>{{ money(detail.price) }}</strong
|
||
><em :class="detailChart.changePercent >= 0 ? 'up' : 'down'"
|
||
>{{ detailChart.changePercent >= 0 ? '▲' : '▼' }}
|
||
{{ Math.abs(detailChart.changePercent).toFixed(2) }}% ·
|
||
{{ period === '1D' ? t('marketDetail.today') : period }}</em
|
||
>
|
||
</section>
|
||
<SkyCard class="big-chart"
|
||
><svg
|
||
:key="`${detail.id}-${period}`"
|
||
class="detail-chart"
|
||
viewBox="0 0 320 180"
|
||
preserveAspectRatio="none"
|
||
role="img"
|
||
:aria-label="`${detail.symbol} · ${period}`"
|
||
>
|
||
<defs>
|
||
<linearGradient id="fill" x1="0" y1="0" x2="0" y2="1">
|
||
<stop offset="0" stop-color="#31d6aa" stop-opacity=".4" />
|
||
<stop offset="1" stop-color="#31d6aa" stop-opacity="0" />
|
||
</linearGradient>
|
||
</defs>
|
||
<g class="detail-chart__grid">
|
||
<line
|
||
v-for="tick in detailChart.yTicks"
|
||
:key="tick.y"
|
||
x1="52"
|
||
:y1="tick.y"
|
||
x2="307"
|
||
:y2="tick.y"
|
||
/>
|
||
<line
|
||
v-for="tick in detailChart.xTicks"
|
||
:key="tick.x"
|
||
:x1="tick.x"
|
||
y1="14"
|
||
:x2="tick.x"
|
||
y2="137"
|
||
/>
|
||
</g>
|
||
<g class="detail-chart__axis">
|
||
<text
|
||
v-for="tick in detailChart.yTicks"
|
||
:key="tick.label"
|
||
x="47"
|
||
:y="tick.y + 3"
|
||
text-anchor="end"
|
||
>
|
||
{{ tick.label }}
|
||
</text>
|
||
<text
|
||
v-for="(tick, index) in detailChart.xTicks"
|
||
:key="tick.label"
|
||
:x="tick.x"
|
||
y="158"
|
||
:text-anchor="
|
||
index === 0 ? 'start' : index === 4 ? 'end' : 'middle'
|
||
"
|
||
>
|
||
{{ tick.label }}
|
||
</text>
|
||
</g>
|
||
<polygon :points="detailChart.areaPoints" fill="url(#fill)" />
|
||
<polyline :points="detailChart.points" />
|
||
<g class="detail-chart__marker">
|
||
<line
|
||
:x1="detailChart.marker.x"
|
||
y1="14"
|
||
:x2="detailChart.marker.x"
|
||
y2="137"
|
||
/>
|
||
<circle
|
||
:cx="detailChart.marker.x"
|
||
:cy="detailChart.marker.y"
|
||
r="4.5"
|
||
/>
|
||
<text
|
||
:x="detailChart.marker.x - 7"
|
||
:y="detailChart.marker.labelY"
|
||
text-anchor="end"
|
||
>
|
||
{{ chartMoney(Number(detail.price)) }}
|
||
</text>
|
||
</g>
|
||
</svg>
|
||
<div class="periods">
|
||
<button
|
||
v-for="value in CHART_PERIODS"
|
||
:key="value"
|
||
:class="{ active: period === value }"
|
||
:aria-pressed="period === value"
|
||
@click="period = value"
|
||
>
|
||
{{ value }}
|
||
</button>
|
||
</div></SkyCard
|
||
>
|
||
<p class="detail-copy">{{ t('marketDetail.description') }}</p>
|
||
<h2 class="title">{{ t('marketDetail.investment') }}</h2>
|
||
<SkyCard class="investment"
|
||
><small>{{ t('marketDetail.totalValue') }}</small
|
||
><strong>{{ privateMoney(detailHolding?.value ?? '0') }}</strong>
|
||
<div>
|
||
<CryptoLogo class="coin" :market="detail" /><span
|
||
><b>{{ detail.name }}</b
|
||
><small
|
||
>{{ quantity(detailHolding?.quantity ?? '0') }}
|
||
{{ detail.symbol }}</small
|
||
></span
|
||
><span
|
||
><b>{{ privateMoney(detailHolding?.value ?? '0') }}</b
|
||
><small
|
||
>{{ t('portfolio.avg') }}
|
||
{{ money(detailHolding?.averagePrice ?? detail.price) }}</small
|
||
></span
|
||
>
|
||
</div></SkyCard
|
||
>
|
||
<h2 class="title">{{ t('marketDetail.statistics') }}</h2>
|
||
<SkyCard class="stats"
|
||
><div>
|
||
<span>{{ t('marketDetail.high24h') }}</span
|
||
><b>{{ money(detail.high24h) }}</b>
|
||
</div>
|
||
<div>
|
||
<span>{{ t('marketDetail.low24h') }}</span
|
||
><b>{{ money(detail.low24h) }}</b>
|
||
</div>
|
||
<div>
|
||
<span>{{ t('marketDetail.supply') }}</span
|
||
><b>{{ compact(detail.issuedSupply) }} {{ detail.symbol }}</b>
|
||
</div>
|
||
<div>
|
||
<span>{{ t('marketDetail.liquidity') }}</span
|
||
><b>{{ compact(detail.treasuryAvailable) }} {{ detail.symbol }}</b>
|
||
</div></SkyCard
|
||
>
|
||
</SkyScrollArea>
|
||
|
||
<SkyScrollArea
|
||
v-else
|
||
:key="`tab-${tab}`"
|
||
class="vault-view"
|
||
with-tabbar
|
||
padded
|
||
>
|
||
<template v-if="tab === 'portfolio'">
|
||
<section class="portfolio-shell">
|
||
<div class="portfolio-glow" />
|
||
<div class="portfolio-topline">
|
||
<span
|
||
><ShieldCheck :size="13" />{{ t('portfolio.performance') }}</span
|
||
>
|
||
<span class="forecast-pill">{{ t('portfolio.forecast') }}</span>
|
||
</div>
|
||
<div class="portfolio-value">
|
||
<strong :class="portfolioProfitLoss >= 0 ? 'up' : 'down'">
|
||
{{ privateSignedMoney(portfolioProfitLoss) }}
|
||
</strong>
|
||
<span :class="portfolioProfitPercent >= 0 ? 'up' : 'down'">
|
||
{{ portfolioProfitPercent >= 0 ? '↗' : '↘' }}
|
||
{{ Math.abs(portfolioProfitPercent).toFixed(2) }}%
|
||
<small>{{ t('portfolio.return') }}</small>
|
||
</span>
|
||
</div>
|
||
<div
|
||
class="portfolio-chart"
|
||
role="img"
|
||
:aria-label="t('portfolio.chartLabel')"
|
||
>
|
||
<div class="portfolio-chart__legend">
|
||
<span><i />{{ t('portfolio.history') }}</span>
|
||
<span
|
||
><i class="is-forecast" />{{ t('portfolio.forecast') }}</span
|
||
>
|
||
</div>
|
||
<svg
|
||
:viewBox="`0 0 ${portfolioChart.width} ${portfolioChart.height}`"
|
||
aria-hidden="true"
|
||
>
|
||
<rect
|
||
class="portfolio-chart__forecast-zone"
|
||
:x="portfolioChart.currentX"
|
||
y="0"
|
||
:width="portfolioChart.width - portfolioChart.currentX"
|
||
:height="portfolioChart.height"
|
||
/>
|
||
<line
|
||
class="portfolio-chart__grid"
|
||
x1="18"
|
||
y1="15"
|
||
x2="306"
|
||
y2="15"
|
||
/>
|
||
<line
|
||
class="portfolio-chart__zero"
|
||
x1="18"
|
||
:y1="portfolioChart.zeroY"
|
||
x2="306"
|
||
:y2="portfolioChart.zeroY"
|
||
/>
|
||
<line
|
||
class="portfolio-chart__grid"
|
||
x1="18"
|
||
:y1="portfolioChart.height - 16"
|
||
x2="306"
|
||
:y2="portfolioChart.height - 16"
|
||
/>
|
||
<line
|
||
class="portfolio-chart__boundary"
|
||
:x1="portfolioChart.currentX"
|
||
y1="4"
|
||
:x2="portfolioChart.currentX"
|
||
:y2="portfolioChart.height - 8"
|
||
/>
|
||
<polyline
|
||
class="portfolio-history"
|
||
:points="portfolioChart.historyPoints"
|
||
/>
|
||
<polyline
|
||
class="portfolio-forecast"
|
||
:points="portfolioChart.forecastPoints"
|
||
/>
|
||
<circle
|
||
class="portfolio-current-dot"
|
||
:cx="portfolioChart.currentX"
|
||
:cy="portfolioChart.currentY"
|
||
r="4.5"
|
||
/>
|
||
<g
|
||
class="portfolio-current-label"
|
||
:transform="`translate(${portfolioChart.currentX - 48} ${portfolioChart.labelY})`"
|
||
>
|
||
<rect width="96" height="20" rx="10" />
|
||
<text x="48" y="13" text-anchor="middle">
|
||
{{ t('portfolio.current') }}
|
||
</text>
|
||
</g>
|
||
<text class="portfolio-scale-label" x="18" y="10">
|
||
{{ privateSignedMoney(portfolioChart.scale) }}
|
||
</text>
|
||
<text
|
||
class="portfolio-scale-label"
|
||
x="18"
|
||
:y="portfolioChart.height - 3"
|
||
>
|
||
{{ privateSignedMoney(-portfolioChart.scale) }}
|
||
</text>
|
||
</svg>
|
||
</div>
|
||
<div class="portfolio-metrics">
|
||
<div>
|
||
<small>{{ t('portfolio.cash') }}</small>
|
||
<b>{{ privateMoney(crypto.data?.cashBalance ?? '0') }}</b>
|
||
</div>
|
||
<i />
|
||
<div>
|
||
<small>{{ t('portfolio.invested') }}</small>
|
||
<b>{{ privateMoney(investedValue) }}</b>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<div class="quick">
|
||
<button @click="setTab('markets')">
|
||
<span><ChartNoAxesCombined :size="20" /></span>
|
||
<b>{{ t('quick.trade') }}</b></button
|
||
><button @click="openSettlement('deposit')">
|
||
<span><ArrowDownLeft :size="20" /></span>
|
||
<b>{{ t('actions.deposit') }}</b></button
|
||
><button @click="openSettlement('withdraw')">
|
||
<span><ArrowUpRight :size="20" /></span>
|
||
<b>{{ t('actions.withdraw') }}</b></button
|
||
><button @click="openSend">
|
||
<span><Send :size="20" /></span>
|
||
<b>{{ t('quick.send') }}</b>
|
||
</button>
|
||
</div>
|
||
<SkyCard class="allocation-card">
|
||
<div class="allocation-copy">
|
||
<span><WalletCards :size="19" /></span>
|
||
<div>
|
||
<small>{{ t('portfolio.allocation') }}</small>
|
||
<b
|
||
>{{ Math.round(100 - cashShare) }}%
|
||
{{ t('portfolio.inMarket') }}</b
|
||
>
|
||
</div>
|
||
<strong>{{ Math.round(cashShare) }}%</strong>
|
||
</div>
|
||
<div class="allocation-track">
|
||
<i :style="{ width: `${Math.max(3, 100 - cashShare)}%` }" />
|
||
</div>
|
||
</SkyCard>
|
||
<div class="heading">
|
||
<h2>{{ t('portfolio.holdings') }}</h2>
|
||
<em>{{ holdings.length }} {{ t('portfolio.assets') }}</em>
|
||
</div>
|
||
<SkyEmptyState
|
||
v-if="!holdings.length"
|
||
:title="t('portfolio.empty')"
|
||
:description="t('portfolio.emptyBody')"
|
||
/><button
|
||
v-for="holding in holdings"
|
||
v-else
|
||
:key="holding.assetId"
|
||
class="row asset-row"
|
||
@click="openMarket(market(holding.assetId)!)"
|
||
>
|
||
<CryptoLogo class="coin" :market="market(holding.assetId)!" /><span
|
||
><b>{{ market(holding.assetId)?.name }}</b
|
||
><small
|
||
>{{ quantity(holding.quantity) }}
|
||
{{ market(holding.assetId)?.symbol }}</small
|
||
><i class="asset-allocation"
|
||
><i
|
||
:style="{
|
||
width: `${allocation(holding.value)}%`,
|
||
}" /></i></span
|
||
><span
|
||
><b>{{ privateMoney(holding.value) }}</b
|
||
><small
|
||
:class="
|
||
(market(holding.assetId)?.changePercent ?? 0) >= 0
|
||
? 'up'
|
||
: 'down'
|
||
"
|
||
>{{ market(holding.assetId)?.changePercent.toFixed(2) }}%</small
|
||
></span
|
||
><ChevronRight :size="17" />
|
||
</button>
|
||
<SkyStatusCard
|
||
tone="neutral"
|
||
:title="t('portfolio.insightTitle')"
|
||
:subtitle="t('portfolio.insightBody')"
|
||
><template #icon><Sparkles :size="19" /></template
|
||
></SkyStatusCard>
|
||
</template>
|
||
|
||
<template v-else-if="tab === 'markets'">
|
||
<div class="market-head">
|
||
<span
|
||
><small>{{ t('markets.eyebrow') }}</small>
|
||
<h2>{{ t('markets.title') }}</h2></span
|
||
><em>{{ t('markets.live') }}</em>
|
||
</div>
|
||
<button
|
||
v-if="topMover"
|
||
class="featured-market"
|
||
@click="openMarket(topMover)"
|
||
>
|
||
<div class="featured-market__top">
|
||
<CryptoLogo class="coin large" :market="topMover" />
|
||
<span>
|
||
<small>{{ t('markets.movers') }}</small>
|
||
<b>{{ topMover.name }}</b>
|
||
<em>{{ topMover.symbol }}</em>
|
||
</span>
|
||
<ChevronRight :size="18" />
|
||
</div>
|
||
<div class="featured-market__quote">
|
||
<strong>{{ money(topMover.price) }}</strong>
|
||
<span :class="topMover.changePercent >= 0 ? 'up' : 'down'">
|
||
{{ topMover.changePercent >= 0 ? '↗' : '↘' }}
|
||
{{ Math.abs(topMover.changePercent).toFixed(2) }}%
|
||
</span>
|
||
</div>
|
||
<svg viewBox="0 0 320 74" preserveAspectRatio="none">
|
||
<polyline :points="points(topMover.sparkline, 320, 70)" />
|
||
</svg>
|
||
<div class="featured-market__stats">
|
||
<span
|
||
><small>{{ t('marketDetail.high24h') }}</small
|
||
><b>{{ money(topMover.high24h) }}</b></span
|
||
>
|
||
<span
|
||
><small>{{ t('marketDetail.low24h') }}</small
|
||
><b>{{ money(topMover.low24h) }}</b></span
|
||
>
|
||
</div>
|
||
</button>
|
||
<div class="heading market-list-heading">
|
||
<h2>{{ t('portfolio.assets') }}</h2>
|
||
<em>{{ markets.length }} {{ t('markets.live') }}</em>
|
||
</div>
|
||
<div class="market-grid market-grid--advanced">
|
||
<button
|
||
v-for="item in markets"
|
||
:key="item.id"
|
||
@click="openMarket(item)"
|
||
>
|
||
<div class="market-tile__top">
|
||
<CryptoLogo class="coin" :market="item" />
|
||
<span
|
||
><b>{{ item.symbol }}</b
|
||
><small>{{ item.name }}</small></span
|
||
>
|
||
<em :class="item.changePercent >= 0 ? 'up' : 'down'">
|
||
{{ item.changePercent >= 0 ? '+' : ''
|
||
}}{{ item.changePercent.toFixed(2) }}%
|
||
</em>
|
||
</div>
|
||
<div class="market-tile__price">
|
||
<b>{{ money(item.price) }}</b>
|
||
<small>{{ t('markets.today') }}</small>
|
||
</div>
|
||
<svg viewBox="0 0 150 52" preserveAspectRatio="none">
|
||
<polyline :points="points(item.sparkline, 150, 50)" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<SkyCard class="movers"
|
||
><div class="heading">
|
||
<h2>{{ t('markets.movers') }}</h2>
|
||
<em>{{ t('markets.today') }}</em>
|
||
</div>
|
||
<button
|
||
v-for="item in [...markets]
|
||
.sort((a, b) => b.changePercent - a.changePercent)
|
||
.slice(0, 5)"
|
||
:key="item.id"
|
||
@click="openMarket(item)"
|
||
>
|
||
<CryptoLogo class="coin" :market="item" /><span
|
||
><b>{{ item.name }}</b
|
||
><small>{{ item.symbol }} · {{ t('markets.today') }}</small></span
|
||
><span
|
||
><b>{{ money(item.price) }}</b
|
||
><small :class="item.changePercent >= 0 ? 'up' : 'down'"
|
||
>{{ item.changePercent >= 0 ? '+' : ''
|
||
}}{{ item.changePercent.toFixed(2) }}%</small
|
||
></span
|
||
>
|
||
</button></SkyCard
|
||
>
|
||
<SkyStatusCard
|
||
tone="accent"
|
||
:title="t('markets.noticeTitle')"
|
||
:subtitle="t('markets.notice')"
|
||
/>
|
||
</template>
|
||
|
||
<template v-else-if="tab === 'activity'">
|
||
<section class="activity-overview">
|
||
<div class="activity-head activity-dashboard">
|
||
<div class="activity-dashboard__copy">
|
||
<p><History :size="14" />{{ t('activity.volume') }}</p>
|
||
<strong>{{ privateMoney(profile?.totalVolume ?? '0') }}</strong>
|
||
<span class="activity-trade-count"
|
||
><i />{{ profile?.totalTrades }}
|
||
{{ t('activity.completedTrades') }}</span
|
||
>
|
||
</div>
|
||
<div class="activity-status">
|
||
<span class="activity-status__icon">
|
||
<ShieldCheck :size="19" />
|
||
</span>
|
||
<b>{{ t('statuses.completed') }}</b>
|
||
</div>
|
||
</div>
|
||
<div class="activity-filter-panel">
|
||
<SkySegmented
|
||
class="activity-filters"
|
||
strong
|
||
rounded
|
||
:aria-label="t('activity.title')"
|
||
:active-index="
|
||
activityFilter === 'all'
|
||
? 0
|
||
: activityFilter === 'trades'
|
||
? 1
|
||
: 2
|
||
"
|
||
:item-count="3"
|
||
><SkySegmentedButton
|
||
:active="activityFilter === 'all'"
|
||
@click="activityFilter = 'all'"
|
||
><span class="activity-filter-content"
|
||
><History :size="15" /><span>{{
|
||
t('activity.filters.all')
|
||
}}</span></span
|
||
></SkySegmentedButton
|
||
><SkySegmentedButton
|
||
:active="activityFilter === 'trades'"
|
||
@click="activityFilter = 'trades'"
|
||
><span class="activity-filter-content"
|
||
><ChartNoAxesCombined :size="15" /><span>{{
|
||
t('activity.filters.trades')
|
||
}}</span></span
|
||
></SkySegmentedButton
|
||
><SkySegmentedButton
|
||
:active="activityFilter === 'wallet'"
|
||
@click="activityFilter = 'wallet'"
|
||
><span class="activity-filter-content"
|
||
><WalletCards :size="15" /><span>{{
|
||
t('activity.filters.wallet')
|
||
}}</span></span
|
||
></SkySegmentedButton
|
||
></SkySegmented
|
||
>
|
||
</div>
|
||
</section>
|
||
<div class="heading activity-title">
|
||
<h2>{{ t('activity.title') }}</h2>
|
||
<em>{{ activities.length }}</em>
|
||
</div>
|
||
<SkyEmptyState
|
||
v-if="!activities.length"
|
||
:title="t('activity.empty')"
|
||
:description="t('activity.emptyBody')"
|
||
/>
|
||
<div
|
||
v-for="item in activities"
|
||
v-else
|
||
:key="item.id"
|
||
class="row static activity-row"
|
||
>
|
||
<span :class="['activity-icon', `activity-icon--${item.type}`]">
|
||
<ArrowDownLeft v-if="item.type === 'deposit'" :size="18" />
|
||
<ArrowUpRight v-else-if="item.type === 'withdrawal'" :size="18" />
|
||
<Send v-else-if="item.type === 'transfer_out'" :size="18" />
|
||
<KeyRound v-else-if="item.type === 'transfer_in'" :size="18" />
|
||
<ChartNoAxesCombined v-else :size="18" /> </span
|
||
><span
|
||
><b>{{ t(`activityTypes.${item.type}`) }}</b
|
||
><small
|
||
>{{ market(item.marketId)?.symbol ?? t('activity.cash') }} ·
|
||
{{
|
||
new Intl.DateTimeFormat(locale, {
|
||
dateStyle: 'medium',
|
||
timeStyle: 'short',
|
||
}).format(item.createdAt)
|
||
}}</small
|
||
></span
|
||
><span
|
||
><b :class="activityIsDebit(item) ? 'down' : 'up'"
|
||
>{{ activityIsDebit(item) ? '−' : '+'
|
||
}}{{ activityValue(item) }}</b
|
||
><small>{{ t(`statuses.${item.status}`) }}</small></span
|
||
>
|
||
<i class="timeline-dot" />
|
||
</div>
|
||
</template>
|
||
|
||
<template v-else>
|
||
<section class="profile-card">
|
||
<div class="profile-card__mesh" />
|
||
<div class="profile-card__top">
|
||
<span class="profile-avatar">{{
|
||
profile?.handle.slice(0, 2).toUpperCase()
|
||
}}</span>
|
||
<span class="profile-status"><i />{{ t('profile.verified') }}</span>
|
||
</div>
|
||
<div class="profile-card__identity">
|
||
<span>
|
||
<h2>@{{ profile?.handle }}</h2>
|
||
<p><ShieldCheck :size="15" />{{ t('profile.verified') }}</p>
|
||
</span>
|
||
<SkyButton
|
||
class="profile-edit-button"
|
||
rounded
|
||
small
|
||
@click="openProfileEditor"
|
||
>
|
||
<Settings2 :size="15" />{{ t('profile.edit') }}
|
||
</SkyButton>
|
||
</div>
|
||
<div class="profile-card__id">
|
||
<small>VAULTX ID</small>
|
||
<b>{{ profile?.id.slice(0, 8).toUpperCase() }}</b>
|
||
<Fingerprint :size="26" />
|
||
</div>
|
||
</section>
|
||
<SkyCard class="wallet-key-card" :content-wrap="false">
|
||
<div class="wallet-key-card__content">
|
||
<span class="wallet-key-card__icon"><KeyRound :size="20" /></span>
|
||
<span>
|
||
<small>{{ t('profile.walletKey') }}</small>
|
||
<b>{{ profile?.walletKey }}</b>
|
||
<em>{{ t('profile.walletKeyBody') }}</em>
|
||
</span>
|
||
</div>
|
||
<div class="wallet-key-card__actions">
|
||
<SkyButton variant="secondary" tonal @click="copyWalletKey">
|
||
<CheckCircle2 v-if="walletKeyCopied" :size="16" />
|
||
<Copy v-else :size="16" />
|
||
<span>{{
|
||
t(walletKeyCopied ? 'profile.copied' : 'profile.copyKey')
|
||
}}</span>
|
||
</SkyButton>
|
||
<SkyButton @click="shareWalletKey">
|
||
<Share2 :size="16" /><span>{{ t('profile.shareKey') }}</span>
|
||
</SkyButton>
|
||
</div>
|
||
</SkyCard>
|
||
<div class="profile-stats profile-stats--premium">
|
||
<div>
|
||
<b>{{ profile?.totalTrades }}</b
|
||
><small>{{ t('profile.trades') }}</small>
|
||
</div>
|
||
<div>
|
||
<b>{{ privateMoney(profile?.totalVolume ?? '0') }}</b
|
||
><small>{{ t('profile.volume') }}</small>
|
||
</div>
|
||
<div>
|
||
<b>{{
|
||
profile
|
||
? new Intl.DateTimeFormat(locale, {
|
||
month: 'short',
|
||
year: 'numeric',
|
||
}).format(profile.createdAt)
|
||
: '—'
|
||
}}</b>
|
||
<small>{{ t('profile.memberSince') }}</small>
|
||
</div>
|
||
</div>
|
||
<SkyCard class="security-overview" :content-wrap="false">
|
||
<div class="security-score">
|
||
<ShieldCheck :size="22" /><b>100</b><small>/100</small>
|
||
</div>
|
||
<span
|
||
><small>{{ t('profile.securityTitle') }}</small
|
||
><b>{{ t('profile.verified') }}</b
|
||
><em>{{ t('profile.securityBody') }}</em></span
|
||
>
|
||
</SkyCard>
|
||
<h2 class="title">{{ t('profile.preferences') }}</h2>
|
||
<SkyCard class="settings"
|
||
><label
|
||
><span
|
||
><BellRing :size="18" /><span
|
||
><b>{{ t('profile.priceAlerts') }}</b
|
||
><small>{{ t('profile.priceAlertsBody') }}</small></span
|
||
></span
|
||
><SkyToggle
|
||
v-model="priceAlerts"
|
||
@change="savePreferences" /></label
|
||
><label
|
||
><span
|
||
><Fingerprint :size="18" /><span
|
||
><b>{{ t('profile.confirmations') }}</b
|
||
><small>{{ t('profile.confirmationsBody') }}</small></span
|
||
></span
|
||
><SkyToggle
|
||
v-model="confirmations"
|
||
@change="savePreferences" /></label
|
||
><label
|
||
><span
|
||
><EyeOff :size="18" /><span
|
||
><b>{{ t('profile.hideBalances') }}</b
|
||
><small>{{ t('profile.hideBalancesBody') }}</small></span
|
||
></span
|
||
><SkyToggle
|
||
v-model="hideBalances"
|
||
@change="savePreferences" /></label
|
||
></SkyCard>
|
||
<p v-if="saved" class="profile-feedback success">
|
||
<ShieldCheck :size="15" />{{ t('profile.saved') }}
|
||
</p>
|
||
<p v-if="formError" class="profile-feedback error">{{ formError }}</p>
|
||
</template>
|
||
</SkyScrollArea>
|
||
|
||
<div
|
||
v-if="authenticated && detail"
|
||
class="detail-trade-dock"
|
||
role="group"
|
||
:aria-label="t('trade.actions')"
|
||
>
|
||
<SkyButton
|
||
large
|
||
class="detail-trade-action detail-trade-action--buy"
|
||
@click="openTrade(detail, 'buy')"
|
||
>
|
||
<span><ArrowDownLeft :size="19" /></span>
|
||
<span>
|
||
<b>{{ t('trade.buy') }}</b>
|
||
<small>{{ t('trade.buyBody') }}</small>
|
||
</span>
|
||
</SkyButton>
|
||
<SkyButton
|
||
large
|
||
variant="secondary"
|
||
class="detail-trade-action detail-trade-action--sell"
|
||
@click="openTrade(detail, 'sell')"
|
||
>
|
||
<span><ArrowUpRight :size="19" /></span>
|
||
<span>
|
||
<b>{{ t('trade.sell') }}</b>
|
||
<small>{{ t('trade.sellBody') }}</small>
|
||
</span>
|
||
</SkyButton>
|
||
</div>
|
||
|
||
<SkyPillNavigation
|
||
v-if="authenticated && !detail"
|
||
layout="full"
|
||
:label="t('navigation')"
|
||
><SkySegmented navigation
|
||
><SkySegmentedButton
|
||
:active="tab === 'portfolio'"
|
||
@click="setTab('portfolio')"
|
||
><WalletCards :size="19" /><span>{{
|
||
t('tabs.portfolio')
|
||
}}</span></SkySegmentedButton
|
||
><SkySegmentedButton
|
||
:active="tab === 'markets'"
|
||
@click="setTab('markets')"
|
||
><ChartNoAxesCombined :size="19" /><span>{{
|
||
t('tabs.markets')
|
||
}}</span></SkySegmentedButton
|
||
><SkySegmentedButton
|
||
:active="tab === 'activity'"
|
||
@click="setTab('activity')"
|
||
><History :size="19" /><span>{{
|
||
t('tabs.activity')
|
||
}}</span></SkySegmentedButton
|
||
><SkySegmentedButton
|
||
:active="tab === 'profile'"
|
||
@click="setTab('profile')"
|
||
><UserRound :size="19" /><span>{{
|
||
t('tabs.profile')
|
||
}}</span></SkySegmentedButton
|
||
></SkySegmented
|
||
></SkyPillNavigation
|
||
>
|
||
|
||
<SkySheet
|
||
:opened="sheet !== null"
|
||
swipe-to-close
|
||
grabber-clickable
|
||
:grabber-label="phone.t('Common.close')"
|
||
@backdropclick="closeSheet"
|
||
@escape="closeSheet"
|
||
@grabberclick="closeSheet"
|
||
@swipeclose="closeSheet"
|
||
><div
|
||
v-if="sheet"
|
||
class="sheet"
|
||
:class="{
|
||
'sheet--profile': sheet === 'profile',
|
||
'sheet--settlement': sheet === 'deposit' || sheet === 'withdraw',
|
||
}"
|
||
>
|
||
<header class="sheet-header" data-sky-sheet-drag-handle>
|
||
<div class="sheet-heading">
|
||
<span
|
||
class="sheet-icon"
|
||
:class="{
|
||
'sheet-icon--crypto': sheet === 'trade' && selectedMarket,
|
||
}"
|
||
>
|
||
<CryptoLogo
|
||
v-if="sheet === 'trade' && selectedMarket"
|
||
class="sheet-crypto-logo"
|
||
:market="selectedMarket"
|
||
/>
|
||
<ArrowDownLeft v-else-if="sheet === 'deposit'" :size="19" />
|
||
<ArrowUpRight v-else-if="sheet === 'withdraw'" :size="19" />
|
||
<Send v-else-if="sheet === 'send'" :size="19" />
|
||
<UserRound v-else :size="19" />
|
||
</span>
|
||
<span>
|
||
<small>{{
|
||
sheet === 'trade' && selectedMarket
|
||
? selectedMarket.name
|
||
: sheet === 'profile'
|
||
? t('profile.account')
|
||
: sheet === 'send'
|
||
? t('transfer.subtitle')
|
||
: t('activity.cash')
|
||
}}</small>
|
||
<h2>
|
||
{{
|
||
sheet === 'trade'
|
||
? `${t(`trade.${side}`)} ${selectedMarket?.symbol}`
|
||
: sheet === 'profile'
|
||
? t('profile.editTitle')
|
||
: sheet === 'send'
|
||
? t('transfer.title')
|
||
: t(`actions.${sheet}`)
|
||
}}
|
||
</h2>
|
||
</span>
|
||
</div>
|
||
</header>
|
||
<template v-if="sheet === 'trade' && selectedMarket">
|
||
<div class="sheet-market-summary">
|
||
<span>
|
||
<small>{{ t('trade.marketPrice') }}</small>
|
||
<b>{{ money(selectedMarket.price) }}</b>
|
||
</span>
|
||
<em :class="selectedMarket.changePercent >= 0 ? 'up' : 'down'">
|
||
{{ selectedMarket.changePercent >= 0 ? '↗' : '↘' }}
|
||
{{ Math.abs(selectedMarket.changePercent).toFixed(2) }}%
|
||
</em>
|
||
</div>
|
||
<SkySegmented
|
||
class="trade-side-selector"
|
||
strong
|
||
rounded
|
||
:item-count="2"
|
||
:active-index="side === 'buy' ? 0 : 1"
|
||
><SkySegmentedButton
|
||
:active="side === 'buy'"
|
||
@click="selectTradeSide('buy')"
|
||
><ArrowDownLeft :size="16" />{{
|
||
t('trade.buy')
|
||
}}</SkySegmentedButton
|
||
><SkySegmentedButton
|
||
:active="side === 'sell'"
|
||
@click="selectTradeSide('sell')"
|
||
><ArrowUpRight :size="16" />{{
|
||
t('trade.sell')
|
||
}}</SkySegmentedButton
|
||
></SkySegmented
|
||
>
|
||
<div class="trade-entry-card">
|
||
<SkyField
|
||
v-model="amount"
|
||
class="sheet-field"
|
||
:error="Boolean(formError)"
|
||
:label="t('trade.quantity')"
|
||
placeholder="0.000000"
|
||
inputmode="decimal"
|
||
outline
|
||
><template #leading><ChartCandlestick :size="18" /></template
|
||
></SkyField>
|
||
<div class="trade-entry-meta">
|
||
<span>
|
||
<small>{{ t('trade.available') }}</small>
|
||
<b v-if="side === 'buy'">{{
|
||
privateMoney(crypto.data?.cashBalance ?? '0')
|
||
}}</b>
|
||
<b v-else>
|
||
{{ quantity(selectedHolding?.quantity ?? '0') }}
|
||
{{ selectedMarket.symbol }}
|
||
</b>
|
||
</span>
|
||
<span>
|
||
<small>{{ t('trade.estimated') }}</small>
|
||
<b>{{ money(estimatedTradeValue) }}</b>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div v-if="!crypto.pendingQuote" class="trade-protection">
|
||
<ShieldCheck :size="18" />
|
||
<span>
|
||
<b>{{ t('trade.protected') }}</b>
|
||
<small>{{ t('trade.protectedBody') }}</small>
|
||
</span>
|
||
</div>
|
||
<SkyCard
|
||
v-if="crypto.pendingQuote"
|
||
class="quote"
|
||
:content-wrap="false"
|
||
>
|
||
<div class="quote-heading">
|
||
<span>
|
||
<small>{{ t('trade.review') }}</small>
|
||
<b>{{ t(`trade.${side}`) }} {{ selectedMarket.symbol }}</b>
|
||
</span>
|
||
<ShieldCheck :size="18" />
|
||
</div>
|
||
<div>
|
||
<span>{{ t('trade.price') }}</span
|
||
><b>{{ money(crypto.pendingQuote.price) }}</b>
|
||
</div>
|
||
<div>
|
||
<span>{{ t('trade.gross') }}</span
|
||
><b>{{ money(crypto.pendingQuote.gross) }}</b>
|
||
</div>
|
||
<div>
|
||
<span>{{ t('trade.fee') }}</span
|
||
><b>{{ money(crypto.pendingQuote.fee) }}</b>
|
||
</div>
|
||
<div>
|
||
<span>{{ t('trade.total') }}</span
|
||
><b>{{ money(crypto.pendingQuote.net) }}</b>
|
||
</div>
|
||
<small class="quote-expiry">{{ t('trade.quoteExpiry') }}</small>
|
||
</SkyCard>
|
||
<p v-if="formError" class="error">{{ formError }}</p>
|
||
<SkyButton
|
||
block
|
||
large
|
||
class="trade-submit"
|
||
:class="`trade-submit--${side}`"
|
||
@click="crypto.pendingQuote ? executeQuote() : requestQuote()"
|
||
>{{
|
||
t(crypto.pendingQuote ? 'trade.confirm' : 'trade.getQuote')
|
||
}}</SkyButton
|
||
> </template
|
||
><template v-else-if="sheet === 'send'">
|
||
<div class="transfer-intro">
|
||
<span><KeyRound :size="19" /></span>
|
||
<div>
|
||
<b>{{ t('transfer.keyTitle') }}</b
|
||
><small>{{ t('transfer.keyBody') }}</small>
|
||
</div>
|
||
</div>
|
||
<div class="transfer-key-entry">
|
||
<SkyField
|
||
v-model="transferWalletKey"
|
||
class="sheet-field"
|
||
:label="t('transfer.walletKey')"
|
||
placeholder="VX-0000-0000-0000-0000"
|
||
maxlength="22"
|
||
autocomplete="off"
|
||
outline
|
||
><template #leading><KeyRound :size="17" /></template
|
||
></SkyField>
|
||
<SkyButton
|
||
class="transfer-resolve"
|
||
@click="resolveTransferRecipient"
|
||
>
|
||
{{ t('transfer.verify') }}
|
||
</SkyButton>
|
||
</div>
|
||
<div v-if="transferRecipient" class="transfer-recipient">
|
||
<span><CheckCircle2 :size="20" /></span>
|
||
<div>
|
||
<small>{{ t('transfer.verifiedRecipient') }}</small
|
||
><b>@{{ transferRecipient.handle }}</b
|
||
><em>{{ transferRecipient.walletKey }}</em>
|
||
</div>
|
||
</div>
|
||
<div class="transfer-assets">
|
||
<small>{{ t('transfer.asset') }}</small>
|
||
<div>
|
||
<button
|
||
v-for="holding in holdings"
|
||
:key="holding.assetId"
|
||
:class="{ active: transferMarketId === holding.assetId }"
|
||
@click="transferMarketId = holding.assetId"
|
||
>
|
||
<CryptoLogo :market="market(holding.assetId)!" />
|
||
<span
|
||
><b>{{ market(holding.assetId)?.symbol }}</b
|
||
><small>{{ quantity(holding.quantity) }}</small></span
|
||
>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<SkyField
|
||
v-model="transferQuantity"
|
||
class="sheet-field"
|
||
:label="t('transfer.quantity')"
|
||
placeholder="0.000000"
|
||
inputmode="decimal"
|
||
outline
|
||
><template #trailing
|
||
><b>{{ transferMarket?.symbol }}</b></template
|
||
><template #leading><Send :size="18" /></template
|
||
></SkyField>
|
||
<div class="transfer-balance">
|
||
<span>{{ t('transfer.available') }}</span>
|
||
<b
|
||
>{{ quantity(transferHolding?.quantity ?? '0') }}
|
||
{{ transferMarket?.symbol }}</b
|
||
>
|
||
</div>
|
||
<SkyField
|
||
v-model="transferPassword"
|
||
class="sheet-field"
|
||
:label="t('transfer.password')"
|
||
type="password"
|
||
autocomplete="current-password"
|
||
outline
|
||
><template #leading><LockKeyhole :size="17" /></template
|
||
></SkyField>
|
||
<div class="trade-protection">
|
||
<ShieldCheck :size="17" />
|
||
<span
|
||
><b>{{ t('transfer.protected') }}</b
|
||
><small>{{ t('transfer.protectedBody') }}</small></span
|
||
>
|
||
</div>
|
||
<p v-if="formError" class="error">{{ formError }}</p>
|
||
<SkyButton
|
||
block
|
||
large
|
||
class="transfer-submit"
|
||
@click="submitTransfer"
|
||
>
|
||
<Send :size="17" />{{ t('transfer.confirm') }}
|
||
</SkyButton> </template
|
||
><template v-else-if="sheet === 'profile'">
|
||
<p class="sheet-copy">{{ t('profile.editBody') }}</p>
|
||
<form class="profile-edit-sheet" @submit.prevent="saveProfile">
|
||
<div class="profile-edit-fields">
|
||
<SkyField
|
||
v-model="profileHandle"
|
||
class="sheet-field profile-edit-field"
|
||
:error="Boolean(formError)"
|
||
:label="t('auth.handle')"
|
||
maxlength="20"
|
||
autocomplete="username"
|
||
outline
|
||
><template #leading><UserRound :size="18" /></template
|
||
></SkyField>
|
||
<SkyField
|
||
v-model="profileCurrentPassword"
|
||
class="sheet-field profile-edit-field"
|
||
:error="Boolean(formError)"
|
||
:label="t('profile.currentPassword')"
|
||
:placeholder="t('profile.currentPasswordPlaceholder')"
|
||
type="password"
|
||
autocomplete="current-password"
|
||
outline
|
||
><template #leading><LockKeyhole :size="18" /></template
|
||
></SkyField>
|
||
<SkyField
|
||
v-model="profileNewPassword"
|
||
class="sheet-field profile-edit-field"
|
||
:error="Boolean(formError)"
|
||
:label="t('profile.newPassword')"
|
||
:placeholder="t('profile.newPasswordPlaceholder')"
|
||
type="password"
|
||
autocomplete="new-password"
|
||
outline
|
||
><template #leading><Fingerprint :size="18" /></template
|
||
></SkyField>
|
||
</div>
|
||
<div class="profile-edit-note">
|
||
<ShieldCheck :size="17" />
|
||
<span>{{ t('profile.passwordSecurity') }}</span>
|
||
</div>
|
||
<p v-if="formError" class="error">{{ formError }}</p>
|
||
<SkyButton block large class="profile-save-button" type="submit">
|
||
<ShieldCheck :size="18" />
|
||
<span>{{ t('profile.saveChanges') }}</span>
|
||
</SkyButton>
|
||
</form>
|
||
</template>
|
||
<template v-else>
|
||
<form class="settlement-form" @submit.prevent="submitSettlement">
|
||
<div class="settlement-intro">
|
||
<span><ShieldCheck :size="18" /></span>
|
||
<p class="sheet-copy settlement-copy">
|
||
{{
|
||
t(
|
||
sheet === 'deposit'
|
||
? 'settlement.depositBody'
|
||
: 'settlement.withdrawBody',
|
||
)
|
||
}}
|
||
</p>
|
||
</div>
|
||
<div class="settlement-fields">
|
||
<SkyField
|
||
v-model="amount"
|
||
class="sheet-field settlement-field"
|
||
:error="Boolean(formError)"
|
||
:label="t('settlement.amount')"
|
||
placeholder="0"
|
||
inputmode="numeric"
|
||
outline
|
||
><template #leading><WalletCards :size="18" /></template
|
||
></SkyField>
|
||
<SkyField
|
||
v-model="financialPassword"
|
||
class="sheet-field settlement-field"
|
||
:error="Boolean(formError)"
|
||
:label="t('auth.password')"
|
||
type="password"
|
||
autocomplete="current-password"
|
||
outline
|
||
><template #leading><LockKeyhole :size="18" /></template
|
||
></SkyField>
|
||
</div>
|
||
<p v-if="formError" class="error settlement-error">
|
||
{{ formError }}
|
||
</p>
|
||
<SkyButton block large class="settlement-submit" type="submit">
|
||
<ShieldCheck :size="18" />
|
||
<span>{{ t('settlement.confirm') }}</span>
|
||
</SkyButton>
|
||
</form>
|
||
</template>
|
||
</div></SkySheet
|
||
>
|
||
<SkyDialog
|
||
:opened="logoutDialogOpen"
|
||
role="alertdialog"
|
||
@backdropclick="!logoutSubmitting && (logoutDialogOpen = false)"
|
||
@escape="!logoutSubmitting && (logoutDialogOpen = false)"
|
||
>
|
||
<template #title>{{ t('logoutConfirm.title') }}</template>
|
||
<p>{{ t('logoutConfirm.body') }}</p>
|
||
<template #buttons>
|
||
<SkyDialogButton
|
||
:disabled="logoutSubmitting"
|
||
@click="logoutDialogOpen = false"
|
||
>
|
||
{{ t('logoutConfirm.cancel') }}
|
||
</SkyDialogButton>
|
||
<SkyDialogButton
|
||
strong
|
||
class="vault-logout-confirm"
|
||
:disabled="logoutSubmitting"
|
||
@click="signOut"
|
||
>
|
||
{{
|
||
t(
|
||
logoutSubmitting
|
||
? 'logoutConfirm.signingOut'
|
||
: 'logoutConfirm.confirm',
|
||
)
|
||
}}
|
||
</SkyDialogButton>
|
||
</template>
|
||
</SkyDialog>
|
||
</SkyAppPage>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.crypto-app {
|
||
--card: #11151b;
|
||
--muted: rgba(225, 234, 240, 0.58);
|
||
color: #f8fbfd;
|
||
background:
|
||
radial-gradient(
|
||
circle at 50% -10%,
|
||
rgba(42, 91, 105, 0.25),
|
||
transparent 33%
|
||
),
|
||
#05070b;
|
||
}
|
||
.vault-logout-confirm {
|
||
color: #fff;
|
||
background: #c4475c;
|
||
}
|
||
.state,
|
||
.auth {
|
||
display: grid;
|
||
align-content: center;
|
||
gap: 18px;
|
||
min-height: 100%;
|
||
text-align: center;
|
||
}
|
||
.auth-hero {
|
||
display: grid;
|
||
justify-items: center;
|
||
gap: 6px;
|
||
}
|
||
.profile-head > span {
|
||
display: grid;
|
||
place-items: center;
|
||
width: 68px;
|
||
height: 68px;
|
||
border-radius: 22px;
|
||
color: #04110e;
|
||
background: linear-gradient(145deg, #6ff5cd, #3d8fff);
|
||
box-shadow: 0 18px 45px rgba(49, 214, 170, 0.18);
|
||
}
|
||
.auth-hero h2,
|
||
.auth-hero small {
|
||
margin: 0;
|
||
}
|
||
.auth-hero h2 {
|
||
font-size: 23px;
|
||
line-height: 1.15;
|
||
}
|
||
.auth-hero small {
|
||
max-width: 260px;
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
line-height: 1.45;
|
||
}
|
||
.auth {
|
||
align-content: center;
|
||
justify-items: center;
|
||
min-height: 0;
|
||
flex: 1 1 auto;
|
||
padding-top: 18px !important;
|
||
padding-bottom: 22px !important;
|
||
}
|
||
.auth-shell {
|
||
display: grid;
|
||
gap: 18px;
|
||
width: 100%;
|
||
max-width: 338px;
|
||
}
|
||
.auth-panel {
|
||
padding: 14px;
|
||
text-align: left;
|
||
background: #0b1118;
|
||
border: 1px solid rgba(255, 255, 255, 0.075);
|
||
border-radius: 22px;
|
||
box-shadow:
|
||
0 20px 46px rgba(0, 0, 0, 0.3),
|
||
inset 0 1px rgba(255, 255, 255, 0.04);
|
||
}
|
||
.auth-mode {
|
||
min-height: 48px;
|
||
margin-bottom: 14px;
|
||
padding: 3px;
|
||
background: rgba(255, 255, 255, 0.045);
|
||
border-radius: 14px;
|
||
}
|
||
.auth-mode :deep(.sky-segmented-button) {
|
||
min-height: 42px;
|
||
color: rgba(255, 255, 255, 0.52);
|
||
font-size: 13px;
|
||
font-weight: 800;
|
||
}
|
||
.auth-mode :deep(.sky-segmented-button--active) {
|
||
color: #fff;
|
||
}
|
||
.auth-form {
|
||
gap: 12px;
|
||
}
|
||
.auth-password-hint {
|
||
margin: -3px 4px 0;
|
||
color: rgba(255, 255, 255, 0.58);
|
||
font-size: 11px;
|
||
line-height: 15px;
|
||
}
|
||
.auth-account {
|
||
display: grid;
|
||
grid-template-columns: 40px minmax(0, 1fr) 18px;
|
||
gap: 11px;
|
||
align-items: center;
|
||
min-height: 64px;
|
||
margin-bottom: 12px;
|
||
padding: 10px 13px;
|
||
color: #f8fbfd;
|
||
background: rgba(101, 251, 210, 0.055);
|
||
border: 1px solid rgba(101, 251, 210, 0.15);
|
||
border-radius: 17px;
|
||
}
|
||
.auth-account > span {
|
||
display: grid;
|
||
width: 40px;
|
||
height: 40px;
|
||
place-items: center;
|
||
color: var(--vault-mint);
|
||
background: rgba(101, 251, 210, 0.09);
|
||
border-radius: 12px;
|
||
}
|
||
.auth-account > div {
|
||
display: grid;
|
||
gap: 3px;
|
||
min-width: 0;
|
||
}
|
||
.auth-account small {
|
||
color: rgba(255, 255, 255, 0.52);
|
||
font-size: 11px;
|
||
font-weight: 750;
|
||
}
|
||
.auth-account strong {
|
||
overflow: hidden;
|
||
font-size: 14px;
|
||
line-height: 1.2;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.auth-account > svg {
|
||
color: var(--vault-mint);
|
||
}
|
||
.auth-field {
|
||
min-height: 68px;
|
||
margin: 0;
|
||
padding: 0 14px;
|
||
color: #f8fbfd;
|
||
background: #080d13;
|
||
border-radius: 17px;
|
||
box-shadow: inset 0 1px rgba(255, 255, 255, 0.035);
|
||
}
|
||
.auth-field:focus-within {
|
||
background: #091119;
|
||
box-shadow:
|
||
0 0 0 3px rgba(101, 251, 210, 0.06),
|
||
inset 0 1px rgba(255, 255, 255, 0.05);
|
||
}
|
||
.auth-field :deep(.sky-field__media) {
|
||
display: grid;
|
||
width: 40px;
|
||
height: 40px;
|
||
place-items: center;
|
||
justify-content: center;
|
||
margin-right: 11px;
|
||
padding: 0;
|
||
color: var(--vault-mint);
|
||
background: rgba(101, 251, 210, 0.07);
|
||
border: 1px solid rgba(101, 251, 210, 0.12);
|
||
border-radius: 11px;
|
||
}
|
||
.auth-field :deep(.sky-field__inner) {
|
||
padding: 11px 0 9px;
|
||
}
|
||
.auth-field :deep(.sky-field__label) {
|
||
margin-top: 0;
|
||
color: rgba(255, 255, 255, 0.56);
|
||
font-size: 11px;
|
||
font-weight: 750;
|
||
}
|
||
.auth-field :deep(.sky-field__label-text) {
|
||
top: 0;
|
||
margin: 0;
|
||
padding: 0;
|
||
background: transparent;
|
||
}
|
||
.auth-field :deep(.sky-field__control) {
|
||
margin: -2px 0 0;
|
||
}
|
||
.auth-field :deep(.sky-field__input) {
|
||
height: 33px;
|
||
min-height: 33px;
|
||
color: #fff;
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
line-height: 21px;
|
||
}
|
||
.auth-field :deep(.sky-field__input::placeholder) {
|
||
color: rgba(255, 255, 255, 0.28);
|
||
}
|
||
.auth-field :deep(.sky-field__border) {
|
||
inset: 0;
|
||
border-color: rgba(255, 255, 255, 0.12);
|
||
border-radius: 17px;
|
||
}
|
||
.auth-field:focus-within :deep(.sky-field__border) {
|
||
border-color: rgba(101, 251, 210, 0.56);
|
||
}
|
||
.auth-submit {
|
||
min-height: 54px;
|
||
margin-top: 3px;
|
||
color: #06110e;
|
||
font-size: 14px;
|
||
font-weight: 900;
|
||
border: 1px solid rgba(101, 251, 210, 0.48);
|
||
border-radius: 12px;
|
||
background: var(--vault-mint);
|
||
box-shadow: none;
|
||
}
|
||
.form {
|
||
display: grid;
|
||
gap: 13px;
|
||
text-align: left;
|
||
}
|
||
.hint,
|
||
.success {
|
||
display: flex;
|
||
gap: 7px;
|
||
align-items: center;
|
||
color: var(--muted);
|
||
font-size: 13px;
|
||
}
|
||
.success,
|
||
.up {
|
||
color: #31d6aa !important;
|
||
}
|
||
.error,
|
||
.down {
|
||
color: #ff5c70 !important;
|
||
}
|
||
.visibility {
|
||
display: grid;
|
||
place-items: center;
|
||
width: 44px;
|
||
height: 44px;
|
||
color: inherit;
|
||
background: none;
|
||
border: 0;
|
||
}
|
||
.portfolio {
|
||
display: grid;
|
||
justify-items: center;
|
||
min-height: 225px;
|
||
text-align: center;
|
||
}
|
||
.portfolio p {
|
||
display: flex;
|
||
gap: 5px;
|
||
align-items: center;
|
||
margin: 15px 0 0;
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
}
|
||
.portfolio > strong {
|
||
margin: 7px 0 2px;
|
||
font-size: 38px;
|
||
letter-spacing: -0.05em;
|
||
}
|
||
.portfolio em,
|
||
.detail-head em {
|
||
font-size: 13px;
|
||
font-style: normal;
|
||
}
|
||
.portfolio svg {
|
||
width: calc(100% + 32px);
|
||
height: 100px;
|
||
margin-top: 8px;
|
||
}
|
||
.portfolio polyline,
|
||
.big-chart polyline,
|
||
.market-grid polyline {
|
||
fill: none;
|
||
stroke: #dffff6;
|
||
stroke-width: 3;
|
||
stroke-linecap: round;
|
||
stroke-linejoin: round;
|
||
vector-effect: non-scaling-stroke;
|
||
filter: drop-shadow(0 0 7px rgba(49, 214, 170, 0.45));
|
||
}
|
||
.quick {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 6px;
|
||
margin-bottom: 20px;
|
||
}
|
||
.quick button {
|
||
display: grid;
|
||
justify-items: center;
|
||
gap: 6px;
|
||
padding: 0;
|
||
color: #eef3f5;
|
||
font-size: 11px;
|
||
background: none;
|
||
border: 0;
|
||
}
|
||
.quick button span {
|
||
display: grid;
|
||
place-items: center;
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 17px;
|
||
background: #242a33;
|
||
}
|
||
.cash {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-bottom: 20px;
|
||
padding: 15px;
|
||
background: linear-gradient(135deg, #172026, #101419);
|
||
}
|
||
.cash > svg {
|
||
color: #49e4b2;
|
||
}
|
||
.cash span {
|
||
display: grid;
|
||
flex: 1;
|
||
}
|
||
.cash small {
|
||
color: var(--muted);
|
||
}
|
||
.heading,
|
||
.market-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin: 0 2px 10px;
|
||
}
|
||
.heading h2,
|
||
.market-head h2,
|
||
.title {
|
||
margin: 0;
|
||
font-size: 18px;
|
||
}
|
||
.heading button {
|
||
display: grid;
|
||
place-items: center;
|
||
width: 40px;
|
||
height: 40px;
|
||
color: #fff;
|
||
background: none;
|
||
border: 0;
|
||
}
|
||
.title {
|
||
margin: 23px 2px 11px;
|
||
}
|
||
.row {
|
||
display: grid;
|
||
grid-template-columns: 42px 1fr auto 18px;
|
||
gap: 10px;
|
||
align-items: center;
|
||
width: 100%;
|
||
margin-bottom: 8px;
|
||
padding: 11px 12px;
|
||
color: #fff;
|
||
text-align: left;
|
||
background: var(--card);
|
||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||
border-radius: 18px;
|
||
}
|
||
.row.static {
|
||
grid-template-columns: 42px 1fr auto;
|
||
}
|
||
.row span:last-of-type {
|
||
text-align: right;
|
||
}
|
||
.row b,
|
||
.row small {
|
||
display: block;
|
||
}
|
||
.row small {
|
||
margin-top: 3px;
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
}
|
||
.coin,
|
||
.activity-icon {
|
||
display: grid;
|
||
place-items: center;
|
||
width: 38px;
|
||
height: 38px;
|
||
border-radius: 13px;
|
||
color: #fff;
|
||
font-weight: 900;
|
||
}
|
||
.activity-icon {
|
||
color: #49e4b2;
|
||
background: rgba(49, 214, 170, 0.12);
|
||
}
|
||
.market-head small {
|
||
color: #49e4b2;
|
||
font-size: 11px;
|
||
font-weight: 800;
|
||
letter-spacing: 0.1em;
|
||
text-transform: uppercase;
|
||
}
|
||
.market-head em,
|
||
.heading em {
|
||
color: #49e4b2;
|
||
font-size: 11px;
|
||
font-style: normal;
|
||
}
|
||
.market-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 10px;
|
||
margin-bottom: 18px;
|
||
}
|
||
.market-grid button {
|
||
display: grid;
|
||
min-height: 188px;
|
||
padding: 15px;
|
||
color: #fff;
|
||
text-align: left;
|
||
background: var(--card);
|
||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||
border-radius: 21px;
|
||
}
|
||
.market-grid button > small {
|
||
margin-top: 9px;
|
||
color: var(--muted);
|
||
}
|
||
.market-grid button > b {
|
||
font-size: 18px;
|
||
}
|
||
.market-grid em {
|
||
font-size: 12px;
|
||
font-style: normal;
|
||
}
|
||
.market-grid svg {
|
||
align-self: end;
|
||
width: 100%;
|
||
height: 48px;
|
||
}
|
||
.market-grid polyline {
|
||
stroke: #31d6aa;
|
||
stroke-width: 2;
|
||
}
|
||
.movers {
|
||
padding: 8px 12px;
|
||
margin-bottom: 15px;
|
||
background: var(--card);
|
||
}
|
||
.movers button {
|
||
display: grid;
|
||
grid-template-columns: 42px 1fr auto;
|
||
gap: 10px;
|
||
align-items: center;
|
||
width: 100%;
|
||
padding: 10px 2px;
|
||
color: #fff;
|
||
text-align: left;
|
||
background: none;
|
||
border: 0;
|
||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||
}
|
||
.movers button span:last-child {
|
||
text-align: right;
|
||
}
|
||
.movers b,
|
||
.movers small {
|
||
display: block;
|
||
}
|
||
.movers small {
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
}
|
||
.detail-head {
|
||
display: grid;
|
||
justify-items: center;
|
||
padding: 12px 0 15px;
|
||
text-align: center;
|
||
}
|
||
.coin.large {
|
||
width: 58px;
|
||
height: 58px;
|
||
border-radius: 20px;
|
||
font-size: 20px;
|
||
}
|
||
.detail-head p {
|
||
margin: 9px 0 0;
|
||
color: var(--muted);
|
||
font-size: 13px;
|
||
}
|
||
.detail-head > strong {
|
||
font-size: 34px;
|
||
}
|
||
.big-chart {
|
||
padding: 7px 0;
|
||
background: #080b0e;
|
||
overflow: hidden;
|
||
}
|
||
.big-chart svg {
|
||
width: 100%;
|
||
height: 180px;
|
||
}
|
||
.detail-chart__grid line {
|
||
stroke: rgba(255, 255, 255, 0.06);
|
||
stroke-width: 1;
|
||
vector-effect: non-scaling-stroke;
|
||
}
|
||
.detail-chart__axis text {
|
||
fill: rgba(214, 224, 230, 0.58);
|
||
font-size: 10px;
|
||
font-weight: 650;
|
||
}
|
||
.detail-chart__marker line {
|
||
stroke: rgba(101, 251, 210, 0.3);
|
||
stroke-width: 1;
|
||
stroke-dasharray: 3 4;
|
||
vector-effect: non-scaling-stroke;
|
||
}
|
||
.detail-chart__marker circle {
|
||
fill: #07110f;
|
||
stroke: var(--vault-mint);
|
||
stroke-width: 3;
|
||
vector-effect: non-scaling-stroke;
|
||
filter: drop-shadow(0 0 5px rgba(101, 251, 210, 0.8));
|
||
}
|
||
.detail-chart__marker text {
|
||
fill: #fff;
|
||
font-size: 10px;
|
||
font-weight: 800;
|
||
paint-order: stroke;
|
||
stroke: rgba(7, 11, 16, 0.94);
|
||
stroke-width: 4px;
|
||
stroke-linejoin: round;
|
||
}
|
||
.detail-chart {
|
||
animation: detail-chart-in 240ms ease-out;
|
||
}
|
||
@keyframes detail-chart-in {
|
||
from {
|
||
opacity: 0.35;
|
||
transform: translateY(3px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
.periods {
|
||
display: grid;
|
||
grid-template-columns: repeat(5, 1fr);
|
||
padding: 3px 9px;
|
||
}
|
||
.periods button {
|
||
min-height: 44px;
|
||
color: var(--muted);
|
||
background: none;
|
||
border: 0;
|
||
border-radius: 11px;
|
||
transition:
|
||
color 160ms ease,
|
||
background 160ms ease,
|
||
box-shadow 160ms ease,
|
||
transform 160ms ease;
|
||
}
|
||
.periods button.active {
|
||
color: #fff;
|
||
background: #252a31;
|
||
}
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.detail-chart {
|
||
animation: none;
|
||
}
|
||
}
|
||
.detail-copy {
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
}
|
||
.investment,
|
||
.stats {
|
||
padding: 16px;
|
||
background: var(--card);
|
||
}
|
||
.investment > small {
|
||
color: var(--muted);
|
||
}
|
||
.investment > strong {
|
||
display: block;
|
||
margin: 3px 0 14px;
|
||
font-size: 28px;
|
||
}
|
||
.investment > div {
|
||
display: grid;
|
||
grid-template-columns: 42px 1fr auto;
|
||
gap: 10px;
|
||
align-items: center;
|
||
}
|
||
.investment > div span:last-child {
|
||
text-align: right;
|
||
}
|
||
.investment b,
|
||
.investment small {
|
||
display: block;
|
||
}
|
||
.investment small {
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
}
|
||
.stats div {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 12px 0;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||
font-size: 12px;
|
||
}
|
||
.stats div:last-child {
|
||
border: 0;
|
||
}
|
||
.stats span {
|
||
color: var(--muted);
|
||
}
|
||
.detail-trade-dock {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 8px;
|
||
}
|
||
.activity-head,
|
||
.profile-head {
|
||
display: grid;
|
||
justify-items: center;
|
||
padding: 22px 0;
|
||
text-align: center;
|
||
}
|
||
.activity-head p,
|
||
.activity-head span {
|
||
margin: 0;
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
}
|
||
.activity-head strong {
|
||
font-size: 32px;
|
||
}
|
||
.profile-head > span {
|
||
width: 78px;
|
||
height: 78px;
|
||
font-size: 24px;
|
||
font-weight: 900;
|
||
}
|
||
.profile-head h2 {
|
||
margin: 10px 0 2px;
|
||
}
|
||
.profile-head p {
|
||
display: flex;
|
||
gap: 5px;
|
||
align-items: center;
|
||
margin: 0;
|
||
color: #49e4b2;
|
||
font-size: 12px;
|
||
}
|
||
.profile-stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 7px;
|
||
}
|
||
.profile-stats div {
|
||
display: grid;
|
||
gap: 4px;
|
||
padding: 13px 5px;
|
||
text-align: center;
|
||
background: var(--card);
|
||
border-radius: 16px;
|
||
}
|
||
.profile-stats small {
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
}
|
||
.settings {
|
||
padding: 0 14px;
|
||
background: var(--card);
|
||
}
|
||
.settings label {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
min-height: 67px;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||
}
|
||
.settings label > span {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
}
|
||
.settings b,
|
||
.settings small {
|
||
display: block;
|
||
}
|
||
.settings small {
|
||
max-width: 180px;
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
}
|
||
.sheet {
|
||
display: grid;
|
||
gap: 13px;
|
||
padding: 2px 12px calc(var(--sky-safe-area-bottom) + 20px);
|
||
}
|
||
.sheet h2,
|
||
.sheet p {
|
||
margin: 0;
|
||
}
|
||
.quote {
|
||
display: grid;
|
||
gap: 8px;
|
||
padding: 14px;
|
||
}
|
||
.quote div {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
color: var(--muted);
|
||
font-size: 13px;
|
||
}
|
||
.quote small {
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
}
|
||
|
||
/* VaultX owns a dense trading surface while shared phone geometry stays in Sky UI. */
|
||
.crypto-app {
|
||
--card: #10151d;
|
||
--card-strong: #151c26;
|
||
--muted: rgba(220, 231, 238, 0.57);
|
||
--vault-mint: #65fbd2;
|
||
--vault-blue: #68a7ff;
|
||
background:
|
||
radial-gradient(
|
||
circle at 88% 9%,
|
||
rgba(71, 133, 175, 0.16),
|
||
transparent 28%
|
||
),
|
||
radial-gradient(
|
||
circle at 0% 34%,
|
||
rgba(51, 219, 176, 0.09),
|
||
transparent 28%
|
||
),
|
||
linear-gradient(180deg, #070b11 0%, #030509 74%);
|
||
}
|
||
.crypto-app::before {
|
||
position: absolute;
|
||
inset: 0;
|
||
pointer-events: none;
|
||
content: '';
|
||
background-image: linear-gradient(
|
||
rgba(255, 255, 255, 0.018) 1px,
|
||
transparent 1px
|
||
);
|
||
background-size: 100% 48px;
|
||
mask-image: linear-gradient(to bottom, #000, transparent 58%);
|
||
}
|
||
.crypto-app :deep(.sky-navbar) {
|
||
--sky-navbar-glass: #070b11;
|
||
z-index: 12;
|
||
color: #f8fbfd;
|
||
background: #070b11;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
|
||
}
|
||
.crypto-app :deep(.sky-navbar__right) {
|
||
background: transparent;
|
||
box-shadow: none;
|
||
-webkit-backdrop-filter: none;
|
||
backdrop-filter: none;
|
||
}
|
||
.crypto-app :deep(.vault-navbar--profile) {
|
||
--sky-navbar-glass: transparent;
|
||
background: transparent;
|
||
border-bottom-color: transparent;
|
||
}
|
||
.crypto-app :deep(.vault-navbar--profile .sky-navbar__blur) {
|
||
-webkit-backdrop-filter: none;
|
||
backdrop-filter: none;
|
||
-webkit-mask-image: none;
|
||
mask-image: none;
|
||
}
|
||
.crypto-app :deep(.vault-navbar--profile .sky-navbar__background) {
|
||
background: transparent;
|
||
}
|
||
.vault-header-brand {
|
||
display: inline-flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
.vault-header-logo {
|
||
width: 30px;
|
||
height: 30px;
|
||
flex: 0 0 auto;
|
||
object-fit: contain;
|
||
filter: drop-shadow(0 4px 6px rgba(49, 214, 170, 0.22))
|
||
drop-shadow(0 1px 2px rgba(246, 196, 83, 0.24));
|
||
}
|
||
.vault-section-title {
|
||
font-size: 17px;
|
||
letter-spacing: -0.02em;
|
||
}
|
||
.profile-signout {
|
||
height: 34px;
|
||
min-height: 34px;
|
||
gap: 6px;
|
||
padding: 0 10px;
|
||
color: #ff9aa8;
|
||
font-size: 10px;
|
||
font-weight: 850;
|
||
white-space: nowrap;
|
||
background: linear-gradient(
|
||
145deg,
|
||
rgba(255, 117, 136, 0.15),
|
||
rgba(255, 82, 106, 0.08)
|
||
);
|
||
border: 1px solid rgba(255, 117, 136, 0.25);
|
||
border-radius: var(--sky-radius-pill);
|
||
box-shadow:
|
||
inset 0 1px rgba(255, 255, 255, 0.045),
|
||
0 7px 18px rgba(255, 82, 106, 0.09);
|
||
transition:
|
||
color 160ms ease,
|
||
background 160ms ease,
|
||
border-color 160ms ease,
|
||
transform 160ms ease;
|
||
}
|
||
.profile-signout svg {
|
||
flex: 0 0 auto;
|
||
color: #ff7588;
|
||
}
|
||
.profile-signout:hover {
|
||
color: #fff;
|
||
background: linear-gradient(
|
||
145deg,
|
||
rgba(255, 117, 136, 0.24),
|
||
rgba(255, 82, 106, 0.14)
|
||
);
|
||
border-color: rgba(255, 135, 151, 0.4);
|
||
}
|
||
.profile-signout:active:not(:disabled) {
|
||
opacity: 1;
|
||
transform: translateY(1px) scale(0.98);
|
||
}
|
||
.vault-detail-title {
|
||
display: inline-flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
.vault-detail-logo {
|
||
width: 30px;
|
||
height: 30px;
|
||
border-radius: 11px;
|
||
}
|
||
.vault-detail-title > span:last-child {
|
||
display: grid;
|
||
justify-items: start;
|
||
line-height: 1.05;
|
||
}
|
||
.vault-detail-title b {
|
||
font-size: 14px;
|
||
}
|
||
.vault-detail-title small {
|
||
margin-top: 2px;
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
}
|
||
.crypto-app :deep(.sky-scroll-area__content) {
|
||
position: relative;
|
||
}
|
||
.crypto-app :deep(.sky-pill-navigation) {
|
||
--sky-glass-solid: rgba(14, 19, 27, 0.97);
|
||
}
|
||
.crypto-app :deep(.sky-pill-navigation .sky-segmented-button--active) {
|
||
color: #fff;
|
||
}
|
||
.auth-hero__mark {
|
||
display: grid;
|
||
width: 66px;
|
||
height: 66px;
|
||
place-items: center;
|
||
}
|
||
.auth-hero__mark img {
|
||
display: block;
|
||
width: 66px;
|
||
height: 66px;
|
||
object-fit: contain;
|
||
filter: drop-shadow(0 12px 22px rgba(49, 214, 170, 0.18));
|
||
}
|
||
.portfolio-shell {
|
||
position: relative;
|
||
min-height: 294px;
|
||
margin-bottom: 12px;
|
||
padding: 18px 17px 15px;
|
||
overflow: hidden;
|
||
background:
|
||
radial-gradient(
|
||
circle at 80% 12%,
|
||
rgba(91, 159, 255, 0.2),
|
||
transparent 27%
|
||
),
|
||
radial-gradient(
|
||
circle at 24% 82%,
|
||
rgba(101, 251, 210, 0.16),
|
||
transparent 35%
|
||
),
|
||
linear-gradient(145deg, #151d28, #0a0f16 66%);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: 30px;
|
||
box-shadow:
|
||
0 22px 50px rgba(0, 0, 0, 0.36),
|
||
inset 0 1px rgba(255, 255, 255, 0.08);
|
||
}
|
||
.portfolio-shell::after {
|
||
position: absolute;
|
||
inset: 0;
|
||
pointer-events: none;
|
||
content: '';
|
||
background: linear-gradient(
|
||
112deg,
|
||
transparent 38%,
|
||
rgba(255, 255, 255, 0.045) 49%,
|
||
transparent 60%
|
||
);
|
||
}
|
||
.portfolio-glow {
|
||
position: absolute;
|
||
top: -38px;
|
||
right: -24px;
|
||
width: 150px;
|
||
height: 120px;
|
||
pointer-events: none;
|
||
background: radial-gradient(
|
||
circle,
|
||
rgba(91, 159, 255, 0.28) 0,
|
||
rgba(101, 251, 210, 0.13) 38%,
|
||
transparent 72%
|
||
);
|
||
border: 0;
|
||
border-radius: 50%;
|
||
filter: blur(24px);
|
||
}
|
||
.portfolio-topline,
|
||
.portfolio-value,
|
||
.portfolio-metrics,
|
||
.allocation-copy,
|
||
.market-tile__top,
|
||
.featured-market__top,
|
||
.featured-market__quote,
|
||
.featured-market__stats {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.portfolio-topline {
|
||
position: relative;
|
||
z-index: 1;
|
||
justify-content: space-between;
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.04em;
|
||
}
|
||
.portfolio-topline > span:first-child {
|
||
display: flex;
|
||
gap: 6px;
|
||
align-items: center;
|
||
}
|
||
.portfolio-topline svg {
|
||
color: var(--vault-mint);
|
||
}
|
||
.forecast-pill {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 5px 8px;
|
||
color: #b9c8ff;
|
||
background: rgba(112, 143, 255, 0.09);
|
||
border: 1px solid rgba(140, 169, 255, 0.18);
|
||
border-radius: var(--sky-radius-pill);
|
||
}
|
||
.live-pill,
|
||
.profile-status {
|
||
display: inline-flex;
|
||
gap: 6px;
|
||
align-items: center;
|
||
padding: 6px 9px;
|
||
color: rgba(255, 255, 255, 0.76);
|
||
background: rgba(255, 255, 255, 0.06);
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
border-radius: var(--sky-radius-pill);
|
||
}
|
||
.live-pill i,
|
||
.profile-status i {
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
background: var(--vault-mint);
|
||
box-shadow: 0 0 9px var(--vault-mint);
|
||
}
|
||
.portfolio-value {
|
||
position: relative;
|
||
z-index: 1;
|
||
justify-content: space-between;
|
||
margin-top: 15px;
|
||
}
|
||
.portfolio-value strong {
|
||
font-size: 35px;
|
||
letter-spacing: -0.055em;
|
||
}
|
||
.portfolio-value > span {
|
||
display: grid;
|
||
justify-items: end;
|
||
font-size: 12px;
|
||
font-weight: 800;
|
||
}
|
||
.portfolio-value small {
|
||
margin-top: 2px;
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
}
|
||
.portfolio-chart {
|
||
position: relative;
|
||
height: 148px;
|
||
margin: 8px -8px 0;
|
||
}
|
||
.portfolio-chart svg {
|
||
display: block;
|
||
width: 100%;
|
||
height: 126px;
|
||
overflow: visible;
|
||
}
|
||
.portfolio-chart__legend {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 12px;
|
||
height: 22px;
|
||
padding-right: 7px;
|
||
color: var(--muted);
|
||
font-size: 9px;
|
||
font-weight: 700;
|
||
}
|
||
.portfolio-chart__legend span {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
.portfolio-chart__legend i {
|
||
width: 13px;
|
||
height: 2px;
|
||
border-radius: 4px;
|
||
background: var(--vault-mint);
|
||
}
|
||
.portfolio-chart__legend i.is-forecast {
|
||
background: repeating-linear-gradient(
|
||
90deg,
|
||
#8ca9ff 0 4px,
|
||
transparent 4px 7px
|
||
);
|
||
}
|
||
.portfolio-chart__forecast-zone {
|
||
fill: rgba(112, 143, 255, 0.055);
|
||
}
|
||
.portfolio-chart__grid,
|
||
.portfolio-chart__zero,
|
||
.portfolio-chart__boundary {
|
||
vector-effect: non-scaling-stroke;
|
||
}
|
||
.portfolio-chart__grid {
|
||
stroke: rgba(255, 255, 255, 0.055);
|
||
stroke-width: 1;
|
||
}
|
||
.portfolio-chart__zero {
|
||
stroke: rgba(255, 255, 255, 0.2);
|
||
stroke-width: 1;
|
||
stroke-dasharray: 2 5;
|
||
}
|
||
.portfolio-chart__boundary {
|
||
stroke: rgba(140, 169, 255, 0.22);
|
||
stroke-width: 1;
|
||
stroke-dasharray: 3 5;
|
||
}
|
||
.portfolio-history,
|
||
.portfolio-forecast,
|
||
.featured-market polyline {
|
||
fill: none;
|
||
stroke: #dffff6;
|
||
stroke-width: 2.5;
|
||
stroke-linecap: round;
|
||
stroke-linejoin: round;
|
||
vector-effect: non-scaling-stroke;
|
||
filter: drop-shadow(0 0 8px rgba(101, 251, 210, 0.5));
|
||
}
|
||
.portfolio-forecast {
|
||
stroke: #8ca9ff;
|
||
stroke-dasharray: 7 7;
|
||
filter: drop-shadow(0 0 7px rgba(112, 143, 255, 0.4));
|
||
}
|
||
.portfolio-current-dot {
|
||
fill: var(--vault-mint);
|
||
stroke: #fff;
|
||
stroke-width: 2;
|
||
vector-effect: non-scaling-stroke;
|
||
filter: drop-shadow(0 0 6px var(--vault-mint));
|
||
}
|
||
.portfolio-current-label rect {
|
||
fill: #101923;
|
||
stroke: rgba(101, 251, 210, 0.45);
|
||
stroke-width: 1;
|
||
vector-effect: non-scaling-stroke;
|
||
}
|
||
.portfolio-current-label text {
|
||
fill: #fff;
|
||
font-size: 9px;
|
||
font-weight: 800;
|
||
letter-spacing: 0.01em;
|
||
}
|
||
.portfolio-scale-label {
|
||
fill: rgba(255, 255, 255, 0.46);
|
||
font-size: 9px;
|
||
font-weight: 700;
|
||
}
|
||
.portfolio-metrics {
|
||
position: relative;
|
||
z-index: 1;
|
||
justify-content: space-around;
|
||
padding-top: 12px;
|
||
border-top: 1px solid rgba(255, 255, 255, 0.07);
|
||
}
|
||
.portfolio-metrics div {
|
||
display: grid;
|
||
gap: 2px;
|
||
min-width: 40%;
|
||
}
|
||
.portfolio-metrics div:last-child {
|
||
text-align: right;
|
||
}
|
||
.portfolio-metrics small {
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
}
|
||
.portfolio-metrics b {
|
||
font-size: 13px;
|
||
}
|
||
.portfolio-metrics > i {
|
||
width: 1px;
|
||
height: 27px;
|
||
background: rgba(255, 255, 255, 0.08);
|
||
}
|
||
.quick {
|
||
gap: 8px;
|
||
margin: 0 0 14px;
|
||
}
|
||
.quick button {
|
||
align-content: center;
|
||
min-width: 0;
|
||
min-height: 84px;
|
||
gap: 8px;
|
||
padding: 11px 5px;
|
||
background: linear-gradient(
|
||
160deg,
|
||
rgba(30, 39, 51, 0.94),
|
||
rgba(12, 17, 24, 0.94)
|
||
);
|
||
border: 1px solid rgba(255, 255, 255, 0.075);
|
||
border-radius: 19px;
|
||
box-shadow: inset 0 1px rgba(255, 255, 255, 0.045);
|
||
transition:
|
||
transform 180ms ease,
|
||
background 180ms ease,
|
||
border-color 180ms ease,
|
||
box-shadow 180ms ease;
|
||
}
|
||
.quick button span {
|
||
width: 38px;
|
||
height: 38px;
|
||
border: 1px solid rgba(101, 251, 210, 0.14);
|
||
border-radius: 14px;
|
||
color: var(--vault-mint);
|
||
background: rgba(101, 251, 210, 0.08);
|
||
transition:
|
||
transform 180ms ease,
|
||
background 180ms ease,
|
||
border-color 180ms ease,
|
||
box-shadow 180ms ease;
|
||
}
|
||
.quick button b {
|
||
max-width: 100%;
|
||
overflow: hidden;
|
||
font-size: 12px;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.quick button:focus-visible {
|
||
border-color: rgba(101, 251, 210, 0.5);
|
||
outline: 2px solid rgba(101, 251, 210, 0.78);
|
||
outline-offset: 2px;
|
||
}
|
||
.quick button:active {
|
||
transform: translateY(-1px) scale(0.98);
|
||
}
|
||
@media (hover: hover) and (pointer: fine) {
|
||
.quick button:hover {
|
||
background: linear-gradient(
|
||
160deg,
|
||
rgba(37, 52, 64, 0.98),
|
||
rgba(15, 23, 31, 0.98)
|
||
);
|
||
border-color: rgba(101, 251, 210, 0.28);
|
||
box-shadow:
|
||
0 14px 26px rgba(0, 0, 0, 0.3),
|
||
0 0 20px rgba(101, 251, 210, 0.06),
|
||
inset 0 1px rgba(255, 255, 255, 0.08);
|
||
transform: translateY(-3px);
|
||
}
|
||
.quick button:hover span {
|
||
background: rgba(101, 251, 210, 0.15);
|
||
border-color: rgba(101, 251, 210, 0.36);
|
||
box-shadow: 0 8px 18px rgba(101, 251, 210, 0.14);
|
||
transform: scale(1.05);
|
||
}
|
||
}
|
||
.allocation-card {
|
||
margin-bottom: 21px;
|
||
padding: 14px;
|
||
background: linear-gradient(
|
||
125deg,
|
||
rgba(21, 29, 39, 0.98),
|
||
rgba(12, 17, 23, 0.98)
|
||
);
|
||
border: 1px solid rgba(255, 255, 255, 0.07);
|
||
}
|
||
.allocation-copy {
|
||
gap: 10px;
|
||
}
|
||
.allocation-copy > span {
|
||
display: grid;
|
||
place-items: center;
|
||
width: 38px;
|
||
height: 38px;
|
||
color: var(--vault-mint);
|
||
background: rgba(101, 251, 210, 0.1);
|
||
border-radius: 13px;
|
||
}
|
||
.allocation-copy > div {
|
||
display: grid;
|
||
flex: 1;
|
||
gap: 2px;
|
||
}
|
||
.allocation-copy small {
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
}
|
||
.allocation-copy b {
|
||
font-size: 13px;
|
||
}
|
||
.allocation-copy > strong {
|
||
color: var(--vault-mint);
|
||
font-size: 14px;
|
||
}
|
||
.allocation-track,
|
||
.asset-allocation {
|
||
display: block;
|
||
height: 4px;
|
||
margin-top: 11px;
|
||
overflow: hidden;
|
||
background: rgba(255, 255, 255, 0.07);
|
||
border-radius: var(--sky-radius-pill);
|
||
}
|
||
.allocation-track i,
|
||
.asset-allocation i {
|
||
display: block;
|
||
height: 100%;
|
||
background: linear-gradient(90deg, var(--vault-mint), var(--vault-blue));
|
||
border-radius: inherit;
|
||
}
|
||
.asset-row {
|
||
min-height: 72px;
|
||
margin-bottom: 9px;
|
||
padding: 12px;
|
||
background: linear-gradient(
|
||
145deg,
|
||
rgba(19, 26, 35, 0.97),
|
||
rgba(10, 14, 20, 0.97)
|
||
);
|
||
border-color: rgba(255, 255, 255, 0.07);
|
||
border-radius: 20px;
|
||
box-shadow: inset 0 1px rgba(255, 255, 255, 0.035);
|
||
}
|
||
.asset-allocation {
|
||
width: 72px;
|
||
height: 3px;
|
||
margin-top: 7px;
|
||
}
|
||
.featured-market {
|
||
position: relative;
|
||
width: 100%;
|
||
margin-bottom: 20px;
|
||
padding: 16px;
|
||
overflow: hidden;
|
||
color: #fff;
|
||
text-align: left;
|
||
background:
|
||
radial-gradient(
|
||
circle at 88% 12%,
|
||
rgba(101, 251, 210, 0.16),
|
||
transparent 28%
|
||
),
|
||
linear-gradient(145deg, #18212d, #0a0f16 68%);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: 26px;
|
||
box-shadow:
|
||
0 20px 45px rgba(0, 0, 0, 0.28),
|
||
inset 0 1px rgba(255, 255, 255, 0.07);
|
||
}
|
||
.featured-market__top {
|
||
gap: 11px;
|
||
}
|
||
.featured-market__top > span:nth-child(2) {
|
||
display: grid;
|
||
flex: 1;
|
||
}
|
||
.featured-market__top small {
|
||
color: var(--vault-mint);
|
||
font-size: 11px;
|
||
font-weight: 800;
|
||
letter-spacing: 0.08em;
|
||
text-transform: uppercase;
|
||
}
|
||
.featured-market__top b {
|
||
margin-top: 2px;
|
||
font-size: 16px;
|
||
}
|
||
.featured-market__top em {
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
font-style: normal;
|
||
}
|
||
.featured-market__quote {
|
||
justify-content: space-between;
|
||
margin-top: 17px;
|
||
}
|
||
.featured-market__quote strong {
|
||
font-size: 28px;
|
||
letter-spacing: -0.04em;
|
||
}
|
||
.featured-market__quote span {
|
||
font-size: 13px;
|
||
font-weight: 800;
|
||
}
|
||
.featured-market > svg {
|
||
width: calc(100% + 32px);
|
||
height: 74px;
|
||
margin: 2px -16px 6px;
|
||
}
|
||
.featured-market__stats {
|
||
gap: 8px;
|
||
}
|
||
.featured-market__stats > span {
|
||
display: grid;
|
||
flex: 1;
|
||
gap: 2px;
|
||
padding: 9px 10px;
|
||
background: rgba(255, 255, 255, 0.045);
|
||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||
border-radius: 12px;
|
||
}
|
||
.featured-market__stats small {
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
}
|
||
.featured-market__stats b {
|
||
font-size: 12px;
|
||
}
|
||
.market-list-heading {
|
||
margin-top: 2px;
|
||
}
|
||
.market-grid--advanced {
|
||
grid-template-columns: 1fr;
|
||
gap: 9px;
|
||
}
|
||
.market-grid--advanced button {
|
||
display: block;
|
||
min-height: 154px;
|
||
padding: 14px;
|
||
background: linear-gradient(
|
||
145deg,
|
||
rgba(19, 26, 35, 0.96),
|
||
rgba(10, 14, 20, 0.96)
|
||
);
|
||
border-radius: 20px;
|
||
}
|
||
.market-tile__top {
|
||
gap: 10px;
|
||
}
|
||
.market-tile__top > span:nth-child(2) {
|
||
display: grid;
|
||
flex: 1;
|
||
}
|
||
.market-tile__top b {
|
||
font-size: 13px;
|
||
}
|
||
.market-tile__top small {
|
||
margin: 1px 0 0 !important;
|
||
font-size: 11px !important;
|
||
}
|
||
.market-tile__top em {
|
||
padding: 5px 7px;
|
||
font-size: 11px;
|
||
background: rgba(101, 251, 210, 0.08);
|
||
border-radius: var(--sky-radius-pill);
|
||
}
|
||
.market-tile__price {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 6px;
|
||
margin-top: 11px;
|
||
}
|
||
.market-tile__price b {
|
||
font-size: 20px;
|
||
}
|
||
.market-tile__price small {
|
||
margin: 0 !important;
|
||
}
|
||
.market-grid--advanced svg {
|
||
height: 43px;
|
||
margin-top: 4px;
|
||
}
|
||
.activity-overview {
|
||
margin-bottom: 18px;
|
||
padding: 1px;
|
||
overflow: hidden;
|
||
background: linear-gradient(
|
||
135deg,
|
||
rgba(101, 251, 210, 0.3),
|
||
rgba(104, 167, 255, 0.17) 48%,
|
||
rgba(255, 255, 255, 0.06)
|
||
);
|
||
border-radius: 29px;
|
||
box-shadow: 0 20px 46px rgba(0, 0, 0, 0.32);
|
||
}
|
||
.activity-dashboard {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
min-height: 122px;
|
||
margin: 0;
|
||
padding: 21px 18px 17px;
|
||
overflow: hidden;
|
||
text-align: left;
|
||
background:
|
||
radial-gradient(
|
||
circle at 91% 18%,
|
||
rgba(101, 251, 210, 0.21),
|
||
transparent 33%
|
||
),
|
||
radial-gradient(
|
||
circle at 12% 105%,
|
||
rgba(104, 167, 255, 0.16),
|
||
transparent 42%
|
||
),
|
||
linear-gradient(145deg, #18232f, #0b1119 68%);
|
||
border: 0;
|
||
border-radius: 28px 28px 18px 18px;
|
||
box-shadow: inset 0 1px rgba(255, 255, 255, 0.08);
|
||
}
|
||
.activity-dashboard::after {
|
||
position: absolute;
|
||
inset: 0;
|
||
pointer-events: none;
|
||
content: '';
|
||
background: linear-gradient(
|
||
118deg,
|
||
transparent 32%,
|
||
rgba(255, 255, 255, 0.04) 48%,
|
||
transparent 64%
|
||
);
|
||
}
|
||
.activity-dashboard__copy {
|
||
position: relative;
|
||
z-index: 1;
|
||
display: grid;
|
||
gap: 5px;
|
||
}
|
||
.activity-dashboard__copy p {
|
||
display: flex;
|
||
gap: 6px;
|
||
align-items: center;
|
||
color: rgba(255, 255, 255, 0.62);
|
||
font-weight: 700;
|
||
letter-spacing: 0.02em;
|
||
}
|
||
.activity-dashboard__copy p svg {
|
||
color: var(--vault-mint);
|
||
}
|
||
.activity-dashboard__copy strong {
|
||
font-size: 29px;
|
||
letter-spacing: -0.045em;
|
||
}
|
||
.activity-trade-count {
|
||
display: inline-flex;
|
||
gap: 6px;
|
||
align-items: center;
|
||
justify-self: start;
|
||
padding: 5px 8px;
|
||
color: rgba(255, 255, 255, 0.68) !important;
|
||
background: rgba(255, 255, 255, 0.055);
|
||
border: 1px solid rgba(255, 255, 255, 0.065);
|
||
border-radius: var(--sky-radius-pill);
|
||
}
|
||
.activity-trade-count i {
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
background: var(--vault-mint);
|
||
box-shadow: 0 0 8px rgba(101, 251, 210, 0.68);
|
||
}
|
||
.activity-status {
|
||
position: relative;
|
||
z-index: 1;
|
||
display: grid;
|
||
place-items: center;
|
||
gap: 5px;
|
||
width: 72px;
|
||
min-height: 66px;
|
||
flex: 0 0 auto;
|
||
padding: 7px;
|
||
background: linear-gradient(
|
||
145deg,
|
||
rgba(101, 251, 210, 0.13),
|
||
rgba(8, 17, 24, 0.7)
|
||
);
|
||
border: 1px solid rgba(101, 251, 210, 0.18);
|
||
border-radius: 17px;
|
||
box-shadow:
|
||
inset 0 1px rgba(255, 255, 255, 0.07),
|
||
0 10px 24px rgba(0, 0, 0, 0.18);
|
||
}
|
||
.activity-status__icon {
|
||
display: grid;
|
||
place-items: center;
|
||
width: 32px;
|
||
height: 32px;
|
||
color: #07130f;
|
||
background: linear-gradient(145deg, #91ffe1, #40d6b0);
|
||
border-radius: 10px;
|
||
box-shadow: 0 7px 16px rgba(101, 251, 210, 0.18);
|
||
}
|
||
.activity-status b {
|
||
color: var(--vault-mint);
|
||
font-size: 9px;
|
||
line-height: 1.1;
|
||
text-align: center;
|
||
white-space: nowrap;
|
||
}
|
||
.activity-filter-panel {
|
||
padding: 8px;
|
||
background: linear-gradient(180deg, #0b1118, #080d13);
|
||
border-top: 1px solid rgba(255, 255, 255, 0.055);
|
||
border-radius: 0 0 28px 28px;
|
||
}
|
||
.activity-filters {
|
||
width: 100%;
|
||
height: 50px;
|
||
min-height: 50px;
|
||
gap: 4px;
|
||
padding: 3px;
|
||
background: rgba(255, 255, 255, 0.045);
|
||
border: 1px solid rgba(255, 255, 255, 0.055);
|
||
border-radius: 17px;
|
||
box-shadow: inset 0 1px rgba(255, 255, 255, 0.035);
|
||
}
|
||
:deep(.activity-filters.sky-segmented--strong .sky-segmented__highlight) {
|
||
top: 3px;
|
||
bottom: 3px;
|
||
inset-inline-start: 3px;
|
||
background: linear-gradient(
|
||
135deg,
|
||
rgba(101, 251, 210, 0.2),
|
||
rgba(104, 167, 255, 0.14)
|
||
);
|
||
border: 1px solid rgba(101, 251, 210, 0.35);
|
||
border-radius: 13px;
|
||
box-shadow:
|
||
inset 0 1px rgba(255, 255, 255, 0.08),
|
||
0 7px 18px rgba(0, 0, 0, 0.22);
|
||
}
|
||
.activity-filters :deep(.sky-segmented-button) {
|
||
min-height: 42px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0 7px;
|
||
color: rgba(255, 255, 255, 0.5);
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
line-height: 1;
|
||
border-radius: 13px;
|
||
transition:
|
||
color var(--sky-transition-normal) ease,
|
||
transform var(--sky-transition-fast) ease;
|
||
}
|
||
.activity-filters :deep(.activity-filter-content) {
|
||
display: inline-flex;
|
||
max-width: 100%;
|
||
gap: 6px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 15px;
|
||
line-height: 1;
|
||
white-space: nowrap;
|
||
transform: translateY(-3px);
|
||
}
|
||
.activity-filters :deep(.activity-filter-content svg) {
|
||
display: block;
|
||
flex: 0 0 auto;
|
||
}
|
||
:deep(.activity-filters.sky-segmented--strong .sky-segmented-button--active) {
|
||
color: #fff;
|
||
}
|
||
.activity-filters :deep(.sky-segmented-button:active:not(:disabled)) {
|
||
transform: scale(0.98);
|
||
}
|
||
.activity-title {
|
||
margin-top: 0;
|
||
}
|
||
.activity-row {
|
||
position: relative;
|
||
min-height: 70px;
|
||
margin-left: 10px;
|
||
width: calc(100% - 10px);
|
||
overflow: visible;
|
||
background: linear-gradient(
|
||
145deg,
|
||
rgba(19, 26, 35, 0.95),
|
||
rgba(9, 13, 18, 0.95)
|
||
);
|
||
}
|
||
.activity-row::before {
|
||
position: absolute;
|
||
top: -13px;
|
||
bottom: -13px;
|
||
left: -11px;
|
||
width: 1px;
|
||
content: '';
|
||
background: rgba(255, 255, 255, 0.09);
|
||
}
|
||
.timeline-dot {
|
||
position: absolute;
|
||
top: 31px;
|
||
left: -14px;
|
||
width: 7px;
|
||
height: 7px;
|
||
border: 2px solid #070b11;
|
||
border-radius: 50%;
|
||
background: var(--vault-mint);
|
||
box-shadow: 0 0 7px var(--vault-mint);
|
||
}
|
||
.activity-icon--withdrawal {
|
||
color: #ff7588;
|
||
background: rgba(255, 92, 112, 0.1);
|
||
}
|
||
.activity-icon--deposit {
|
||
color: var(--vault-blue);
|
||
background: rgba(104, 167, 255, 0.1);
|
||
}
|
||
.profile-card {
|
||
position: relative;
|
||
min-height: 244px;
|
||
padding: 18px;
|
||
overflow: hidden;
|
||
background:
|
||
radial-gradient(
|
||
circle at 82% 14%,
|
||
rgba(104, 167, 255, 0.28),
|
||
transparent 30%
|
||
),
|
||
radial-gradient(
|
||
circle at 16% 90%,
|
||
rgba(101, 251, 210, 0.2),
|
||
transparent 30%
|
||
),
|
||
linear-gradient(145deg, #1a2532, #0b1119 70%);
|
||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||
border-radius: 29px;
|
||
box-shadow:
|
||
0 22px 55px rgba(0, 0, 0, 0.35),
|
||
inset 0 1px rgba(255, 255, 255, 0.08);
|
||
}
|
||
.profile-card__mesh {
|
||
position: absolute;
|
||
right: -35px;
|
||
bottom: -50px;
|
||
width: 190px;
|
||
height: 190px;
|
||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||
border-radius: 50%;
|
||
box-shadow:
|
||
inset 0 0 0 24px rgba(255, 255, 255, 0.015),
|
||
inset 0 0 0 48px rgba(255, 255, 255, 0.012);
|
||
}
|
||
.profile-card__top {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
.profile-avatar {
|
||
display: grid;
|
||
place-items: center;
|
||
width: 56px;
|
||
height: 56px;
|
||
color: #07100e;
|
||
font-size: 17px;
|
||
font-weight: 900;
|
||
background: linear-gradient(145deg, #b8ffea, #4b94ff);
|
||
border: 1px solid rgba(255, 255, 255, 0.55);
|
||
border-radius: 19px;
|
||
box-shadow: 0 12px 30px rgba(101, 251, 210, 0.18);
|
||
}
|
||
.profile-status {
|
||
max-width: 150px;
|
||
color: var(--vault-mint);
|
||
font-size: 11px;
|
||
}
|
||
.profile-card__identity {
|
||
position: relative;
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: end;
|
||
justify-content: space-between;
|
||
margin-top: 15px;
|
||
}
|
||
.profile-card__identity > span {
|
||
min-width: 0;
|
||
flex: 1;
|
||
}
|
||
.profile-card h2 {
|
||
margin: 0 0 3px;
|
||
font-size: 24px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.profile-card__identity p {
|
||
display: flex;
|
||
gap: 6px;
|
||
align-items: center;
|
||
margin: 0;
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
}
|
||
.profile-card__identity p svg {
|
||
color: var(--vault-mint);
|
||
}
|
||
.profile-edit-button {
|
||
width: auto;
|
||
min-width: auto;
|
||
min-height: 36px;
|
||
flex: 0 0 auto;
|
||
padding: 0 12px;
|
||
color: #fff;
|
||
background: rgba(101, 251, 210, 0.1);
|
||
border: 1px solid rgba(101, 251, 210, 0.2);
|
||
box-shadow: inset 0 1px rgba(255, 255, 255, 0.06);
|
||
}
|
||
.profile-card__id {
|
||
position: relative;
|
||
display: grid;
|
||
grid-template-columns: 1fr auto;
|
||
margin-top: 20px;
|
||
padding-top: 12px;
|
||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||
}
|
||
.profile-card__id small {
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
letter-spacing: 0.14em;
|
||
}
|
||
.profile-card__id b {
|
||
grid-column: 1;
|
||
margin-top: 2px;
|
||
font-size: 13px;
|
||
letter-spacing: 0.16em;
|
||
}
|
||
.profile-card__id svg {
|
||
grid-column: 2;
|
||
grid-row: 1 / span 2;
|
||
color: rgba(255, 255, 255, 0.45);
|
||
}
|
||
.wallet-key-card {
|
||
display: grid;
|
||
gap: 12px;
|
||
margin-top: 11px;
|
||
padding: 13px;
|
||
background:
|
||
radial-gradient(
|
||
circle at 92% 0%,
|
||
rgba(101, 251, 210, 0.14),
|
||
transparent 36%
|
||
),
|
||
linear-gradient(145deg, #151e28, #0a1017);
|
||
border: 1px solid rgba(101, 251, 210, 0.13);
|
||
border-radius: 19px;
|
||
}
|
||
.wallet-key-card__content {
|
||
display: grid;
|
||
grid-template-columns: auto minmax(0, 1fr);
|
||
gap: 11px;
|
||
align-items: center;
|
||
}
|
||
.wallet-key-card__icon {
|
||
display: grid;
|
||
place-items: center;
|
||
width: 42px;
|
||
height: 42px;
|
||
color: var(--vault-mint);
|
||
background: rgba(101, 251, 210, 0.09);
|
||
border: 1px solid rgba(101, 251, 210, 0.13);
|
||
border-radius: 14px;
|
||
}
|
||
.wallet-key-card__content > span:nth-child(2) {
|
||
display: grid;
|
||
min-width: 0;
|
||
gap: 2px;
|
||
}
|
||
.wallet-key-card small,
|
||
.wallet-key-card em {
|
||
color: var(--muted);
|
||
font-size: 9px;
|
||
font-style: normal;
|
||
}
|
||
.wallet-key-card b {
|
||
overflow: hidden;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
font-size: 11px;
|
||
letter-spacing: 0.035em;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.wallet-key-card__actions {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 7px;
|
||
}
|
||
.wallet-key-card__actions :deep(.sky-button) {
|
||
width: 100%;
|
||
min-width: 0;
|
||
min-height: 42px;
|
||
gap: 6px;
|
||
padding: 0 10px;
|
||
overflow: hidden;
|
||
font-size: 11px;
|
||
font-weight: 800;
|
||
border-radius: 13px;
|
||
}
|
||
.wallet-key-card__actions :deep(.sky-button > span) {
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.profile-stats--premium {
|
||
margin-top: 10px;
|
||
}
|
||
.profile-stats--premium div {
|
||
min-height: 66px;
|
||
align-content: center;
|
||
background: linear-gradient(145deg, #151c25, #0b1016);
|
||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||
}
|
||
.profile-feedback {
|
||
margin: 10px 3px 0;
|
||
}
|
||
.security-overview {
|
||
display: flex;
|
||
gap: 13px;
|
||
align-items: center;
|
||
margin-top: 11px;
|
||
padding: 13px;
|
||
background: linear-gradient(
|
||
135deg,
|
||
rgba(101, 251, 210, 0.09),
|
||
rgba(104, 167, 255, 0.06)
|
||
);
|
||
border: 1px solid rgba(101, 251, 210, 0.14);
|
||
}
|
||
.security-score {
|
||
position: relative;
|
||
display: grid;
|
||
place-items: center;
|
||
width: 62px;
|
||
height: 62px;
|
||
flex: 0 0 auto;
|
||
color: var(--vault-mint);
|
||
border: 4px solid rgba(101, 251, 210, 0.2);
|
||
border-top-color: var(--vault-mint);
|
||
border-radius: 50%;
|
||
}
|
||
.security-score svg {
|
||
position: absolute;
|
||
opacity: 0.16;
|
||
}
|
||
.security-score b {
|
||
font-size: 14px;
|
||
line-height: 1;
|
||
}
|
||
.security-score small {
|
||
font-size: 9px;
|
||
}
|
||
.security-overview > span {
|
||
display: grid;
|
||
gap: 2px;
|
||
}
|
||
.security-overview > span small {
|
||
color: var(--vault-mint);
|
||
font-size: 11px;
|
||
}
|
||
.security-overview > span b {
|
||
font-size: 13px;
|
||
}
|
||
.security-overview > span em {
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
font-style: normal;
|
||
line-height: 1.35;
|
||
}
|
||
.settings,
|
||
.stats,
|
||
.investment,
|
||
.movers {
|
||
background: linear-gradient(
|
||
145deg,
|
||
rgba(20, 27, 36, 0.97),
|
||
rgba(9, 13, 18, 0.97)
|
||
);
|
||
border: 1px solid rgba(255, 255, 255, 0.065);
|
||
box-shadow: inset 0 1px rgba(255, 255, 255, 0.035);
|
||
}
|
||
.settings label > span > svg {
|
||
color: var(--vault-mint);
|
||
}
|
||
.detail-head {
|
||
position: relative;
|
||
margin-bottom: 10px;
|
||
padding: 18px 0 10px;
|
||
}
|
||
.detail-head::before {
|
||
position: absolute;
|
||
top: -65px;
|
||
width: 230px;
|
||
height: 230px;
|
||
border-radius: 50%;
|
||
content: '';
|
||
background: radial-gradient(
|
||
circle,
|
||
rgba(101, 251, 210, 0.13),
|
||
transparent 65%
|
||
);
|
||
}
|
||
.detail-head > * {
|
||
position: relative;
|
||
}
|
||
.big-chart {
|
||
background:
|
||
linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
|
||
linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px),
|
||
linear-gradient(145deg, #101720, #070b10);
|
||
background-size:
|
||
100% 40px,
|
||
53px 100%,
|
||
auto;
|
||
border: 1px solid rgba(255, 255, 255, 0.07);
|
||
border-radius: 24px;
|
||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.25);
|
||
}
|
||
.periods {
|
||
margin: 0 7px 7px;
|
||
padding: 4px;
|
||
background: rgba(255, 255, 255, 0.035);
|
||
border-radius: 14px;
|
||
}
|
||
.periods button.active {
|
||
color: #07100e;
|
||
background: var(--vault-mint);
|
||
box-shadow: 0 5px 18px rgba(101, 251, 210, 0.18);
|
||
}
|
||
.vault-detail-scroll {
|
||
padding-bottom: calc(
|
||
var(--sky-safe-area-bottom) + var(--sky-tabbar-height) + var(--sky-space-5)
|
||
);
|
||
}
|
||
.detail-trade-dock {
|
||
position: absolute;
|
||
right: calc(var(--sky-page-gutter) + var(--sky-safe-area-right));
|
||
bottom: calc(var(--sky-safe-area-bottom) + 4px);
|
||
left: calc(var(--sky-page-gutter) + var(--sky-safe-area-left));
|
||
z-index: 11;
|
||
padding: 6px;
|
||
background: #080d13;
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: 23px;
|
||
box-shadow:
|
||
0 18px 46px rgba(0, 0, 0, 0.62),
|
||
inset 0 1px rgba(255, 255, 255, 0.06);
|
||
}
|
||
.detail-trade-action {
|
||
min-width: 0;
|
||
min-height: 56px;
|
||
gap: 9px;
|
||
justify-content: flex-start;
|
||
padding: 0 12px;
|
||
border-radius: 17px;
|
||
}
|
||
.detail-trade-action > span:first-child {
|
||
display: grid;
|
||
place-items: center;
|
||
width: 32px;
|
||
height: 32px;
|
||
flex: 0 0 auto;
|
||
background: rgba(255, 255, 255, 0.14);
|
||
border-radius: 11px;
|
||
}
|
||
.detail-trade-action > span:last-child {
|
||
display: grid;
|
||
min-width: 0;
|
||
gap: 1px;
|
||
text-align: left;
|
||
}
|
||
.detail-trade-action b {
|
||
font-size: 13px;
|
||
}
|
||
.detail-trade-action small {
|
||
overflow: hidden;
|
||
color: rgba(255, 255, 255, 0.68);
|
||
font-size: 10px;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.detail-trade-action--buy {
|
||
background: #179d7d;
|
||
box-shadow: none;
|
||
}
|
||
.detail-trade-action--sell {
|
||
background: #202630;
|
||
border: 1px solid rgba(255, 117, 136, 0.22);
|
||
box-shadow: none;
|
||
}
|
||
.vault-view {
|
||
animation: vault-view-in 220ms cubic-bezier(0.22, 1, 0.36, 1);
|
||
}
|
||
@keyframes vault-view-in {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateX(10px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateX(0);
|
||
}
|
||
}
|
||
.crypto-app :deep(.sky-sheet__panel) {
|
||
max-height: calc(100% - var(--sky-safe-area-top) - 20px);
|
||
overflow-y: auto;
|
||
color: #f8fbfd;
|
||
background:
|
||
radial-gradient(
|
||
circle at 92% 0%,
|
||
rgba(101, 251, 210, 0.1),
|
||
transparent 27%
|
||
),
|
||
#0d1219;
|
||
border: 1px solid rgba(255, 255, 255, 0.09);
|
||
border-bottom: 0;
|
||
border-radius: 28px 28px 0 0;
|
||
box-shadow: 0 -22px 70px rgba(0, 0, 0, 0.52);
|
||
}
|
||
.crypto-app :deep(.sky-sheet__grabber) {
|
||
background: transparent;
|
||
}
|
||
.crypto-app :deep(.sky-sheet__grabber::after) {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
}
|
||
.sheet {
|
||
background: transparent;
|
||
}
|
||
.sheet-header,
|
||
.sheet-heading,
|
||
.sheet-market-summary {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.sheet-header {
|
||
min-height: 58px;
|
||
justify-content: flex-start;
|
||
gap: 12px;
|
||
padding: 0 2px 12px;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
|
||
}
|
||
.sheet-heading {
|
||
width: 100%;
|
||
min-width: 0;
|
||
gap: 10px;
|
||
}
|
||
.sheet-heading > span:last-child {
|
||
display: grid;
|
||
gap: 2px;
|
||
min-width: 0;
|
||
}
|
||
.sheet-heading small {
|
||
overflow: hidden;
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.sheet-heading h2 {
|
||
font-size: 18px;
|
||
letter-spacing: -0.025em;
|
||
}
|
||
.sheet-icon {
|
||
display: grid;
|
||
place-items: center;
|
||
width: 40px;
|
||
height: 40px;
|
||
flex: 0 0 auto;
|
||
color: #07100e;
|
||
font-size: 15px;
|
||
font-weight: 900;
|
||
background: linear-gradient(145deg, var(--vault-mint), var(--vault-blue));
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
border-radius: 14px;
|
||
box-shadow: 0 9px 24px rgba(101, 251, 210, 0.12);
|
||
}
|
||
.sheet-icon--crypto {
|
||
background: transparent;
|
||
border: 0;
|
||
box-shadow: none;
|
||
}
|
||
.sheet-crypto-logo {
|
||
width: 40px;
|
||
height: 40px;
|
||
}
|
||
.sheet-market-summary {
|
||
justify-content: space-between;
|
||
gap: 9px;
|
||
min-height: 72px;
|
||
padding: 13px 15px;
|
||
background:
|
||
radial-gradient(
|
||
circle at 90% 0%,
|
||
rgba(101, 251, 210, 0.14),
|
||
transparent 42%
|
||
),
|
||
linear-gradient(145deg, #17212c, #0b1016);
|
||
border: 1px solid rgba(255, 255, 255, 0.07);
|
||
border-radius: 19px;
|
||
box-shadow: inset 0 1px rgba(255, 255, 255, 0.045);
|
||
}
|
||
.sheet-market-summary span {
|
||
display: grid;
|
||
gap: 3px;
|
||
}
|
||
.sheet-market-summary small {
|
||
color: var(--muted);
|
||
font-size: 10px;
|
||
font-weight: 750;
|
||
letter-spacing: 0.06em;
|
||
text-transform: uppercase;
|
||
}
|
||
.sheet-market-summary b {
|
||
font-size: 22px;
|
||
letter-spacing: -0.035em;
|
||
}
|
||
.sheet-market-summary em {
|
||
padding: 7px 9px;
|
||
font-size: 11px;
|
||
font-style: normal;
|
||
font-weight: 800;
|
||
background: rgba(101, 251, 210, 0.08);
|
||
border: 1px solid currentColor;
|
||
border-radius: var(--sky-radius-pill);
|
||
}
|
||
.trade-side-selector {
|
||
min-height: 50px;
|
||
padding: 3px;
|
||
background: rgba(255, 255, 255, 0.045);
|
||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||
border-radius: 17px;
|
||
}
|
||
.trade-side-selector :deep(.sky-segmented-button) {
|
||
min-height: 42px;
|
||
gap: 7px;
|
||
color: rgba(255, 255, 255, 0.52);
|
||
font-size: 12px;
|
||
font-weight: 800;
|
||
border-radius: 13px;
|
||
}
|
||
:deep(.trade-side-selector.sky-segmented--strong .sky-segmented__highlight) {
|
||
top: 3px;
|
||
bottom: 3px;
|
||
inset-inline-start: 3px;
|
||
background: linear-gradient(
|
||
135deg,
|
||
rgba(101, 251, 210, 0.18),
|
||
rgba(104, 167, 255, 0.12)
|
||
);
|
||
border: 1px solid rgba(101, 251, 210, 0.28);
|
||
border-radius: 13px;
|
||
}
|
||
:deep(
|
||
.trade-side-selector.sky-segmented--strong .sky-segmented-button--active
|
||
) {
|
||
color: #fff;
|
||
}
|
||
.trade-entry-card {
|
||
display: grid;
|
||
gap: 10px;
|
||
padding: 12px;
|
||
background: linear-gradient(145deg, #131b24, #0a0f15);
|
||
border: 1px solid rgba(255, 255, 255, 0.07);
|
||
border-radius: 19px;
|
||
}
|
||
.trade-entry-meta {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 8px;
|
||
}
|
||
.trade-entry-meta > span {
|
||
display: grid;
|
||
gap: 2px;
|
||
min-width: 0;
|
||
padding: 9px 10px;
|
||
background: rgba(255, 255, 255, 0.035);
|
||
border: 1px solid rgba(255, 255, 255, 0.04);
|
||
border-radius: 12px;
|
||
}
|
||
.trade-entry-meta small {
|
||
color: var(--muted);
|
||
font-size: 10px;
|
||
}
|
||
.trade-entry-meta b {
|
||
overflow: hidden;
|
||
font-size: 11px;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.trade-protection {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: flex-start;
|
||
padding: 11px 12px;
|
||
color: var(--vault-mint);
|
||
background: rgba(101, 251, 210, 0.055);
|
||
border: 1px solid rgba(101, 251, 210, 0.12);
|
||
border-radius: 15px;
|
||
}
|
||
.trade-protection svg {
|
||
flex: 0 0 auto;
|
||
}
|
||
.trade-protection span {
|
||
display: grid;
|
||
gap: 2px;
|
||
}
|
||
.trade-protection b {
|
||
color: #f8fbfd;
|
||
font-size: 11px;
|
||
}
|
||
.trade-protection small {
|
||
color: var(--muted);
|
||
font-size: 10px;
|
||
line-height: 1.4;
|
||
}
|
||
.trade-submit {
|
||
min-height: 52px;
|
||
border-radius: 16px;
|
||
font-size: 13px;
|
||
font-weight: 850;
|
||
}
|
||
.trade-submit--buy {
|
||
background: #179d7d;
|
||
box-shadow: none;
|
||
}
|
||
.trade-submit--sell {
|
||
background: #c4475c;
|
||
box-shadow: none;
|
||
}
|
||
.transfer-intro,
|
||
.transfer-recipient,
|
||
.transfer-balance {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.transfer-intro {
|
||
gap: 11px;
|
||
padding: 13px;
|
||
background: linear-gradient(
|
||
145deg,
|
||
rgba(101, 251, 210, 0.09),
|
||
rgba(104, 167, 255, 0.055)
|
||
);
|
||
border: 1px solid rgba(101, 251, 210, 0.13);
|
||
border-radius: 17px;
|
||
}
|
||
.transfer-intro > span,
|
||
.transfer-recipient > span {
|
||
display: grid;
|
||
place-items: center;
|
||
width: 38px;
|
||
height: 38px;
|
||
flex: 0 0 auto;
|
||
color: var(--vault-mint);
|
||
background: rgba(101, 251, 210, 0.09);
|
||
border-radius: 12px;
|
||
}
|
||
.transfer-intro > div,
|
||
.transfer-recipient > div {
|
||
display: grid;
|
||
min-width: 0;
|
||
gap: 2px;
|
||
}
|
||
.transfer-intro b,
|
||
.transfer-recipient b {
|
||
font-size: 12px;
|
||
}
|
||
.transfer-intro small,
|
||
.transfer-recipient small,
|
||
.transfer-recipient em {
|
||
color: var(--muted);
|
||
font-size: 10px;
|
||
font-style: normal;
|
||
line-height: 1.35;
|
||
}
|
||
.transfer-key-entry {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr);
|
||
gap: 8px;
|
||
}
|
||
.transfer-resolve {
|
||
width: 100%;
|
||
min-height: 50px;
|
||
font-size: 12px;
|
||
font-weight: 850;
|
||
border-radius: 15px;
|
||
}
|
||
.transfer-recipient {
|
||
gap: 10px;
|
||
padding: 11px;
|
||
background: rgba(101, 251, 210, 0.055);
|
||
border: 1px solid rgba(101, 251, 210, 0.15);
|
||
border-radius: 15px;
|
||
}
|
||
.transfer-recipient em {
|
||
overflow: hidden;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.transfer-assets {
|
||
display: grid;
|
||
gap: 7px;
|
||
}
|
||
.transfer-assets > small {
|
||
color: var(--muted);
|
||
font-size: 10px;
|
||
font-weight: 800;
|
||
letter-spacing: 0.06em;
|
||
text-transform: uppercase;
|
||
}
|
||
.transfer-assets > div {
|
||
display: flex;
|
||
gap: 7px;
|
||
overflow-x: auto;
|
||
scrollbar-width: none;
|
||
}
|
||
.transfer-assets button {
|
||
display: flex;
|
||
min-width: 105px;
|
||
gap: 7px;
|
||
align-items: center;
|
||
padding: 8px;
|
||
color: rgba(255, 255, 255, 0.58);
|
||
text-align: left;
|
||
background: rgba(255, 255, 255, 0.035);
|
||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||
border-radius: 14px;
|
||
}
|
||
.transfer-assets button.active {
|
||
color: #fff;
|
||
background: rgba(101, 251, 210, 0.09);
|
||
border-color: rgba(101, 251, 210, 0.27);
|
||
}
|
||
.transfer-assets button :deep(.crypto-logo) {
|
||
width: 31px;
|
||
height: 31px;
|
||
}
|
||
.transfer-assets button span {
|
||
display: grid;
|
||
gap: 1px;
|
||
}
|
||
.transfer-assets button b {
|
||
font-size: 11px;
|
||
}
|
||
.transfer-assets button small {
|
||
font-size: 9px;
|
||
}
|
||
.transfer-balance {
|
||
justify-content: space-between;
|
||
margin-top: -7px;
|
||
padding: 0 3px;
|
||
color: var(--muted);
|
||
font-size: 10px;
|
||
}
|
||
.transfer-balance b {
|
||
color: #fff;
|
||
}
|
||
.transfer-submit {
|
||
min-height: 52px;
|
||
gap: 8px;
|
||
color: #06110e;
|
||
font-weight: 900;
|
||
background: var(--vault-mint);
|
||
border-radius: 16px;
|
||
box-shadow: none;
|
||
}
|
||
.sheet-copy {
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
}
|
||
.sheet--settlement {
|
||
gap: 14px;
|
||
}
|
||
.settlement-form {
|
||
display: grid;
|
||
width: 100%;
|
||
gap: 13px;
|
||
}
|
||
.settlement-intro {
|
||
display: flex;
|
||
gap: 11px;
|
||
align-items: flex-start;
|
||
padding: 12px 13px;
|
||
background:
|
||
radial-gradient(
|
||
circle at 0% 0%,
|
||
rgba(101, 251, 210, 0.08),
|
||
transparent 52%
|
||
),
|
||
rgba(255, 255, 255, 0.025);
|
||
border: 1px solid rgba(255, 255, 255, 0.065);
|
||
border-radius: 16px;
|
||
box-shadow: inset 0 1px rgba(255, 255, 255, 0.035);
|
||
}
|
||
.settlement-intro > span {
|
||
display: grid;
|
||
width: 32px;
|
||
height: 32px;
|
||
flex: 0 0 auto;
|
||
place-items: center;
|
||
color: var(--vault-mint);
|
||
background: rgba(101, 251, 210, 0.08);
|
||
border: 1px solid rgba(101, 251, 210, 0.14);
|
||
border-radius: 11px;
|
||
}
|
||
.settlement-copy {
|
||
padding-top: 1px;
|
||
color: rgba(238, 245, 248, 0.66);
|
||
font-size: 11px;
|
||
line-height: 1.55;
|
||
}
|
||
.settlement-fields {
|
||
display: grid;
|
||
gap: 10px;
|
||
}
|
||
.sheet-field {
|
||
min-height: 64px;
|
||
margin: 0;
|
||
padding: 0 15px;
|
||
color: #f8fbfd;
|
||
background:
|
||
linear-gradient(145deg, rgba(255, 255, 255, 0.045), transparent), #090f16;
|
||
border-radius: 17px;
|
||
box-shadow: inset 0 1px rgba(255, 255, 255, 0.04);
|
||
}
|
||
.sheet-field:focus-within {
|
||
background:
|
||
linear-gradient(145deg, rgba(101, 251, 210, 0.07), transparent), #090f16;
|
||
box-shadow:
|
||
0 0 0 3px rgba(101, 251, 210, 0.065),
|
||
inset 0 1px rgba(255, 255, 255, 0.05);
|
||
}
|
||
.sheet-field :deep(.sky-field__media) {
|
||
display: grid;
|
||
width: 36px;
|
||
height: 36px;
|
||
justify-content: center;
|
||
margin-right: 12px;
|
||
padding: 0;
|
||
place-items: center;
|
||
color: var(--vault-mint);
|
||
background: rgba(101, 251, 210, 0.07);
|
||
border: 1px solid rgba(101, 251, 210, 0.13);
|
||
border-radius: 12px;
|
||
}
|
||
.sheet-field :deep(.sky-field__inner) {
|
||
padding: 12px 0 10px;
|
||
}
|
||
.sheet-field :deep(.sky-field__label) {
|
||
margin-top: 0;
|
||
color: rgba(255, 255, 255, 0.55);
|
||
font-size: 10px;
|
||
font-weight: 750;
|
||
}
|
||
.sheet-field :deep(.sky-field__label-text) {
|
||
top: 0;
|
||
margin: 0;
|
||
padding: 0;
|
||
background: transparent;
|
||
}
|
||
.sheet-field :deep(.sky-field__control) {
|
||
margin: -2px 0 0;
|
||
}
|
||
.sheet-field :deep(.sky-field__input) {
|
||
height: 31px;
|
||
min-height: 31px;
|
||
color: #fff;
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
line-height: 21px;
|
||
}
|
||
.sheet-field :deep(.sky-field__input::placeholder) {
|
||
color: rgba(255, 255, 255, 0.24);
|
||
}
|
||
.sheet-field :deep(.sky-field__border) {
|
||
inset: 0;
|
||
border-color: rgba(255, 255, 255, 0.12);
|
||
border-radius: 17px;
|
||
}
|
||
.sheet-field:focus-within :deep(.sky-field__border) {
|
||
border-color: rgba(101, 251, 210, 0.58);
|
||
}
|
||
.sheet-field.sky-field--error :deep(.sky-field__border) {
|
||
border-color: rgba(255, 117, 136, 0.6);
|
||
}
|
||
.settlement-error {
|
||
margin: -3px 2px 0;
|
||
}
|
||
.settlement-submit {
|
||
min-height: 54px;
|
||
gap: 8px;
|
||
color: #06110e;
|
||
font-size: 13px;
|
||
font-weight: 900;
|
||
background: var(--vault-mint);
|
||
border: 1px solid rgba(101, 251, 210, 0.48);
|
||
border-radius: 17px;
|
||
box-shadow: none;
|
||
}
|
||
.settlement-submit:active {
|
||
box-shadow: none;
|
||
transform: translateY(1px);
|
||
}
|
||
@media (hover: hover) {
|
||
.settlement-submit:hover {
|
||
filter: brightness(1.06);
|
||
}
|
||
}
|
||
.sheet--profile {
|
||
gap: 14px;
|
||
}
|
||
.sheet--profile .sheet-copy {
|
||
padding: 0 3px;
|
||
font-size: 11px;
|
||
line-height: 1.55;
|
||
}
|
||
.profile-edit-sheet {
|
||
display: grid;
|
||
width: 100%;
|
||
gap: 13px;
|
||
}
|
||
.profile-edit-fields {
|
||
display: grid;
|
||
gap: 9px;
|
||
}
|
||
.profile-edit-note {
|
||
display: flex;
|
||
gap: 9px;
|
||
align-items: flex-start;
|
||
padding: 11px 12px;
|
||
color: rgba(255, 255, 255, 0.62);
|
||
font-size: 11px;
|
||
line-height: 1.4;
|
||
background: rgba(101, 251, 210, 0.06);
|
||
border: 1px solid rgba(101, 251, 210, 0.12);
|
||
border-radius: 14px;
|
||
}
|
||
.profile-edit-note svg {
|
||
flex: 0 0 auto;
|
||
color: var(--vault-mint);
|
||
}
|
||
.profile-save-button {
|
||
min-height: 52px;
|
||
gap: 8px;
|
||
color: #06110e;
|
||
font-size: 13px;
|
||
font-weight: 900;
|
||
background: var(--vault-mint);
|
||
border: 1px solid rgba(101, 251, 210, 0.48);
|
||
border-radius: 16px;
|
||
box-shadow: none;
|
||
}
|
||
.profile-save-button:active {
|
||
box-shadow: none;
|
||
transform: translateY(1px);
|
||
}
|
||
.quote {
|
||
display: grid;
|
||
gap: 0;
|
||
padding: 13px;
|
||
background: linear-gradient(145deg, #151d27, #0c1117);
|
||
border: 1px solid rgba(255, 255, 255, 0.07);
|
||
border-radius: 18px;
|
||
}
|
||
.quote-heading {
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
padding: 0 0 11px !important;
|
||
color: #f8fbfd !important;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
|
||
}
|
||
.quote-heading > span {
|
||
display: grid;
|
||
gap: 2px;
|
||
}
|
||
.quote-heading small {
|
||
color: var(--vault-mint);
|
||
font-size: 10px;
|
||
font-weight: 800;
|
||
letter-spacing: 0.07em;
|
||
text-transform: uppercase;
|
||
}
|
||
.quote-heading > svg {
|
||
color: var(--vault-mint);
|
||
}
|
||
.quote > div:not(.quote-heading) {
|
||
padding: 6px 0;
|
||
}
|
||
.quote > div:nth-last-of-type(1) {
|
||
margin-top: 4px;
|
||
padding-top: 10px;
|
||
color: #f8fbfd;
|
||
border-top: 1px solid rgba(255, 255, 255, 0.07);
|
||
}
|
||
.quote-expiry {
|
||
margin-top: 8px;
|
||
padding: 8px 9px;
|
||
line-height: 1.35;
|
||
background: rgba(255, 255, 255, 0.035);
|
||
border-radius: 10px;
|
||
}
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.portfolio-shell::after {
|
||
display: none;
|
||
}
|
||
.vault-view {
|
||
animation: none;
|
||
}
|
||
}
|
||
.phone-app--performance .portfolio-shell,
|
||
.phone-app--performance .featured-market,
|
||
.phone-app--performance .profile-card,
|
||
.phone-app--performance .activity-overview,
|
||
.phone-app--performance .activity-status {
|
||
box-shadow: none;
|
||
}
|
||
</style>
|