mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 17:23:25 +00:00
* 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.
2991 lines
82 KiB
Vue
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>
|