Files
sky_phone/frontend/src/views/apps/SkyRideApp.vue
T
DerEchteAlec 97db163cd0 FIX - harden inventory contracts and resolve phone interaction issues (#20)
* 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.
2026-08-21 17:19:27 +02:00

2991 lines
82 KiB
Vue

<script setup lang="ts">
import {
Bell,
BriefcaseBusiness,
Camera,
CarFront,
Check,
CheckCircle2,
ChevronRight,
CircleUserRound,
CircleDollarSign,
Clock3,
Crosshair,
History,
House,
Images,
MapPin,
MessageCircle,
Navigation,
Phone,
Pencil,
Power,
Route,
ShieldCheck,
Sparkles,
Star,
Trash2,
UserRound,
X,
} from 'lucide-vue-next'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useCallsStore } from '@/stores/calls'
import { useMessageMediaStore } from '@/stores/messageMedia'
import { usePhoneStore } from '@/stores/phone'
import { useSkyRideStore } from '@/stores/skyride'
import type { PhoneMedia } from '@/types/media'
import type {
SkyRideChangedMessage,
SkyRideCustomFareInput,
SkyRideDistanceUnit,
SkyRideFareMode,
SkyRideLocation,
SkyRideMode,
SkyRideQuoteOption,
SkyRideRide,
SkyRideRideStatus,
} from '@/types/skyride'
import { isTrustedRootMessageSource } from '@/utils/windowMessages'
import {
SkyAppPage as kPage,
SkyBadge as kBadge,
SkyBlock as kBlock,
SkyBlockHeader as kBlockHeader,
SkyBlockTitle as kBlockTitle,
SkyButton,
SkyButton as kButton,
SkyCard as kCard,
SkyChip as kChip,
SkyDialog as kDialog,
SkyDialogButton as kDialogButton,
SkyField,
SkyField as kListInput,
SkyLink as kLink,
SkyList as kList,
SkyListItem as kListItem,
SkyNavbar as kNavbar,
SkySegmented as kSegmented,
SkySegmentedButton as kSegmentedButton,
SkySettingsGroup,
SkySettingsRow,
SkySheet,
SkySheet as kSheet,
SkySpinner as kPreloader,
SkyTabBar as kTabbar,
SkyTabButton as kTabbarLink,
SkyNotification as kNotification,
SkyToggle as kToggle,
} from '@/ui'
type SkyRideTab = 'home' | 'rides' | 'activity' | 'messages' | 'profile'
type LocationTarget = 'pickup' | 'destination'
type ProfileMediaContext = {
avatarMediaId: number
name: string
selectedAvatar: PhoneMedia | null
}
const phone = usePhoneStore()
const skyride = useSkyRideStore()
const calls = useCallsStore()
const messageMedia = useMessageMediaStore()
const router = useRouter()
const activeTab = ref<SkyRideTab>('home')
const mode = ref<SkyRideMode>('rider')
const pickup = ref<SkyRideLocation | null>(null)
const destination = ref<SkyRideLocation | null>(null)
const locationTarget = ref<LocationTarget | null>(null)
const selectedQuoteId = ref<string | null>(null)
const fareMode = ref<SkyRideFareMode>('calculated')
const customFareInput = ref('')
const cancelDialogOpened = ref(false)
const rating = ref(0)
const tip = ref(0)
const ratingComment = ref('')
const toastText = ref('')
const profileEditorOpened = ref(false)
const profileName = ref('')
const profileAvatarMediaId = ref(0)
const selectedProfileAvatar = ref<PhoneMedia | null>(null)
let toastTimer: number | undefined
const tabs = [
{ icon: House, id: 'home' as const },
{ icon: Route, id: 'rides' as const },
{ icon: Bell, id: 'activity' as const },
{ icon: MessageCircle, id: 'messages' as const },
{ icon: CircleUserRound, id: 'profile' as const },
]
const selectedQuote = computed(() =>
skyride.quote?.options.find(
(option) => option.quoteId === selectedQuoteId.value,
),
)
const canRequestSelectedQuote = computed(() => {
const option = selectedQuote.value
if (!option) return false
if (fareMode.value === 'calculated') {
return option.fareMode === 'calculated'
}
return (
option.fareMode === 'custom' &&
Number(customFareInput.value) === option.price
)
})
const activeContact = computed(() =>
mode.value === 'driver'
? skyride.activeRide?.passenger
: skyride.activeRide?.driver,
)
const canCancelRide = computed(() =>
[
'searching',
'accepted',
'driver_arriving',
'arrived',
'in_progress',
].includes(skyride.activeRide?.status ?? ''),
)
const driverAction = computed<'arrive' | 'start' | 'complete' | null>(() => {
const status = skyride.activeRide?.status
if (status === 'accepted' || status === 'driver_arriving') return 'arrive'
if (status === 'arrived') return 'start'
if (status === 'in_progress') return 'complete'
return null
})
const ratingRide = computed(() => skyride.pendingRating)
const profileAvatarUrl = computed(
() =>
selectedProfileAvatar.value?.url ??
(profileAvatarMediaId.value > 0 ? skyride.profile?.avatarUrl : null),
)
const canSaveProfile = computed(() => {
const length = Array.from(profileName.value.trim()).length
return length >= 2 && length <= 50 && !skyride.isActionPending
})
function showToast(message: string): void {
if (toastTimer) window.clearTimeout(toastTimer)
toastText.value = message
toastTimer = window.setTimeout(() => {
toastText.value = ''
toastTimer = undefined
}, 2200)
}
function errorText(error?: string): string {
const key = error ?? 'request_failed'
const translated = phone.t(`Apps.skyride.errors.${key}`)
return translated === `Apps.skyride.errors.${key}`
? phone.t('Apps.skyride.errors.request_failed')
: translated
}
function locationLabel(location: SkyRideLocation | null): string {
if (!location) return phone.t('Apps.skyride.notSelected')
if (!location.id) return location.label
const key = `Apps.skyride.quickLocations.${location.id}`
const translated = phone.t(key)
return translated === key ? phone.t('Apps.skyride.savedPlace') : translated
}
function paymentMethodLabel(method: string): string {
const key = `Apps.skyride.paymentMethods.${method}`
const translated = phone.t(key)
return translated === key
? phone.t('Apps.skyride.paymentMethods.default')
: translated
}
function formatMoney(amount: number, currency: string): string {
if (/^[A-Z]{3}$/.test(currency)) {
return new Intl.NumberFormat(phone.lang, {
currency,
maximumFractionDigits: 0,
style: 'currency',
}).format(amount)
}
return `${currency}${new Intl.NumberFormat(phone.lang, {
maximumFractionDigits: 0,
}).format(amount)}`
}
function formatQuoteDistance(
distance: number,
unit: SkyRideDistanceUnit,
): string {
return phone.t(
unit === 'mile'
? 'Apps.skyride.distanceMiles'
: 'Apps.skyride.distanceKilometers',
{
distance: distance.toLocaleString(phone.lang, {
maximumFractionDigits: 1,
}),
},
)
}
function formatRideDistance(distanceMeters: number): string {
return formatQuoteDistance(Math.max(0, distanceMeters) / 1000, 'kilometer')
}
function formatDistanceRate(option: SkyRideQuoteOption): string {
const unitKey =
skyride.quote?.distanceUnit === 'mile' ? 'perMile' : 'perKilometer'
return phone.t(`Apps.skyride.${unitKey}`, {
price: formatMoney(option.pricePerDistanceUnit, option.currency),
})
}
function formatDuration(durationSeconds: number): string {
return phone.t('Apps.skyride.durationMinutes', {
minutes: Math.max(1, Math.round(durationSeconds / 60)).toLocaleString(
phone.lang,
),
})
}
function formatDate(timestamp: number): string {
return new Intl.DateTimeFormat(phone.lang, {
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
month: 'short',
}).format(new Date(timestamp * 1000))
}
function statusLabel(status: SkyRideRideStatus): string {
return phone.t(`Apps.skyride.status.${status}`)
}
function vehicleLabel(ride: SkyRideRide): string {
const model = ride.driver?.vehicle.model.trim() ?? ''
return model && !/^-?\d+$/.test(model)
? model
: phone.t('Apps.skyride.vehicle')
}
function updateRatingComment(event: Event): void {
ratingComment.value = (event.target as HTMLInputElement).value
}
function openLocationPicker(target: LocationTarget): void {
if (skyride.activeRide) return
locationTarget.value = target
}
function chooseLocation(location: SkyRideLocation): void {
const target = locationTarget.value
if (!target) return
if (target === 'pickup') pickup.value = { ...location }
else destination.value = { ...location }
locationTarget.value = null
skyride.clearQuote()
}
function chooseQuickDestination(location: SkyRideLocation): void {
destination.value = { ...location }
skyride.clearQuote()
}
async function useCurrentLocation(): Promise<void> {
const target = locationTarget.value
if (!target) return
const response = await skyride.getPlayerCoordinates()
if (!response.success || !response.data) {
showToast(errorText(response.error))
return
}
chooseLocation({
coords: response.data.coords,
label: phone.t('Apps.skyride.currentLocation'),
})
}
async function createQuote(customFare?: SkyRideCustomFareInput): Promise<void> {
if (!pickup.value || !destination.value) return
const preferredService =
customFare?.serviceClass ?? selectedQuote.value?.serviceClass
const response = await skyride.createQuote(
pickup.value,
destination.value,
customFare,
)
if (!response.success || !response.data) {
showToast(errorText(response.error))
return
}
const option =
response.data.options.find(
(candidate) =>
candidate.available && candidate.serviceClass === preferredService,
) ?? response.data.options.find((candidate) => candidate.available)
selectedQuoteId.value = option?.quoteId ?? null
fareMode.value = option?.fareMode ?? 'calculated'
customFareInput.value = option ? String(option.price) : ''
}
async function requestRide(
option: SkyRideQuoteOption | undefined,
): Promise<void> {
if (!option) return
const response = await skyride.requestRide(option)
if (!response.success) showToast(errorText(response.error))
}
function selectQuoteOption(option: SkyRideQuoteOption): void {
if (!option.available) return
selectedQuoteId.value = option.quoteId
fareMode.value = option.fareMode
customFareInput.value = String(option.price)
}
function selectFareMode(nextMode: SkyRideFareMode): void {
fareMode.value = nextMode
const option = selectedQuote.value
if (!option) return
customFareInput.value = String(
nextMode === 'custom' && option.fareMode === 'custom'
? option.price
: option.calculatedPrice,
)
}
function updateCustomFareInput(event: Event): void {
customFareInput.value = (event.target as HTMLInputElement).value
}
async function applyCustomFare(): Promise<void> {
const option = selectedQuote.value
const price = Number(customFareInput.value)
if (
!option ||
!Number.isInteger(price) ||
price < option.minimumCustomPrice ||
price > option.maximumCustomPrice
) {
showToast(errorText('invalid_custom_fare'))
return
}
await createQuote({ price, serviceClass: option.serviceClass })
}
async function applyCalculatedFare(): Promise<void> {
if (selectedQuote.value?.fareMode === 'calculated') {
selectFareMode('calculated')
return
}
fareMode.value = 'calculated'
await createQuote()
}
async function toggleDriverStatus(): Promise<void> {
const response = await skyride.setDriverStatus(!skyride.driverOnline)
if (!response.success) showToast(errorText(response.error))
}
async function acceptRide(ride: SkyRideRide): Promise<void> {
const response = await skyride.performRideAction('accept', ride.id)
if (!response.success) showToast(errorText(response.error))
}
async function performDriverAction(): Promise<void> {
const ride = skyride.activeRide
const action = driverAction.value
if (!ride || !action) return
const response = await skyride.performRideAction(action, ride.id)
if (!response.success) showToast(errorText(response.error))
}
async function confirmCancel(): Promise<void> {
const ride = skyride.activeRide
if (!ride) return
const response = await skyride.cancelRide(ride.id, 'changed_mind')
if (!response.success) showToast(errorText(response.error))
cancelDialogOpened.value = false
}
async function setRideWaypoint(): Promise<void> {
const ride = skyride.activeRide
if (!ride) return
const location =
mode.value === 'driver' && ride.status !== 'in_progress'
? ride.pickup
: ride.destination
const response = await skyride.setWaypoint(location.coords)
showToast(
response.success
? phone.t('Apps.skyride.waypointSet')
: errorText(response.error),
)
}
async function callActiveContact(): Promise<void> {
const number = activeContact.value?.phoneNumber
if (!number) return
const response = await calls.dial(number)
if (!response.success) showToast(errorText(response.error))
}
function openMessages(): void {
void router.push('/apps/messages')
}
function syncProfileEditor(): void {
if (!skyride.profile) return
profileName.value = skyride.profile.name
profileAvatarMediaId.value = skyride.profile.avatarMediaId ?? 0
selectedProfileAvatar.value = null
}
function openProfileEditor(): void {
syncProfileEditor()
profileEditorOpened.value = true
}
function closeProfileEditor(): void {
profileEditorOpened.value = false
syncProfileEditor()
}
function openProfileMedia(app: 'camera' | 'photos'): void {
messageMedia.begin(
'skyride:profile-avatar',
'photo',
'/apps/skyride?profileEdit=1',
1,
{
avatarMediaId: profileAvatarMediaId.value,
name: profileName.value,
selectedAvatar: selectedProfileAvatar.value,
} satisfies ProfileMediaContext,
)
profileEditorOpened.value = false
void router.push({
path: `/apps/${app}`,
query: { mediaAttachment: 'photo' },
})
}
function removeProfileAvatar(): void {
selectedProfileAvatar.value = null
profileAvatarMediaId.value = 0
}
async function saveProfile(): Promise<void> {
if (!canSaveProfile.value) {
showToast(phone.t('Apps.skyride.errors.invalid_profile_name'))
return
}
const response = await skyride.updateProfile({
avatarMediaId:
selectedProfileAvatar.value?.id ?? profileAvatarMediaId.value,
name: profileName.value.trim(),
})
if (!response.success) {
showToast(errorText(response.error))
return
}
profileEditorOpened.value = false
syncProfileEditor()
showToast(phone.t('Apps.skyride.profileSaved'))
}
async function submitRating(): Promise<void> {
const ride = ratingRide.value
if (!ride || rating.value < 1) return
const response = await skyride.rateRide(
ride.id,
rating.value,
tip.value,
ratingComment.value.trim(),
)
if (!response.success) {
showToast(errorText(response.error))
return
}
rating.value = 0
tip.value = 0
ratingComment.value = ''
showToast(phone.t('Apps.skyride.ratingSaved'))
}
function dismissRating(): void {
skyride.pendingRating = null
rating.value = 0
tip.value = 0
ratingComment.value = ''
}
function selectTab(tab: SkyRideTab): void {
activeTab.value = tab
if (tab === 'rides' || tab === 'activity') void skyride.loadHistory()
}
function handleSkyRideMessage(event: MessageEvent<unknown>): void {
if (!isTrustedRootMessageSource(event.source, window)) return
if (typeof event.data !== 'object' || event.data === null) return
const message = event.data as Partial<SkyRideChangedMessage>
if (message.type !== 'skyride:changed' || !message.data) return
skyride.applyUpdate(message.data)
}
watch(
() => skyride.driverEligible,
(eligible) => {
if (!eligible) mode.value = 'rider'
},
)
watch(
() => skyride.activeRide,
(ride) => {
if (!ride || !skyride.profile) return
mode.value = ride.passenger?.id === skyride.profile.id ? 'rider' : 'driver'
},
)
onMounted(async () => {
window.addEventListener('message', handleSkyRideMessage)
const profileSelection = messageMedia.consumeMany<ProfileMediaContext>(
'skyride:profile-avatar',
)
await skyride.bootstrap()
syncProfileEditor()
if (profileSelection?.context) {
profileName.value = profileSelection.context.name
profileAvatarMediaId.value = profileSelection.context.avatarMediaId
selectedProfileAvatar.value =
profileSelection.media[0] ?? profileSelection.context.selectedAvatar
if (profileSelection.media[0]) {
profileAvatarMediaId.value = profileSelection.media[0].id
}
activeTab.value = 'profile'
profileEditorOpened.value = true
}
const response = await skyride.getPlayerCoordinates()
if (response.success && response.data) {
if (!pickup.value) {
pickup.value = {
coords: response.data.coords,
label: phone.t('Apps.skyride.currentLocation'),
}
}
}
})
onBeforeUnmount(() => {
window.removeEventListener('message', handleSkyRideMessage)
if (toastTimer) window.clearTimeout(toastTimer)
})
</script>
<template>
<k-page
class="skyride-app pb-safe-24"
:class="{ 'skyride-app--dark': phone.isDarkMode }"
:label="phone.t('Apps.skyride.name')"
:dark="phone.isDarkMode"
accent="#c49a00"
accent-soft="rgba(245, 197, 24, 0.16)"
>
<div class="skyride-ambient" aria-hidden="true"></div>
<k-navbar
class="skyride-navbar"
:title="phone.t('Apps.skyride.name')"
:subtitle="phone.t(`Apps.skyride.mode.${mode}`)"
/>
<k-block
v-if="skyride.isLoading && !skyride.profile"
class="skyride-loading"
>
<k-preloader />
<span>{{ phone.t('Apps.skyride.loading') }}</span>
</k-block>
<k-card
v-else-if="!skyride.profile"
:content-wrap="false"
class="skyride-unavailable"
>
<CarFront :size="36" aria-hidden="true" />
<strong>{{ phone.t('Apps.skyride.unavailable') }}</strong>
<p>{{ errorText(skyride.error) }}</p>
<k-button rounded @click="skyride.bootstrap()">
{{ phone.t('Apps.skyride.tryAgain') }}
</k-button>
</k-card>
<template v-else>
<div class="skyride-mode">
<k-segmented v-if="skyride.driverEligible">
<k-segmented-button
:active="mode === 'rider'"
:disabled="Boolean(skyride.activeRide)"
@click="mode = 'rider'"
>
{{ phone.t('Apps.skyride.mode.rider') }}
</k-segmented-button>
<k-segmented-button
:active="mode === 'driver'"
:disabled="Boolean(skyride.activeRide)"
@click="mode = 'driver'"
>
{{ phone.t('Apps.skyride.mode.driver') }}
</k-segmented-button>
</k-segmented>
</div>
<div class="skyride-scroll">
<template v-if="activeTab === 'home'">
<section v-if="mode === 'rider'" class="skyride-home-panel">
<template v-if="!skyride.activeRide">
<k-block-header inset component="header" class="skyride-heading">
<div>
<span>{{ phone.t('Apps.skyride.riderEyebrow') }}</span>
<h1>{{ phone.t('Apps.skyride.whereTo') }}</h1>
</div>
<span class="skyride-heading__icon" aria-hidden="true">
<Navigation :size="20" aria-hidden="true" />
</span>
</k-block-header>
<k-list inset strong class="skyride-location-list">
<k-list-item
link
:title="phone.t('Apps.skyride.pickup')"
:subtitle="locationLabel(pickup)"
@click="openLocationPicker('pickup')"
>
<template #media><span class="skyride-dot"></span></template>
</k-list-item>
<k-list-item
link
:title="phone.t('Apps.skyride.destination')"
:subtitle="locationLabel(destination)"
@click="openLocationPicker('destination')"
>
<template #media><MapPin :size="19" /></template>
</k-list-item>
</k-list>
<template v-if="!skyride.quote">
<k-block-title class="skyride-custom-block-title">{{
phone.t('Apps.skyride.quickDestinations')
}}</k-block-title>
<k-block class="skyride-quick-grid">
<k-button
v-for="location in skyride.quickLocations.slice(0, 4)"
:key="location.id ?? location.label"
large
rounded
variant="secondary"
class="skyride-quick-card"
@click="chooseQuickDestination(location)"
>
<component
:is="location.id === 'work' ? BriefcaseBusiness : MapPin"
:size="18"
aria-hidden="true"
/>
<span>{{ locationLabel(location) }}</span>
</k-button>
</k-block>
<k-button
large
rounded
class="skyride-primary"
:disabled="!pickup || !destination || skyride.isActionPending"
@click="createQuote()"
>
<k-preloader v-if="skyride.isActionPending" />
<template v-else>
{{ phone.t('Apps.skyride.viewRides') }}
<ChevronRight :size="18" aria-hidden="true" />
</template>
</k-button>
</template>
<template v-else>
<div class="skyride-quote-summary">
<k-chip class="skyride-quote-chip">{{
formatQuoteDistance(
skyride.quote.distance,
skyride.quote.distanceUnit,
)
}}</k-chip>
<k-chip class="skyride-quote-chip">{{
formatDuration(skyride.quote.durationSeconds)
}}</k-chip>
<k-link @click="skyride.clearQuote()">
{{ phone.t('Apps.skyride.change') }}
</k-link>
</div>
<k-list inset strong class="skyride-service-list">
<k-list-item
v-for="option in skyride.quote.options"
:key="option.quoteId"
:link="option.available"
:aria-disabled="!option.available"
:class="{
'is-selected': selectedQuoteId === option.quoteId,
'is-unavailable': !option.available,
}"
:title="
phone.t(
`Apps.skyride.services.${option.serviceClass}.name`,
)
"
:subtitle="
phone.t('Apps.skyride.serviceMeta', {
eta: option.etaMinutes.toLocaleString(phone.lang),
seats: option.seats.toLocaleString(phone.lang),
})
"
@click="selectQuoteOption(option)"
>
<template #media>
<span class="skyride-service-icon">
<CarFront :size="22" aria-hidden="true" />
</span>
</template>
<template #after>
<strong>{{
formatMoney(option.price, option.currency)
}}</strong>
<Check
v-if="selectedQuoteId === option.quoteId"
:size="16"
/>
</template>
</k-list-item>
</k-list>
<k-card
v-if="selectedQuote"
:content-wrap="false"
class="skyride-fare-card"
>
<div class="skyride-fare-card__heading">
<span class="skyride-fare-card__icon">
<CircleDollarSign :size="19" aria-hidden="true" />
</span>
<div>
<small>{{ phone.t('Apps.skyride.fare') }}</small>
<strong>{{
formatMoney(selectedQuote.price, selectedQuote.currency)
}}</strong>
</div>
<span>{{ formatDistanceRate(selectedQuote) }}</span>
</div>
<k-segmented>
<k-segmented-button
:active="fareMode === 'calculated'"
@click="applyCalculatedFare"
>
{{ phone.t('Apps.skyride.calculatedFare') }}
</k-segmented-button>
<k-segmented-button
:active="fareMode === 'custom'"
@click="selectFareMode('custom')"
>
{{ phone.t('Apps.skyride.customFare') }}
</k-segmented-button>
</k-segmented>
<div
v-if="fareMode === 'calculated'"
class="skyride-fare-breakdown"
>
<span>{{
phone.t('Apps.skyride.calculatedFareBody')
}}</span>
<strong>{{
formatMoney(
selectedQuote.calculatedPrice,
selectedQuote.currency,
)
}}</strong>
</div>
<div v-else class="skyride-custom-fare">
<k-list inset strong>
<k-list-input
id="skyride-custom-fare"
outline
type="number"
inputmode="numeric"
step="1"
:label="phone.t('Apps.skyride.customFare')"
:min="selectedQuote.minimumCustomPrice"
:max="selectedQuote.maximumCustomPrice"
:value="customFareInput"
:help="
phone.t('Apps.skyride.customFareRange', {
maximum: formatMoney(
selectedQuote.maximumCustomPrice,
selectedQuote.currency,
),
minimum: formatMoney(
selectedQuote.minimumCustomPrice,
selectedQuote.currency,
),
})
"
@input="updateCustomFareInput"
/>
</k-list>
<k-button
small
rounded
:disabled="skyride.isActionPending"
@click="applyCustomFare"
>
{{ phone.t('Apps.skyride.applyCustomFare') }}
</k-button>
</div>
</k-card>
<p class="skyride-player-driver-notice">
{{ phone.t('Apps.skyride.playerDriverNotice') }}
</p>
<k-button
large
rounded
class="skyride-primary"
:disabled="
!canRequestSelectedQuote || skyride.isActionPending
"
@click="requestRide(selectedQuote)"
>
<k-preloader v-if="skyride.isActionPending" />
<template v-else>
{{ phone.t('Apps.skyride.requestRide') }}
<ChevronRight :size="18" aria-hidden="true" />
</template>
</k-button>
</template>
</template>
<template v-else>
<k-card :content-wrap="false" class="skyride-ride-status-card">
<span
class="skyride-status-icon"
:class="'is-' + skyride.activeRide.status"
>
<k-preloader
v-if="skyride.activeRide.status === 'searching'"
/>
<CheckCircle2 v-else :size="22" aria-hidden="true" />
</span>
<div>
<small>{{ phone.t('Apps.skyride.rideStatus') }}</small>
<h1>{{ statusLabel(skyride.activeRide.status) }}</h1>
<p>
{{
phone.t(
`Apps.skyride.statusBody.${skyride.activeRide.status}`,
)
}}
</p>
</div>
</k-card>
<k-card
v-if="skyride.activeRide.driver"
:content-wrap="false"
class="skyride-person-card"
>
<div class="skyride-avatar">
<img
v-if="skyride.activeRide.driver.avatarUrl"
:src="skyride.activeRide.driver.avatarUrl"
alt=""
/>
<UserRound v-else :size="22" aria-hidden="true" />
</div>
<div class="skyride-person-card__body">
<strong>{{ skyride.activeRide.driver.name }}</strong>
<span>
<Star :size="13" fill="currentColor" aria-hidden="true" />
{{
skyride.activeRide.driver.rating.toLocaleString(
phone.lang,
)
}}
· {{ vehicleLabel(skyride.activeRide) }}
</span>
<b>{{ skyride.activeRide.driver.vehicle.plate }}</b>
</div>
<div class="skyride-contact-actions">
<k-button
small
rounded
outline
:disabled="!skyride.activeRide.driver.phoneNumber"
:aria-label="phone.t('Apps.skyride.call')"
@click="callActiveContact"
>
<Phone :size="17" aria-hidden="true" />
</k-button>
<k-button
small
rounded
outline
:aria-label="phone.t('Apps.skyride.message')"
@click="openMessages"
>
<MessageCircle :size="17" aria-hidden="true" />
</k-button>
</div>
</k-card>
<k-card :content-wrap="false" class="skyride-trip-card">
<div class="skyride-route-stop">
<span class="skyride-dot"></span>
<div>
<small>{{ phone.t('Apps.skyride.pickup') }}</small>
<strong>{{
locationLabel(skyride.activeRide.pickup)
}}</strong>
</div>
</div>
<i></i>
<div class="skyride-route-stop">
<MapPin :size="18" aria-hidden="true" />
<div>
<small>{{ phone.t('Apps.skyride.destination') }}</small>
<strong>{{
locationLabel(skyride.activeRide.destination)
}}</strong>
</div>
</div>
<div class="skyride-trip-meta">
<span>{{
phone.t(
`Apps.skyride.services.${skyride.activeRide.serviceClass}.name`,
)
}}</span>
<strong>{{
formatMoney(
skyride.activeRide.price,
skyride.activeRide.currency,
)
}}</strong>
</div>
</k-card>
<div class="skyride-active-actions">
<k-button rounded outline @click="setRideWaypoint">
<Navigation :size="17" aria-hidden="true" />
{{ phone.t('Apps.skyride.navigate') }}
</k-button>
<k-button
v-if="canCancelRide"
rounded
variant="danger"
outline
@click="cancelDialogOpened = true"
>
{{ phone.t('Apps.skyride.cancelRide') }}
</k-button>
</div>
<k-block class="skyride-safety-note">
<ShieldCheck :size="18" aria-hidden="true" />
<span>{{ phone.t('Apps.skyride.safetyNote') }}</span>
</k-block>
</template>
</section>
<section v-else class="skyride-home-panel skyride-driver-home">
<k-card :content-wrap="false" class="skyride-driver-status">
<div
class="skyride-driver-status__icon"
:class="{ 'is-online': skyride.driverOnline }"
>
<Power :size="21" aria-hidden="true" />
</div>
<div>
<strong>{{
phone.t(
`Apps.skyride.driver.${skyride.driverOnline ? 'online' : 'offline'}`,
)
}}</strong>
<span>{{
phone.t(
`Apps.skyride.driver.${skyride.driverOnline ? 'onlineBody' : 'offlineBody'}`,
)
}}</span>
</div>
<k-toggle
:checked="skyride.driverOnline"
:disabled="
skyride.isActionPending || Boolean(skyride.activeRide)
"
:aria-label="phone.t('Apps.skyride.driver.toggleStatus')"
@change="toggleDriverStatus"
/>
</k-card>
<template v-if="skyride.activeRide">
<k-card :content-wrap="false" class="skyride-ride-status-card">
<span class="skyride-status-icon"><CarFront :size="22" /></span>
<div>
<small>{{ phone.t('Apps.skyride.rideStatus') }}</small>
<h1>{{ statusLabel(skyride.activeRide.status) }}</h1>
<p>
{{
phone.t(
`Apps.skyride.driver.statusBody.${skyride.activeRide.status}`,
)
}}
</p>
</div>
</k-card>
<k-card
v-if="skyride.activeRide.passenger"
:content-wrap="false"
class="skyride-person-card"
>
<div class="skyride-avatar">
<img
v-if="skyride.activeRide.passenger.avatarUrl"
:src="skyride.activeRide.passenger.avatarUrl"
alt=""
/>
<UserRound v-else :size="22" aria-hidden="true" />
</div>
<div class="skyride-person-card__body">
<small>{{ phone.t('Apps.skyride.passenger') }}</small>
<strong>{{ skyride.activeRide.passenger.name }}</strong>
<span
><Star :size="13" fill="currentColor" />
{{
skyride.activeRide.passenger.rating.toLocaleString(
phone.lang,
)
}}</span
>
</div>
<div class="skyride-contact-actions">
<k-button
small
rounded
outline
:disabled="!skyride.activeRide.passenger.phoneNumber"
@click="callActiveContact"
>
<Phone :size="17" aria-hidden="true" />
</k-button>
<k-button small rounded outline @click="openMessages">
<MessageCircle :size="17" aria-hidden="true" />
</k-button>
</div>
</k-card>
<k-card :content-wrap="false" class="skyride-trip-card">
<div class="skyride-route-stop">
<span class="skyride-dot"></span>
<div>
<small>{{ phone.t('Apps.skyride.pickup') }}</small
><strong>{{
locationLabel(skyride.activeRide.pickup)
}}</strong>
</div>
</div>
<i></i>
<div class="skyride-route-stop">
<MapPin :size="18" />
<div>
<small>{{ phone.t('Apps.skyride.destination') }}</small
><strong>{{
locationLabel(skyride.activeRide.destination)
}}</strong>
</div>
</div>
<div class="skyride-trip-meta">
<span>{{
phone.t(
`Apps.skyride.services.${skyride.activeRide.serviceClass}.name`,
)
}}</span>
<strong>{{
formatMoney(
skyride.activeRide.price,
skyride.activeRide.currency,
)
}}</strong>
</div>
</k-card>
<div class="skyride-driver-actions">
<k-button rounded outline @click="setRideWaypoint">
<Navigation :size="17" />
{{ phone.t('Apps.skyride.navigate') }}
</k-button>
<k-button
v-if="driverAction"
rounded
:disabled="skyride.isActionPending"
@click="performDriverAction"
>
<k-preloader v-if="skyride.isActionPending" />
<template v-else>{{
phone.t(`Apps.skyride.driver.actions.${driverAction}`)
}}</template>
</k-button>
</div>
</template>
<template v-else>
<div class="skyride-driver-metrics">
<k-card :content-wrap="false">
<CircleDollarSign :size="19" />
<strong>{{
formatMoney(
skyride.profile.earningsToday ?? 0,
skyride.profile.currency,
)
}}</strong>
<span>{{ phone.t('Apps.skyride.driver.today') }}</span>
</k-card>
<k-card :content-wrap="false">
<Star :size="19" />
<strong>{{
skyride.profile.rating.toLocaleString(phone.lang)
}}</strong>
<span>{{ phone.t('Apps.skyride.rating') }}</span>
</k-card>
</div>
<k-block-title class="skyride-custom-block-title">{{
phone.t('Apps.skyride.driver.requests')
}}</k-block-title>
<div
v-if="skyride.driverOnline && skyride.availableRequests.length"
class="skyride-request-list"
>
<k-card
v-for="request in skyride.availableRequests"
:key="request.id"
class="skyride-request-card"
>
<div class="skyride-request-card__top">
<span class="skyride-service-icon"
><CarFront :size="20"
/></span>
<div>
<strong>{{
phone.t(
`Apps.skyride.services.${request.serviceClass}.name`,
)
}}</strong>
<span>{{ formatDate(request.createdAt) }}</span>
</div>
<b>{{ formatMoney(request.price, request.currency) }}</b>
</div>
<div class="skyride-request-route">
<span>{{ locationLabel(request.pickup) }}</span>
<ChevronRight :size="15" />
<span>{{ locationLabel(request.destination) }}</span>
</div>
<k-button
rounded
:disabled="skyride.isActionPending"
@click="acceptRide(request)"
>
{{ phone.t('Apps.skyride.driver.accept') }}
</k-button>
</k-card>
</div>
<k-card v-else :content-wrap="false" class="skyride-empty-card">
<Power v-if="!skyride.driverOnline" :size="28" />
<Clock3 v-else :size="28" />
<strong>{{
phone.t(
`Apps.skyride.driver.${skyride.driverOnline ? 'noRequests' : 'goOnline'}`,
)
}}</strong>
<p>
{{
phone.t(
`Apps.skyride.driver.${skyride.driverOnline ? 'noRequestsBody' : 'goOnlineBody'}`,
)
}}
</p>
</k-card>
</template>
</section>
</template>
<template v-else-if="activeTab === 'rides'">
<section class="skyride-section-screen">
<k-block-header
inset
component="header"
class="skyride-screen-title"
>
<History :size="25" aria-hidden="true" />
<div>
<h1>{{ phone.t('Apps.skyride.rides') }}</h1>
<p>{{ phone.t('Apps.skyride.ridesBody') }}</p>
</div>
</k-block-header>
<k-block v-if="skyride.history.length" class="skyride-history-list">
<k-card
v-for="ride in skyride.history"
:key="ride.id"
header-divider
footer-divider
content-wrap-padding="px-4 py-2"
class="skyride-history-card"
>
<template #header>
<div class="skyride-history-card__header">
<span class="skyride-service-icon"
><CarFront :size="19"
/></span>
<div>
<strong>{{
phone.t(
`Apps.skyride.services.${ride.serviceClass}.name`,
)
}}</strong
><span>{{ formatDate(ride.createdAt) }}</span>
</div>
<k-badge>{{ statusLabel(ride.status) }}</k-badge>
</div>
</template>
<div class="skyride-history-route">
<span>{{ locationLabel(ride.pickup) }}</span
><ChevronRight :size="15" /><span>{{
locationLabel(ride.destination)
}}</span>
</div>
<template #footer>
<div class="skyride-history-card__footer">
<div class="skyride-history-card__meta">
<span>{{
ride.driver?.name ??
ride.passenger?.name ??
phone.t('Apps.skyride.ride')
}}</span>
<span class="skyride-history-card__distance">
<Route :size="13" aria-hidden="true" />
{{ formatRideDistance(ride.distanceMeters) }}
</span>
</div>
<strong>{{
formatMoney(ride.finalPrice ?? ride.price, ride.currency)
}}</strong>
</div>
</template>
</k-card>
</k-block>
<k-card v-else :content-wrap="false" class="skyride-empty-card"
><History :size="29" /><strong>{{
phone.t('Apps.skyride.noRides')
}}</strong>
<p>{{ phone.t('Apps.skyride.noRidesBody') }}</p></k-card
>
</section>
</template>
<template v-else-if="activeTab === 'activity'">
<section class="skyride-section-screen">
<k-block-header
inset
component="header"
class="skyride-screen-title"
>
<Bell :size="25" />
<div>
<h1>{{ phone.t('Apps.skyride.activity') }}</h1>
<p>{{ phone.t('Apps.skyride.activityBody') }}</p>
</div>
</k-block-header>
<k-list
v-if="skyride.history.length"
inset
strong
class="skyride-activity-list"
>
<k-list-item
v-for="ride in skyride.history"
:key="ride.id"
:title="statusLabel(ride.status)"
:subtitle="`${locationLabel(ride.destination)} · ${formatDate(ride.updatedAt)}`"
:after="
formatMoney(ride.finalPrice ?? ride.price, ride.currency)
"
>
<template #media
><span class="skyride-activity-icon"
><CheckCircle2 :size="18" /></span
></template>
</k-list-item>
</k-list>
<k-card v-else :content-wrap="false" class="skyride-empty-card"
><Bell :size="29" /><strong>{{
phone.t('Apps.skyride.noActivity')
}}</strong>
<p>{{ phone.t('Apps.skyride.noActivityBody') }}</p></k-card
>
</section>
</template>
<template v-else-if="activeTab === 'messages'">
<section class="skyride-section-screen">
<k-block-header
inset
component="header"
class="skyride-screen-title"
>
<MessageCircle :size="25" />
<div>
<h1>{{ phone.t('Apps.skyride.messages') }}</h1>
<p>{{ phone.t('Apps.skyride.messagesBody') }}</p>
</div>
</k-block-header>
<k-card
v-if="activeContact"
:content-wrap="false"
class="skyride-message-contact"
>
<div class="skyride-avatar">
<img
v-if="activeContact.avatarUrl"
:src="activeContact.avatarUrl"
alt=""
/><UserRound v-else :size="22" />
</div>
<div>
<strong>{{ activeContact.name }}</strong
><span
><Star :size="13" fill="currentColor" />
{{ activeContact.rating.toLocaleString(phone.lang) }}</span
>
</div>
<ChevronRight :size="18" />
</k-card>
<k-block v-if="activeContact" class="skyride-contact-buttons">
<k-button
rounded
:disabled="!activeContact.phoneNumber"
@click="callActiveContact"
><Phone :size="17" />
{{ phone.t('Apps.skyride.call') }}</k-button
>
<k-button rounded @click="openMessages"
><MessageCircle :size="17" />
{{ phone.t('Apps.skyride.openMessages') }}</k-button
>
</k-block>
<k-card v-else :content-wrap="false" class="skyride-empty-card"
><MessageCircle :size="29" /><strong>{{
phone.t('Apps.skyride.noMessages')
}}</strong>
<p>{{ phone.t('Apps.skyride.noMessagesBody') }}</p>
<k-button rounded @click="openMessages">{{
phone.t('Apps.skyride.openMessages')
}}</k-button></k-card
>
</section>
</template>
<template v-else>
<section class="skyride-section-screen skyride-profile">
<div class="skyride-profile-hero">
<button
type="button"
class="skyride-profile-avatar skyride-profile-avatar--editable"
:aria-label="phone.t('Apps.skyride.editProfile')"
@click="openProfileEditor"
>
<img
v-if="skyride.profile.avatarUrl"
:src="skyride.profile.avatarUrl"
alt=""
/><UserRound v-else :size="32" />
<span><Pencil :size="13" /></span>
</button>
<h1>{{ skyride.profile.name }}</h1>
<span
><Star :size="15" fill="currentColor" />
{{ skyride.profile.rating.toLocaleString(phone.lang) }}</span
>
</div>
<k-card :content-wrap="false" class="skyride-profile-stats">
<div>
<strong>{{
skyride.profile.completedRides.toLocaleString(phone.lang)
}}</strong
><span>{{ phone.t('Apps.skyride.completedRides') }}</span>
</div>
<div>
<strong>{{
skyride.profile.cancelledRides.toLocaleString(phone.lang)
}}</strong
><span>{{ phone.t('Apps.skyride.cancelledRides') }}</span>
</div>
<div>
<strong>{{
skyride.profile.acceptanceRate === null
? phone.t('Apps.skyride.notAvailable')
: `${skyride.profile.acceptanceRate}%`
}}</strong
><span>{{ phone.t('Apps.skyride.acceptance') }}</span>
</div>
</k-card>
<k-block-title>{{ phone.t('Apps.skyride.account') }}</k-block-title>
<k-list inset strong>
<k-list-item
link
:title="phone.t('Apps.skyride.editProfile')"
:subtitle="phone.t('Apps.skyride.editProfileBody')"
@click="openProfileEditor"
><template #media><Pencil :size="18" /></template
></k-list-item>
<k-list-item
:title="phone.t('Apps.skyride.paymentMethod')"
:after="
paymentMethodLabel(skyride.profile.defaultPaymentMethod)
"
><template #media><CircleDollarSign :size="18" /></template
></k-list-item>
<k-list-item
:title="phone.t('Apps.skyride.safety')"
:subtitle="phone.t('Apps.skyride.safetyBody')"
><template #media><ShieldCheck :size="18" /></template
></k-list-item>
<k-list-item
v-if="skyride.driverEligible"
:title="phone.t('Apps.skyride.driverMode')"
:subtitle="phone.t('Apps.skyride.driverModeBody')"
><template #media><CarFront :size="18" /></template
><template #after
><k-toggle
:checked="mode === 'driver'"
:disabled="Boolean(skyride.activeRide)"
:aria-label="phone.t('Apps.skyride.driverMode')"
@change="
mode = mode === 'driver' ? 'rider' : 'driver'
" /></template
></k-list-item>
</k-list>
<k-block class="skyride-member-note"
><Sparkles :size="17" /><span>{{
phone.t('Apps.skyride.memberSince', {
date: formatDate(skyride.profile.memberSince),
})
}}</span></k-block
>
</section>
</template>
</div>
<k-tabbar
class="skyride-tabbar"
floating
:label="phone.t('Apps.skyride.navigation')"
>
<div class="skyride-tab-pane">
<k-tabbar-link
v-for="tab in tabs"
:key="tab.id"
:active="activeTab === tab.id"
@click="selectTab(tab.id)"
>
<template #label>{{
phone.t(`Apps.skyride.tabs.${tab.id}`)
}}</template>
<template #icon>
<component :is="tab.icon" :size="23" :stroke-width="2" />
</template>
</k-tabbar-link>
</div>
</k-tabbar>
</template>
<SkySheet
class="skyride-profile-sheet"
:opened="profileEditorOpened"
:aria-label="phone.t('Apps.skyride.editProfile')"
swipe-to-close
@backdropclick="closeProfileEditor"
@escape="closeProfileEditor"
@swipeclose="closeProfileEditor"
>
<section class="skyride-profile-editor">
<div class="skyride-sheet__title">
<div>
<span>{{ phone.t('Apps.skyride.profile') }}</span>
<h2>{{ phone.t('Apps.skyride.editProfile') }}</h2>
</div>
<SkyButton
icon-only
variant="plain"
:aria-label="phone.t('Common.close')"
@click="closeProfileEditor"
>
<X :size="20" />
</SkyButton>
</div>
<div class="skyride-profile-editor__avatar">
<div class="skyride-profile-avatar">
<img v-if="profileAvatarUrl" :src="profileAvatarUrl" alt="" />
<UserRound v-else :size="32" />
</div>
<strong>{{ phone.t('Apps.skyride.profilePhoto') }}</strong>
<div>
<SkyButton
rounded
class="skyride-profile-media-button"
@click="openProfileMedia('photos')"
>
<Images :size="17" /> {{ phone.t('Apps.skyride.gallery') }}
</SkyButton>
<SkyButton
rounded
class="skyride-profile-media-button"
@click="openProfileMedia('camera')"
>
<Camera :size="17" /> {{ phone.t('Apps.skyride.camera') }}
</SkyButton>
</div>
</div>
<SkySettingsGroup :title="phone.t('Apps.skyride.profileDetails')">
<SkyField
v-model="profileName"
layout="inline"
:label="phone.t('Apps.skyride.profileName')"
:maxlength="50"
:placeholder="phone.t('Apps.skyride.profileNamePlaceholder')"
autocomplete="name"
/>
<SkySettingsRow
v-if="profileAvatarUrl"
kind="action"
tone="danger"
:title="phone.t('Apps.skyride.removeProfilePhoto')"
@activate="removeProfileAvatar"
>
<template #leading><Trash2 :size="18" /></template>
</SkySettingsRow>
</SkySettingsGroup>
<div class="skyride-profile-editor__actions">
<SkyButton
block
large
rounded
variant="secondary"
@click="closeProfileEditor"
>
{{ phone.t('Common.cancel') }}
</SkyButton>
<SkyButton
block
large
rounded
:disabled="!canSaveProfile"
@click="saveProfile"
>
<k-preloader v-if="skyride.isActionPending" />
<template v-else>{{
phone.t('Apps.skyride.saveProfile')
}}</template>
</SkyButton>
</div>
</section>
</SkySheet>
<k-sheet
:opened="Boolean(locationTarget)"
@backdropclick="locationTarget = null"
>
<section
v-if="locationTarget"
class="skyride-sheet__content"
role="dialog"
aria-modal="true"
:aria-label="phone.t(`Apps.skyride.chooseLocation.${locationTarget}`)"
>
<div class="skyride-sheet__handle" aria-hidden="true"></div>
<div class="skyride-sheet__title">
<div>
<span>{{ phone.t('Apps.skyride.location') }}</span>
<h2>
{{ phone.t(`Apps.skyride.chooseLocation.${locationTarget}`) }}
</h2>
</div>
<k-link
icon-only
class="skyride-sheet__close"
:aria-label="phone.t('Common.close')"
@click="locationTarget = null"
><X :size="20"
/></k-link>
</div>
<k-list inset strong class="skyride-sheet-list">
<k-list-item
link
:title="phone.t('Apps.skyride.currentLocation')"
:subtitle="phone.t('Apps.skyride.useGps')"
@click="useCurrentLocation"
><template #media><Crosshair :size="19" /></template
></k-list-item>
</k-list>
<k-block-title class="skyride-sheet-section-title">{{
phone.t('Apps.skyride.savedPlaces')
}}</k-block-title>
<k-list inset strong class="skyride-sheet-list skyride-saved-list">
<k-list-item
v-for="location in skyride.quickLocations"
:key="location.id ?? location.label"
link
:title="locationLabel(location)"
@click="chooseLocation(location)"
><template #media
><component
:is="location.id === 'work' ? BriefcaseBusiness : MapPin"
:size="18" /></template
></k-list-item>
</k-list>
</section>
</k-sheet>
<k-sheet :opened="Boolean(ratingRide)" @backdropclick="dismissRating">
<section
v-if="ratingRide"
class="skyride-sheet__content skyride-rating"
role="dialog"
aria-modal="true"
:aria-label="phone.t('Apps.skyride.rateRide')"
>
<div class="skyride-sheet__handle" aria-hidden="true"></div>
<div class="skyride-rating__success"><CheckCircle2 :size="30" /></div>
<h2>{{ phone.t('Apps.skyride.rideComplete') }}</h2>
<p>{{ phone.t('Apps.skyride.rateRideBody') }}</p>
<div
class="skyride-rating-stars"
:aria-label="phone.t('Apps.skyride.rating')"
>
<k-button
v-for="value in 5"
:key="value"
small
rounded
variant="plain"
class="skyride-rating-star"
:class="{ 'is-active': value <= rating }"
:aria-pressed="value <= rating"
:aria-label="
phone.t('Apps.skyride.ratingValue', { rating: value.toString() })
"
@click="rating = value"
>
<Star :size="28" fill="currentColor" />
</k-button>
</div>
<span class="skyride-rating-label">{{
phone.t('Apps.skyride.tip')
}}</span>
<div class="skyride-tip-options">
<k-button
v-for="value in [0, 5, 10, 20]"
:key="value"
small
rounded
:outline="tip !== value"
@click="tip = value"
>{{
value === 0
? phone.t('Apps.skyride.noTip')
: formatMoney(value, ratingRide.currency)
}}</k-button
>
</div>
<k-list inset strong
><k-list-input
id="skyride-rating-comment"
outline
:label="phone.t('Apps.skyride.comment')"
:placeholder="phone.t('Apps.skyride.commentPlaceholder')"
:value="ratingComment"
maxlength="180"
@input="updateRatingComment"
/></k-list>
<k-button
large
rounded
:disabled="rating < 1 || skyride.isActionPending"
@click="submitRating"
><k-preloader v-if="skyride.isActionPending" /><template v-else>{{
phone.t('Apps.skyride.submitRating')
}}</template></k-button
>
<k-link @click="dismissRating">{{
phone.t('Apps.skyride.notNow')
}}</k-link>
</section>
</k-sheet>
<k-dialog
:opened="cancelDialogOpened"
:title="phone.t('Apps.skyride.cancelTitle')"
:content="phone.t('Apps.skyride.cancelBody')"
role="alertdialog"
@backdropclick="cancelDialogOpened = false"
@escape="cancelDialogOpened = false"
>
<template #buttons
><k-dialog-button @click="cancelDialogOpened = false">{{
phone.t('Common.cancel')
}}</k-dialog-button
><k-dialog-button strong @click="confirmCancel">{{
phone.t('Apps.skyride.cancelRide')
}}</k-dialog-button></template
>
</k-dialog>
<k-notification :opened="Boolean(toastText)" :text="toastText" />
</k-page>
</template>
<style scoped>
.skyride-app {
--ride-accent: #f5c518;
--ride-accent-strong: #725600;
--ride-bg-rgb: 244 244 247;
--ride-bg: #f4f4f7;
--ride-card: rgba(255, 255, 255, 0.88);
--ride-card-strong: #fff;
--ride-border: rgba(26, 26, 28, 0.09);
--ride-text: #171719;
--ride-muted: #707078;
--ride-map: #07131f;
--ride-profile-media-bg: #725600;
--sky-bg: var(--ride-bg);
--sky-surface: var(--ride-card-strong);
--sky-surface-muted: #e6e6eb;
--sky-text: var(--ride-text);
--sky-muted: var(--ride-muted);
--sky-hairline: var(--ride-border);
--sky-danger: #d70015;
position: relative;
height: 100%;
overflow: hidden;
color: var(--ride-text);
background: var(--ride-bg);
}
.skyride-app--dark {
--ride-accent-strong: #f5c518;
--ride-bg-rgb: 8 9 11;
--ride-bg: #08090b;
--ride-card: rgba(29, 30, 33, 0.9);
--ride-card-strong: #1c1d20;
--ride-border: rgba(255, 255, 255, 0.09);
--ride-text: #f7f7f8;
--ride-muted: #a2a2aa;
--ride-map: #050a10;
--ride-profile-media-bg: #9b7600;
--sky-surface-muted: #2c2c2e;
--sky-danger: #ff453a;
}
.skyride-ambient {
position: absolute;
inset: 0;
pointer-events: none;
background:
radial-gradient(
circle at 82% 10%,
rgba(245, 197, 24, 0.16),
transparent 28%
),
linear-gradient(180deg, rgba(245, 197, 24, 0.04), transparent 32%);
}
.skyride-navbar {
--k-safe-area-top: 56px;
z-index: 22;
position: relative;
color: var(--ride-text);
}
.skyride-navbar :deep(.sky-navbar__title) {
color: var(--ride-text);
font-size: 18px;
font-weight: 700;
line-height: 20px;
letter-spacing: -0.25px;
}
.skyride-navbar :deep(.sky-navbar__subtitle) {
margin-top: 1px;
color: var(--ride-muted);
font-size: 12px;
font-weight: 600;
line-height: 14px;
opacity: 1;
}
.skyride-mode {
z-index: 21;
position: relative;
min-height: 9px;
padding: 4px 16px 7px;
}
.skyride-mode > :deep(*) {
width: 100%;
}
.skyride-mode :deep(button) {
min-height: 38px;
font-size: 15px;
font-weight: 600;
}
.skyride-scroll {
position: absolute;
z-index: 2;
inset: 148px 0 116px;
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-width: none;
}
.skyride-scroll::-webkit-scrollbar {
display: none;
}
.skyride-loading,
.skyride-unavailable {
position: absolute;
z-index: 4;
inset: 92px 24px 72px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 12px;
margin: 0;
text-align: center;
}
.skyride-unavailable {
padding: 24px;
border: 1px solid var(--ride-border);
border-radius: 22px;
background: var(--ride-card);
}
.skyride-loading span,
.skyride-unavailable p {
color: var(--ride-muted);
}
.skyride-unavailable strong {
font-size: 20px;
}
.skyride-unavailable p {
margin: 0;
font-size: 13px;
}
.skyride-home-panel,
.skyride-section-screen {
position: relative;
z-index: 4;
padding: 18px 14px 24px;
}
.skyride-home-panel {
min-height: 320px;
margin-top: 0;
background: linear-gradient(
180deg,
rgb(var(--ride-bg-rgb) / 0) 0,
rgb(var(--ride-bg-rgb) / 0.78) 54px,
var(--ride-bg) 112px
);
}
.skyride-heading,
.skyride-screen-title {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin: 0;
padding: 0 4px 12px;
}
.skyride-heading span,
.skyride-screen-title p,
.skyride-ride-status-card p {
margin: 0;
color: var(--ride-muted);
font-size: 13px;
line-height: 18px;
}
.skyride-heading h1,
.skyride-ride-status-card h1,
.skyride-screen-title h1 {
margin: 2px 0 0;
color: var(--ride-text);
font-size: 24px;
font-weight: 700;
line-height: 28px;
letter-spacing: -0.55px;
}
.skyride-heading__icon,
.skyride-status-icon {
display: grid;
flex: 0 0 auto;
width: 42px;
height: 42px;
place-items: center;
border-radius: 15px;
color: #111;
background: var(--ride-accent);
box-shadow: none;
}
.skyride-heading__icon > svg {
display: block;
width: 20px;
height: 20px;
}
.skyride-status-icon.is-searching {
color: var(--ride-accent-strong);
background: rgba(245, 197, 24, 0.14);
box-shadow: none;
}
.skyride-location-list,
.skyride-service-list,
.skyride-activity-list,
.skyride-sheet__content :deep(.sky-list),
.skyride-profile :deep(.sky-list) {
margin-block: 0 14px;
}
.skyride-location-list,
.skyride-activity-list {
margin-inline: 2px !important;
}
.skyride-location-list :deep(li),
.skyride-service-list :deep(li),
.skyride-activity-list :deep(li),
.skyride-profile :deep(li),
.skyride-sheet__content :deep(li) {
background: var(--ride-card-strong);
}
.skyride-dot {
display: block;
width: 11px;
height: 11px;
border: 3px solid var(--ride-accent);
border-radius: 50%;
background: #171719;
}
.skyride-quick-grid,
.skyride-driver-metrics {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 9px;
margin: 0 2px 16px;
padding: 0;
}
.skyride-custom-block-title {
height: auto !important;
margin: 20px 4px 10px !important;
padding: 0 !important;
color: var(--ride-muted) !important;
font-size: 15px !important;
font-weight: 650 !important;
line-height: 20px !important;
}
.skyride-quick-card,
.skyride-driver-metrics :deep(.sky-card),
.skyride-person-card,
.skyride-trip-card,
.skyride-driver-status,
.skyride-request-card,
.skyride-history-card,
.skyride-message-contact,
.skyride-empty-card,
.skyride-fare-card,
.skyride-ride-status-card {
margin: 0;
border: 1px solid var(--ride-border);
color: var(--ride-text);
background: var(--ride-card);
box-shadow: 0 7px 23px rgba(0, 0, 0, 0.05);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
}
.skyride-ride-status-card {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
padding: 15px;
background:
linear-gradient(135deg, rgba(245, 197, 24, 0.12), transparent 58%),
var(--ride-card);
}
.skyride-ride-status-card > div {
min-width: 0;
}
.skyride-ride-status-card small {
color: var(--ride-accent-strong);
font-size: 9px;
font-weight: 750;
text-transform: uppercase;
letter-spacing: 0.9px;
}
.skyride-ride-status-card h1 {
margin: 2px 0;
font-size: 20px;
line-height: 1.05;
}
.skyride-ride-status-card p {
line-height: 1.3;
}
.skyride-quick-card {
display: flex;
width: 100%;
min-height: 64px;
align-items: center;
justify-content: flex-start;
gap: 10px;
padding: 11px 13px;
color: inherit;
text-align: left;
}
.skyride-quick-card svg,
.skyride-driver-metrics svg {
color: var(--ride-accent-strong);
}
.skyride-quick-card span {
display: -webkit-box;
min-width: 0;
overflow: hidden;
font-size: 12.5px;
font-weight: 650;
line-height: 16px;
overflow-wrap: anywhere;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.skyride-primary {
width: calc(100% - 4px);
min-height: 50px;
margin: 4px 2px 0;
gap: 5px;
font-size: 16px;
font-weight: 650;
}
.skyride-player-driver-notice {
margin: 2px 7px 8px;
color: var(--ride-muted);
font-size: 12px;
line-height: 17px;
}
.skyride-primary :deep(svg) {
margin-left: 4px;
}
.skyride-quote-summary {
display: flex;
align-items: center;
gap: 8px;
padding: 0 5px 12px;
color: var(--ride-muted);
font-size: 12px;
}
.skyride-quote-chip {
min-width: 0;
padding-inline: 8px;
color: var(--ride-muted);
font-size: 10px;
}
.skyride-quote-summary :deep(button) {
margin-left: auto;
}
.skyride-service-list :deep(li.is-selected) {
box-shadow: inset 3px 0 var(--ride-accent);
}
.skyride-service-list :deep(li.is-unavailable) {
cursor: default;
opacity: 0.45;
}
.skyride-service-list :deep(.sky-list-item__after) {
display: flex;
align-items: center;
gap: 6px;
color: var(--ride-text);
}
.skyride-fare-card {
margin: 0 0 13px;
padding: 14px;
}
.skyride-fare-card__heading {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 10px;
margin-bottom: 11px;
}
.skyride-fare-card__icon {
display: grid;
width: 36px;
height: 36px;
place-items: center;
border-radius: 12px;
color: #151515;
background: var(--ride-accent);
}
.skyride-fare-card__heading > div {
display: flex;
min-width: 0;
flex-direction: column;
}
.skyride-fare-card__heading small,
.skyride-fare-card__heading > span:last-child,
.skyride-fare-breakdown span {
color: var(--ride-muted);
font-size: 10px;
}
.skyride-fare-card__heading strong {
font-size: 17px;
}
.skyride-fare-card__heading > span:last-child {
max-width: 96px;
text-align: right;
}
.skyride-fare-card > :deep(.sky-segmented) {
width: 100%;
}
.skyride-fare-breakdown {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-top: 11px;
padding: 10px 11px;
border-radius: 12px;
background: var(--ride-border);
}
.skyride-custom-fare {
display: grid;
gap: 8px;
margin-top: 10px;
}
.skyride-custom-fare :deep(.sky-list) {
margin: 0;
}
.skyride-custom-fare > :deep(button) {
width: 100%;
}
.skyride-service-icon,
.skyride-activity-icon {
display: grid;
width: 36px;
height: 36px;
place-items: center;
border-radius: 12px;
color: #151515;
background: var(--ride-accent);
}
.skyride-person-card,
.skyride-message-contact {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
padding: 16px;
}
.skyride-avatar,
.skyride-profile-avatar {
display: grid;
flex: 0 0 auto;
width: 46px;
height: 46px;
place-items: center;
overflow: hidden;
border: 2px solid var(--ride-accent);
border-radius: 50%;
color: var(--ride-muted);
background: var(--ride-bg);
}
.skyride-avatar img,
.skyride-profile-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.skyride-person-card__body,
.skyride-message-contact > div:nth-child(2) {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
gap: 2px;
}
.skyride-person-card__body > span,
.skyride-message-contact > div:nth-child(2) span {
display: flex;
align-items: center;
gap: 3px;
color: var(--ride-muted);
font-size: 12px;
}
.skyride-person-card__body b {
width: fit-content;
margin-top: 2px;
padding: 2px 6px;
border-radius: 5px;
color: var(--ride-text);
background: var(--ride-border);
font-size: 10px;
letter-spacing: 0.8px;
}
.skyride-contact-actions,
.skyride-active-actions,
.skyride-driver-actions,
.skyride-contact-buttons {
display: flex;
gap: 8px;
}
.skyride-contact-actions {
flex: 0 0 auto;
margin-left: auto;
}
.skyride-contact-actions :deep(button) {
width: 36px;
min-width: 36px;
flex: 0 0 36px;
padding-inline: 0;
}
.skyride-trip-card {
margin-bottom: 12px;
padding: 16px;
}
.skyride-route-stop {
display: flex;
align-items: center;
gap: 11px;
}
.skyride-route-stop > svg {
flex: 0 0 18px;
color: var(--ride-accent-strong);
}
.skyride-route-stop > div {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
gap: 1px;
}
.skyride-route-stop small,
.skyride-person-card small {
color: var(--ride-muted);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.skyride-route-stop strong {
overflow: hidden;
font-size: 13px;
text-overflow: ellipsis;
white-space: nowrap;
}
.skyride-trip-card > i {
display: block;
width: 1px;
height: 17px;
margin: 1px 0 1px 5px;
border-left: 1px dashed var(--ride-muted);
}
.skyride-trip-meta {
display: flex;
justify-content: space-between;
margin-top: 13px;
padding-top: 11px;
border-top: 1px solid var(--ride-border);
color: var(--ride-muted);
font-size: 12px;
}
.skyride-trip-meta strong {
color: var(--ride-text);
font-size: 14px;
}
.skyride-active-actions > *,
.skyride-driver-actions > *,
.skyride-contact-buttons > * {
flex: 1;
}
.skyride-active-actions :deep(button),
.skyride-driver-actions :deep(button),
.skyride-contact-buttons :deep(button) {
gap: 6px;
}
.skyride-safety-note,
.skyride-member-note {
display: flex;
align-items: center;
gap: 8px;
margin: 12px 2px 0;
color: var(--ride-muted);
font-size: 11px;
line-height: 1.35;
}
.skyride-safety-note svg,
.skyride-member-note svg {
flex: 0 0 auto;
color: var(--ride-accent-strong);
}
.skyride-driver-status {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 11px;
margin-bottom: 14px;
padding: 16px;
}
.skyride-driver-status__icon {
display: grid;
width: 42px;
height: 42px;
place-items: center;
border-radius: 14px;
color: var(--ride-muted);
background: var(--ride-border);
}
.skyride-driver-status__icon.is-online {
color: #151515;
background: var(--ride-accent);
}
.skyride-driver-status > div:nth-child(2) {
display: flex;
min-width: 0;
flex-direction: column;
gap: 2px;
}
.skyride-driver-status span,
.skyride-driver-metrics span,
.skyride-request-card span,
.skyride-history-card span {
color: var(--ride-muted);
font-size: 12px;
line-height: 16px;
}
.skyride-driver-metrics :deep(.sky-card) {
display: flex;
flex-direction: column;
gap: 3px;
padding: 16px;
}
.skyride-driver-metrics strong {
margin-top: 3px;
font-size: 19px;
}
.skyride-request-list,
.skyride-history-list {
display: grid;
gap: 10px;
}
.skyride-history-list {
margin: 0;
padding: 0;
}
.skyride-request-card__top,
.skyride-history-card__header {
display: flex;
align-items: center;
gap: 9px;
}
.skyride-request-card__top > div,
.skyride-history-card__header > div {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
}
.skyride-request-card__top > div > span,
.skyride-history-card__header > div > span {
font-size: 11px;
line-height: 15px;
}
.skyride-request-route,
.skyride-history-route {
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
align-items: center;
gap: 6px;
margin: 12px 0;
padding: 9px;
border-radius: 11px;
background: var(--ride-border);
}
.skyride-request-route span,
.skyride-history-route span {
overflow: hidden;
font-size: 12px;
line-height: 16px;
text-overflow: ellipsis;
white-space: nowrap;
}
.skyride-request-route span:last-child,
.skyride-history-route span:last-child {
text-align: right;
}
.skyride-empty-card {
display: flex;
align-items: center;
flex-direction: column;
gap: 7px;
padding: 27px 16px;
text-align: center;
}
.skyride-empty-card svg {
color: var(--ride-accent-strong);
}
.skyride-empty-card p {
max-width: 250px;
margin: 0;
color: var(--ride-muted);
font-size: 12px;
line-height: 1.45;
}
.skyride-screen-title {
justify-content: flex-start;
margin: 0 0 8px;
}
.skyride-screen-title > svg {
color: var(--ride-accent-strong);
}
.skyride-history-card__footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
font-size: 12px;
}
.skyride-history-card__meta {
display: flex;
min-width: 0;
flex-direction: column;
gap: 2px;
}
.skyride-history-card__distance {
display: inline-flex;
align-items: center;
gap: 4px;
}
.skyride-history-card__distance > svg {
flex: 0 0 auto;
color: var(--ride-accent-strong);
}
.skyride-message-contact > svg {
color: var(--ride-muted);
}
.skyride-contact-buttons {
margin: 10px 0 0;
padding: 0;
}
.skyride-profile-hero {
display: flex;
align-items: center;
flex-direction: column;
padding: 8px 0 18px;
}
.skyride-profile-avatar {
width: 78px;
height: 78px;
border-width: 3px;
box-shadow: 0 0 0 5px rgba(245, 197, 24, 0.13);
}
button.skyride-profile-avatar--editable {
position: relative;
padding: 0;
cursor: pointer;
font: inherit;
}
.skyride-profile-avatar--editable:focus-visible {
outline: 3px solid var(--ride-accent-strong);
outline-offset: 5px;
}
.skyride-profile-avatar--editable > span {
position: absolute;
right: -1px;
bottom: -1px;
display: grid;
width: 25px;
height: 25px;
place-items: center;
border: 2px solid var(--ride-bg);
border-radius: 50%;
color: #171719;
background: var(--ride-accent);
}
.skyride-profile-hero h1 {
margin: 12px 0 3px;
font-size: 22px;
}
.skyride-profile-hero > span {
display: flex;
align-items: center;
gap: 4px;
color: var(--ride-accent-strong);
font-weight: 700;
}
.skyride-profile-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
margin-bottom: 17px;
padding: 13px 4px;
border: 1px solid var(--ride-border);
border-radius: 17px;
background: var(--ride-card);
box-shadow: 0 7px 23px rgba(0, 0, 0, 0.05);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
}
.skyride-profile-stats > div {
display: flex;
min-width: 0;
align-items: center;
flex-direction: column;
gap: 2px;
padding-inline: 4px;
text-align: center;
}
.skyride-profile-stats > div + div {
border-left: 1px solid var(--ride-border);
}
.skyride-profile-stats strong {
font-size: 16px;
}
.skyride-profile-stats span {
color: var(--ride-muted);
font-size: 9px;
line-height: 1.15;
}
.skyride-tabbar {
z-index: 25;
color: var(--ride-text);
}
.skyride-tabbar :deep(.sky-tabbar__inner),
.skyride-tabbar :deep(.sky-tabbar__pane) {
width: 100% !important;
max-width: none !important;
gap: 0 !important;
}
.skyride-tab-pane {
width: 100% !important;
max-width: none !important;
display: flex;
align-items: stretch;
gap: 0 !important;
}
.skyride-tab-pane :deep(> .sky-tab-button) {
flex: 1 1 20%;
min-width: 0 !important;
padding-inline: 1px !important;
outline: none;
}
.skyride-tab-pane :deep(> .sky-tab-button .sky-tab-button__label) {
max-width: 100%;
overflow: hidden;
font-size: 10px;
font-weight: 600;
line-height: 13px;
text-overflow: ellipsis;
white-space: nowrap;
}
.skyride-tab-pane :deep(> .sky-tab-button .sky-tab-button__icon) {
width: 24px;
height: 24px;
}
.skyride-tab-pane :deep(> .sky-tab-button .sky-tab-button__icon > svg) {
display: block;
width: 23px;
height: 23px;
}
.skyride-sheet__content {
max-height: 74vh;
overflow-y: auto;
padding: 8px 14px calc(24px + env(safe-area-inset-bottom));
color: var(--ride-text);
background: var(--ride-bg);
border-radius: 26px 26px 0 0;
}
.skyride-profile-editor {
box-sizing: border-box;
min-height: 0;
padding: 4px 14px calc(var(--sky-safe-area-bottom) + 12px);
color: var(--ride-text);
background: var(--ride-bg);
border-radius: 0 0 28px 28px;
}
.skyride-profile-sheet :deep(.sky-overlay-backdrop) {
background: rgba(0, 0, 0, 0.38);
}
.skyride-profile-sheet :deep(.sky-sheet__panel) {
top: auto;
max-height: 78%;
overflow-x: hidden;
overflow-y: auto;
border: 0;
border-radius: 28px 28px 0 0;
background: var(--ride-bg);
box-shadow: none;
}
.skyride-profile-editor__avatar {
display: flex;
align-items: center;
flex-direction: column;
gap: 11px;
padding: 8px 0 18px;
}
.skyride-profile-editor__avatar > strong {
font-size: 14px;
}
.skyride-profile-editor__avatar > div:last-child,
.skyride-profile-editor__actions {
display: flex;
width: 100%;
gap: 9px;
}
.skyride-profile-editor__avatar > div:last-child {
justify-content: center;
}
.skyride-profile-editor__avatar :deep(.sky-button) {
gap: 6px;
}
.skyride-profile-editor__avatar :deep(.skyride-profile-media-button) {
border-color: var(--ride-profile-media-bg);
background: var(--ride-profile-media-bg);
color: #fff !important;
}
.skyride-profile-editor__avatar
:deep(.skyride-profile-media-button:active:not(:disabled)) {
border-color: var(--ride-profile-media-bg);
background: var(--ride-profile-media-bg);
color: #fff !important;
filter: brightness(0.9);
}
.skyride-profile-editor__actions {
margin-top: 4px;
}
.skyride-profile-editor__actions :deep(.sky-button) {
flex: 1 1 0;
}
.skyride-sheet__handle {
width: 38px;
height: 5px;
margin: 0 auto 14px;
border-radius: 9px;
background: var(--ride-muted);
opacity: 0.65;
}
.skyride-sheet__title {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 4px 13px;
}
.skyride-sheet__title span,
.skyride-rating-label {
color: var(--ride-muted);
font-size: 11px;
font-weight: 650;
line-height: 14px;
text-transform: uppercase;
letter-spacing: 0.65px;
}
.skyride-sheet__title h2,
.skyride-rating h2 {
margin: 3px 0 0;
color: var(--ride-text);
font-size: 22px;
font-weight: 700;
line-height: 27px;
letter-spacing: -0.35px;
}
.skyride-sheet__close {
display: grid;
flex: 0 0 auto;
width: 40px;
height: 40px;
margin-right: -8px;
place-items: center;
border-radius: 50%;
}
.skyride-sheet-list {
margin-block: 0 !important;
}
.skyride-sheet-list :deep(.text-\[17px\]) {
color: var(--ride-text);
font-size: 16px;
line-height: 21px;
}
.skyride-sheet-list :deep(.text-sm) {
color: var(--ride-muted);
font-size: 13px;
line-height: 18px;
}
.skyride-sheet-section-title {
height: auto !important;
margin: 22px 4px 10px !important;
padding: 0 !important;
color: var(--ride-muted) !important;
font-size: 15px !important;
font-weight: 650 !important;
line-height: 20px !important;
}
.skyride-saved-list {
margin-bottom: 4px !important;
}
.skyride-rating {
display: flex;
align-items: center;
flex-direction: column;
text-align: center;
}
.skyride-rating__success {
display: grid;
width: 56px;
height: 56px;
place-items: center;
border-radius: 20px;
color: #151515;
background: var(--ride-accent);
}
.skyride-rating p {
max-width: 270px;
margin: 4px 0 12px;
color: var(--ride-muted);
font-size: 12px;
}
.skyride-rating-stars {
display: flex;
gap: 5px;
margin-bottom: 17px;
}
.skyride-rating-star {
width: 34px;
min-width: 34px;
height: 34px;
padding: 2px;
color: var(--ride-border);
}
.skyride-rating-star.is-active {
color: var(--ride-accent);
}
.skyride-tip-options {
display: flex;
gap: 6px;
margin: 8px 0 12px;
}
.skyride-rating :deep(.sky-list) {
width: 100%;
}
.skyride-rating > :deep(button) {
width: 100%;
}
.skyride-rating > :deep(a),
.skyride-rating > :deep(.sky-link) {
margin-top: 11px;
}
.skyride-dialog {
padding: 20px 20px 8px;
color: var(--ride-text);
text-align: center;
}
.skyride-dialog h2 {
margin: 0 0 7px;
font-size: 18px;
}
.skyride-dialog p {
margin: 0;
color: var(--ride-muted);
font-size: 13px;
line-height: 1.4;
}
.skyride-app :deep(.bg-primary) {
color: #141414;
background-color: var(--ride-accent);
}
.skyride-app :deep(.sky-button--primary) {
color: #171719;
}
.skyride-app :deep(.text-primary) {
color: var(--ride-accent-strong);
}
.skyride-app--dark :deep(.text-primary) {
color: var(--ride-accent);
}
@media (max-height: 730px) {
.skyride-home-panel,
.skyride-section-screen {
padding-top: 14px;
}
}
</style>