Commit 30ed41c1 authored by prurite's avatar prurite
Browse files

BusTimerV2: bug fixes and improvements

parent cdacf9ee
Loading
Loading
Loading
Loading
+0 −2
Original line number Diff line number Diff line
@@ -16,7 +16,6 @@ import TalksTodayNav from './components/TalksTodayNav.vue'
import { BusHomeV2 } from './components/bus-v2/index.mjs'
import BusRouteV2 from './components/bus-v2/BusRouteV2.vue'
import BusStopV2 from './components/bus-v2/BusStopV2.vue'
import BusVehiclesV2 from './components/bus-v2/BusVehiclesV2.vue'
import BusSchedulesV2 from './components/bus-v2/BusSchedulesV2.vue'

export default defineClientConfig({
@@ -36,7 +35,6 @@ export default defineClientConfig({
    app.component("BusHomeV2", BusHomeV2)
    app.component("BusRouteV2", BusRouteV2)
    app.component("BusStopV2", BusStopV2)
    app.component("BusVehiclesV2", BusVehiclesV2)
    app.component("BusSchedulesV2", BusSchedulesV2)

    // 含有echart的组件,注意需要用non-ssr模式
+51 −17
Original line number Diff line number Diff line
@@ -11,6 +11,7 @@
    <section class="panel search" :aria-label="busText('search')">
      <label class="sr-only" for="bus-search">{{ busText('search') }}</label>
      <input id="bus-search" v-model="query" type="search" :placeholder="busText('search')" @keydown.enter="openFirstResult">
      <button v-if="query" class="search-clear" type="button" :aria-label="busLanguage === 'zh' ? '清空搜索' : 'Clear search'" @click="query = ''">×</button>
      <div v-if="query" class="search-results">
        <button v-for="result in searchResults" :key="`${result.kind}-${result.item.id}`" type="button" @click="openResult(result)">
          <small>{{ busText(result.kind === 'route' ? 'routeLabel' : 'stopLabel') }}</small>
@@ -43,11 +44,12 @@
        <div v-if="favoriteRoutes.length" class="quick-links">
          <button v-for="route in favoriteRoutes" :key="route.id" type="button" @click="openRoute(route.id)">🚌 {{ displayName(route, busLanguage) }}</button>
        </div>
        <BusVehicleLegendV2 v-if="favoriteStopCards.length" :language="busLanguage" />
        <article v-for="stop in favoriteStopCards" :key="stop.id" class="nearby-stop">
          <div class="nearby-stop__head"><button type="button" class="link-title" @click="openStop(stop.id)">{{ displayStopName(stop, busLanguage) }}</button><span v-if="stop.distance != null">{{ formatDistance(stop.distance) }}</span></div>
          <p v-if="favoriteArrivals[stop.id]?.loading" class="muted">{{ busText('loading') }}</p>
          <p v-else-if="favoriteArrivals[stop.id]?.error" class="muted">{{ busText('unavailable') }}</p>
          <ul v-else class="arrival-list"><li v-for="arrival in favoriteArrivals[stop.id]?.items" :key="`${arrival.route_direction_id}-${arrival.source}-${arrival.trip_id || arrival.planned_arrival_at || ''}`"><i :style="{ background: arrival.route_color || '#2878c8' }" /><a class="arrival-link" :href="routeDirectionHref(arrival)">{{ arrivalName(arrival) }}</a><strong>{{ arrivalText(arrival) }}</strong></li><li v-if="!favoriteArrivals[stop.id]?.items?.length" class="muted">{{ busText('empty') }}</li></ul>
          <ul v-else class="arrival-list"><li v-for="arrival in favoriteArrivals[stop.id]?.items" :key="`${arrival.route_direction_id}-${arrival.source}-${arrival.trip_id || arrival.planned_arrival_at || ''}`"><i :style="{ background: arrival.route_color || '#2878c8' }" /><a class="arrival-link" :href="routeDirectionHref(arrival)">{{ arrivalName(arrival) }}</a><span>{{ arrivalText(arrival) }}</span><img v-if="showVehicleIcon(arrival)" class="arrival-vehicle" :src="vehicleIcon(arrival)" :alt="vehicleLabel(arrival)"></li><li v-if="!favoriteArrivals[stop.id]?.items?.length" class="muted">{{ busText('empty') }}</li></ul>
        </article>
        <p v-if="!favoriteRoutes.length && !favoriteStops.length" class="muted">{{ busText('noFavorites') }}</p>
      </section>
@@ -57,6 +59,7 @@
          <div class="nearby-title"><h3>{{ busText('nearby') }}</h3><span v-if="locationUpdatedText" class="location-updated">{{ locationUpdatedText }}</span></div>
          <button type="button" :disabled="locationState === 'loading'" @click="locate">{{ busText(locationState === 'loading' ? 'locating' : 'locate') }}</button>
        </div>
        <BusVehicleLegendV2 :language="busLanguage" />
        <p v-if="locationState === 'idle'" class="muted">{{ busText('noLocation') }}</p>
        <p v-else-if="locationState === 'denied' || locationState === 'failed'" class="muted">{{ busText(locationErrorKey || 'locationFailed') }}</p>
        <p v-else-if="locationState === 'ready' && !nearbyStops.length" class="muted">{{ busText('empty') }}</p>
@@ -71,7 +74,8 @@
            <li v-for="arrival in arrivals[stop.id]?.items" :key="`${arrival.route_direction_id}-${arrival.source}-${arrival.trip_id || arrival.planned_arrival_at || ''}`">
              <i :style="{ background: arrival.route_color || '#2878c8' }" />
              <a class="arrival-link" :href="routeDirectionHref(arrival)">{{ arrivalName(arrival) }}</a>
              <strong>{{ arrivalText(arrival) }}</strong>
              <span>{{ arrivalText(arrival) }}</span>
              <img v-if="showVehicleIcon(arrival)" class="arrival-vehicle" :src="vehicleIcon(arrival)" :alt="vehicleLabel(arrival)">
            </li>
            <li v-if="!arrivals[stop.id]?.items?.length" class="muted">{{ busText('empty') }}</li>
          </ul>
@@ -79,8 +83,11 @@
        <button v-if="nearbyStops.length > 2" type="button" class="nearby-toggle" :aria-expanded="allNearbyStops" @click="allNearbyStops = !allNearbyStops">{{ allNearbyStops ? (busLanguage === 'zh' ? '收起' : 'Show less') : (busLanguage === 'zh' ? '展开' : 'Show all') }}</button>
      </section>

      <section class="panel live-vehicles">
        <BusVehiclesV2 :routes="routes" :stops="stops" :vehicles="vehicles" :loading="vehiclesLoading" :error="vehiclesError" :show-all-vehicles="showAllVehicles" @update:show-all-vehicles="setShowAllVehicles" />
      </section>

      <nav class="feature-links" :aria-label="busLanguage === 'zh' ? '巴士功能' : 'Bus features'">
        <a :href="vehiclesHref"><span>{{ busText('vehicles') }}</span></a>
        <a :href="schedulesHref"><span>{{ busText('schedules') }}</span></a>
        <a :href="filesHref"><span>{{ busText('files') }}</span></a>
      </nav>
@@ -91,15 +98,16 @@
<script setup>
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { busApi } from './api.mjs'
import { displayName, displayStopName, formatDistance, formatLocalDateTime, haversineMeters, isTerminalArrival, matchesSearch, sortArrivalsByEstimatedTime, unavailableReasonTextKey } from './core.mjs'
import { displayName, displayStopName, formatDistance, formatLocalDateTime, haversineMeters, isTerminalArrival, matchesSearch, realtimeArrivalText, sortArrivalsByEstimatedTime, unavailableReasonTextKey } from './core.mjs'
import { favoriteRouteIds, favoriteStopIds, loadFavorites } from './favorites.mjs'
import { busLanguage, busText, setBusLanguage } from './i18n.mjs'
import { renderNoticeMarkdown } from './markdown.mjs'
import BusVehicleLegendV2 from './BusVehicleLegendV2.vue'
import BusVehiclesV2 from './BusVehiclesV2.vue'

const props = defineProps({
  routeHref: { type: String, default: '/transport/bustimer_v2_route.html?id=' },
  stopHref: { type: String, default: '/transport/bustimer_v2_stop.html?id=' },
  vehiclesHref: { type: String, default: '/transport/bustimer_v2_vehicles.html' },
  schedulesHref: { type: String, default: '/transport/bustimer_v2_schedules.html' },
  filesHref: { type: String, default: '/transport/bustimer_v2_files.html' },
})
@@ -109,6 +117,10 @@ const error = ref('')
const routes = ref([])
const stops = ref([])
const notices = ref([])
const vehicles = ref([])
const vehiclesLoading = ref(true)
const vehiclesError = ref(false)
const showAllVehicles = ref(false)
const query = ref('')
const arrivals = ref({})
const favoriteArrivals = ref({})
@@ -122,6 +134,8 @@ const locationUpdatedAt = ref(0)
let nearbyCandidates = []
let refreshTimer
let locationRequest = 0
let vehicleRequest = 0
let staticLoaded = false
const LOCATION_CACHE_KEY = 'sustech-bus-v2-location'

const searchResults = computed(() => [
@@ -154,11 +168,14 @@ const noticeScope = (notice) => notice.route_id ? 'route' : notice.stop_id ? 'st
const noticeTitle = (notice) => notice[busLanguage.value === 'en' ? 'title_en' : 'title_zh'] || notice.title_zh || notice.title_en
const noticeTime = (notice) => formatLocalDateTime(notice.starts_at)
const arrivalName = (arrival) => `${arrival[busLanguage.value === 'en' ? 'route_name_en' : 'route_name_zh'] || arrival.route_name_zh || arrival.route_name_en || ''} · ${arrival[busLanguage.value === 'en' ? 'direction_name_en' : 'direction_name_zh'] || arrival.direction_name_zh || arrival.direction_name_en || ''}`
const vehicleIcon = (arrival) => String(arrival.vehicle_type).toUpperCase() === 'SHUTTLE' ? '/sev.png' : '/bus.png'
const vehicleLabel = (arrival) => String(arrival.vehicle_type).toUpperCase() === 'SHUTTLE' ? (busLanguage.value === 'zh' ? '电瓶车' : 'EV Shuttle') : (busLanguage.value === 'zh' ? '巴士' : 'Bus')
const showVehicleIcon = (arrival) => !['LAST_SERVICE_PASSED', 'NOT_OPERATING'].includes(String(arrival.unavailable_reason).toUpperCase())

function arrivalText(arrival) {
  if (arrival.source === 'real_time') {
    const meters = Number(arrival.distance ?? arrival.distance_to_stop ?? arrival.distance_to_next_stop ?? arrival.distance_meters)
    return [busLanguage.value === 'zh' ? `${arrival.eta_minutes} 分钟` : `${arrival.eta_minutes} min`, Number.isFinite(meters) && `${Math.round(meters)}m`].filter(Boolean).join(' ')
    return [realtimeArrivalText(arrival, busLanguage.value), Number.isFinite(meters) && `${Math.round(meters)}m`].filter(Boolean).join(' ')
  }
  if (arrival.source === 'planned') {
    const time = new Date(arrival.planned_arrival_at).toLocaleTimeString(busLanguage.value === 'zh' ? 'zh-CN' : 'en', { hour: '2-digit', minute: '2-digit', hour12: false })
@@ -186,9 +203,11 @@ async function loadArrivals(stopList, target = arrivals) {
}

async function refresh() {
  const position = typeof window === 'undefined' ? null : [window.scrollX, window.scrollY]
  refreshRemaining.value = 30
  await load()
  await load(true)
  if (nearbyCandidates.length) await loadArrivals(nearbyCandidates)
  if (position) window.scrollTo(position[0], position[1])
}

function saveLocation(coords, updatedAt) {
@@ -244,18 +263,33 @@ function locate() {
  geolocation.getCurrentPosition((position) => located(position, false), failed, { enableHighAccuracy: false, timeout: 10000, maximumAge: 60000 })
}

async function load() {
  loading.value = true
  error.value = ''
async function load(refreshing = false) {
  const vehicleLoad = loadVehicles(refreshing)
  if (!refreshing) { loading.value = true; error.value = '' }
  try {
    const [routeData, stopData, noticeData] = await Promise.all([busApi.routes(), busApi.stops(), busApi.notices()])
    routes.value = Array.isArray(routeData) ? routeData : []
    stops.value = Array.isArray(stopData) ? stopData : []
    if (!staticLoaded) await loadStatic()
    const noticeData = await busApi.notices()
    notices.value = (Array.isArray(noticeData) ? noticeData : []).sort((left, right) => right.priority - left.priority)
    await loadArrivals(favoriteStops.value, favoriteArrivals)
  } catch (reason) { error.value = reason.message || String(reason) } finally { loading.value = false }
  } catch (reason) { if (!refreshing) error.value = reason.message || String(reason) } finally { if (!refreshing) loading.value = false }
  await vehicleLoad
}

async function loadStatic() {
  const [routeData, stopData] = await Promise.all([busApi.routes(), busApi.stops()])
  routes.value = Array.isArray(routeData) ? routeData : []
  stops.value = Array.isArray(stopData) ? stopData : []
  staticLoaded = true
}

async function loadVehicles(refreshing = false) {
  const request = ++vehicleRequest
  if (!refreshing) { vehiclesLoading.value = true; vehiclesError.value = false }
  try { const result = await busApi.vehicles(showAllVehicles.value); if (request === vehicleRequest) vehicles.value = Array.isArray(result) ? result : [] } catch { if (request === vehicleRequest && !refreshing) vehiclesError.value = true } finally { if (request === vehicleRequest && !refreshing) vehiclesLoading.value = false }
}

function setShowAllVehicles(value) { showAllVehicles.value = value; loadVehicles() }

onMounted(async () => { loadFavorites(); await load(); useCachedLocation(); refreshTimer = setInterval(() => { if (--refreshRemaining.value < 1) refresh() }, 1000) })
onBeforeUnmount(() => clearInterval(refreshTimer))
defineExpose({ load, locate, refresh, openRoute, openStop, favoriteRouteIds, favoriteStopIds })
@@ -274,7 +308,7 @@ defineExpose({ load, locate, refresh, openRoute, openStop, favoriteRouteIds, fav
.bus-home__header { margin: .5rem 0 .75rem; } .bus-home__header h1, .panel h3 { margin: 0; padding-top: 0; } .bus-home__header h1 { font-size: 1.4rem; line-height: 1.25; } .bus-home__header p, .muted { color: var(--bus-v2-muted); }
.panel h3 { font-size: 1rem; line-height: 1.3; }
.panel { margin: 1rem 0; padding: 1rem; border: 1px solid var(--bus-v2-border); border-radius: .6rem; background: var(--bus-v2-bg); }
.search { position: relative; padding: 0; } .search input { box-sizing: border-box; width: 100%; padding: .85rem 1rem; border: 0; border-radius: .6rem; font: inherit; background: transparent; color: inherit; }
.search { position: relative; padding: 0; } .search input { box-sizing: border-box; width: 100%; padding: .85rem 2.5rem .85rem 1rem; border: 0; border-radius: .6rem; font: inherit; background: transparent; color: inherit; } .search-clear { position: absolute; top: 40%; right: .65rem; border: 0; padding: 0; transform: translateY(-50%); background: transparent; color: var(--bus-v2-muted); font-size: 1.4rem; line-height: 1; }
.search-results { position: absolute; z-index: 2; top: calc(100% + .25rem); width: 100%; overflow: hidden; border: 1px solid var(--bus-v2-border); border-radius: .5rem; background: var(--bus-v2-bg); box-shadow: 0 .5rem 1rem var(--vp-c-shadow); }
.search-results button, .quick-links button { display: block; width: 100%; padding: .65rem 1rem; border: 0; text-align: left; background: transparent; color: inherit; cursor: pointer; } .search-results button:hover, .quick-links button:hover { background: var(--bus-v2-control); }
.search-results small, summary small { margin-right: .5rem; color: var(--bus-v2-muted); } .status { display: flex; align-items: center; gap: .5rem; } .error { color: #b42318; } button { font: inherit; cursor: pointer; } button:disabled { cursor: wait; opacity: .6; }
@@ -284,9 +318,9 @@ details + details { border-top: 1px solid var(--bus-v2-border); } summary { disp
.nearby .section-title button { border-color: var(--bus-v2-link-soft); background: var(--bus-v2-link-soft); color: var(--bus-v2-link); font-weight: 600; }
.nearby-stop { padding: .75rem 0; border-top: 1px solid var(--bus-v2-border); } .nearby-stop__head { align-items: baseline; } .link-title { padding: 0; border: 0; background: transparent; color: var(--bus-v2-link); font-weight: 700; text-align: left; }
.nearby-toggle { padding: .4rem .65rem; border: 1px solid var(--bus-v2-border); border-radius: .35rem; background: transparent; color: inherit; font: inherit; }
.arrival-list { margin: .45rem 0 0; padding: 0; list-style: none; } .arrival-list li { display: grid; grid-template-columns: .35rem minmax(0, 1fr) auto; gap: .4rem; align-items: center; padding: .3rem 0; } .arrival-list i { width: .3rem; height: 1.2rem; border-radius: 2px; } .arrival-link { color: var(--bus-v2-link); text-decoration: none; } .arrival-link:hover, .arrival-link:focus-visible { text-decoration: underline; } .arrival-list .muted { display: block; }
.arrival-list { margin: .45rem 0 0; padding: 0; list-style: none; } .arrival-list li { display: grid; grid-template-columns: .35rem minmax(0, 1fr) auto 1rem; gap: .4rem; align-items: center; padding: .3rem 0; } .arrival-list i { width: .3rem; height: 1.2rem; border-radius: 2px; } .arrival-vehicle { box-sizing: border-box; width: 1rem; height: 1rem; padding: 1px; border-radius: 50%; background: #fff; } .arrival-link { color: var(--bus-v2-link); text-decoration: none; } .arrival-link:hover, .arrival-link:focus-visible { text-decoration: underline; } .arrival-list .muted { display: block; }
.quick-links { display: flex; flex-wrap: wrap; gap: .5rem; } .quick-links button { width: auto; border: 1px solid var(--bus-v2-border); border-radius: .35rem; }
.feature-links { display: grid; grid-template-columns: repeat(3, 1fr); gap: .75rem; } .feature-links a { display: flex; min-height: 4rem; align-items: center; justify-content: center; padding: 0 .75rem; border-radius: .6rem; background: var(--bus-v2-link-soft); color: var(--bus-v2-link); text-align: center; text-decoration: none; font-weight: 600; }
.feature-links { display: grid; grid-template-columns: repeat(2, 1fr); gap: .75rem; } .feature-links a { display: flex; min-height: 4rem; align-items: center; justify-content: center; padding: 0 .75rem; border-radius: .6rem; background: var(--bus-v2-link-soft); color: var(--bus-v2-link); text-align: center; text-decoration: none; font-weight: 600; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); }
@media (max-width: 560px) { .panel { margin: .75rem 0; } .feature-links { grid-template-columns: 1fr; } }
</style>
+18 −7
Original line number Diff line number Diff line
@@ -3,13 +3,14 @@
    <div ref="mapElement" class="bus-map__canvas" :aria-label="language === 'zh' ? '车辆地图' : 'Vehicle map'" />
    <p v-if="mapError" class="bus-map__message" role="alert">{{ mapError }}</p>
    <p v-else-if="loading" class="bus-map__message">{{ language === 'zh' ? '正在加载地图…' : 'Loading map…' }}</p>
    <div class="bus-map__legend"><span><i class="bus" />{{ language === 'zh' ? '巴士' : 'Bus' }}</span><span><i class="shuttle" />{{ language === 'zh' ? '电瓶车' : 'EV Shuttle' }}</span></div>
    <BusVehicleLegendV2 class="bus-map__legend" :language="language" />
  </section>
</template>

<script setup>
import { createApp, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import BusVehicleDetailV2 from './BusVehicleDetailV2.vue'
import BusVehicleLegendV2 from './BusVehicleLegendV2.vue'
import { parseGeometry } from './bus-v2-helpers.mjs'
import { displayName, displayStopName } from './core.mjs'

@@ -25,6 +26,7 @@ const props = defineProps({
  stops: { type: Array, default: () => [] },
  routeId: { type: String, default: '' },
  language: { type: String, default: 'zh' },
  neutral: Boolean,
  // An explicit style remains supported for deployments that host their own PMTiles style.
  styleUrl: { type: String, default: '' },
})
@@ -50,13 +52,15 @@ const activeRoutes = () => props.routes.filter((route) => !props.routeId || rout
const activeVehicles = () => props.vehicles.filter((vehicle) => (!props.routeId || vehicle.route_id === props.routeId) && Number.isFinite(+vehicle.longitude) && Number.isFinite(+vehicle.latitude))
const routeFor = (id) => props.routes.find((route) => route.id === id)
const darkTheme = () => document.documentElement.getAttribute('data-theme') === 'dark' || mediaQuery?.matches
const neutralRouteColor = () => darkTheme() ? '#aaa' : '#666'
const neutralStopColor = () => darkTheme() ? '#ccc' : '#444'
const styleUrl = () => props.styleUrl || (darkTheme() ? DARK_STYLE : LIGHT_STYLE)
const sourceData = (features) => ({ type: 'FeatureCollection', features })

function routeFeatures() {
  return activeRoutes().flatMap((route) => (route.directions || []).map((direction) => ({
    type: 'Feature',
    properties: { color: route.color || '#2878c8' },
    properties: { color: props.neutral ? neutralRouteColor() : route.color || '#2878c8' },
    geometry: { type: 'LineString', coordinates: parseGeometry(direction.geometry_json) },
  })).filter((feature) => feature.geometry.coordinates.length > 1))
}
@@ -76,7 +80,7 @@ function stopFeatures() {
    }
    stops.set(key, {
      type: 'Feature',
      properties: { names: name ? [name] : [], color: route.color || '#2878c8' },
      properties: { names: name ? [name] : [], color: props.neutral ? neutralStopColor() : route.color || '#2878c8' },
      geometry: { type: 'Point', coordinates: [longitude, latitude] },
    })
  })))
@@ -224,8 +228,14 @@ function releaseProtocol() {
function reloadStyle() {
  if (!map || !loaded) return
  activePopup?.remove()
  map.once('style.load', () => requestAnimationFrame(() => { if (map?.isStyleLoaded()) { addLayers(); refresh() } }))
  map.setStyle(styleUrl())
  map.once('idle', restoreOverlays)
  map.setStyle(styleUrl(), { diff: false })
}

function restoreOverlays() {
  if (!map?.isStyleLoaded()) return
  addLayers()
  refresh()
}

async function initialise() {
@@ -239,6 +249,7 @@ async function initialise() {
    map.addControl(new maplibregl.FullscreenControl(), 'top-left')
    map.addControl(createInteractionLockControl(), 'top-left')
    map.addControl(new maplibregl.GeolocateControl({ positionOptions: { enableHighAccuracy: true }, trackUserLocation: true, showUserHeading: true }), 'top-right')
    map.on('style.load', restoreOverlays)
    themeChangeHandler = () => { if (!props.styleUrl) reloadStyle() }
    mediaQuery.addEventListener('change', themeChangeHandler)
    themeObserver = new MutationObserver(themeChangeHandler)
@@ -284,8 +295,8 @@ defineExpose({ refresh, refreshVehicleMarkers })
.bus-map { position: relative; min-height: 22rem; overflow: hidden; border: 1px solid #d9e2ec; border-radius: .6rem; background: #eef4f8; }
.bus-map__canvas { width: 100%; height: 28rem; }
.bus-map__message { position: absolute; top: .75rem; left: .75rem; z-index: 1; margin: 0; padding: .45rem .65rem; border-radius: .35rem; background: rgba(255, 255, 255, .9); color: #526172; }
.bus-map__legend { position: absolute; z-index: 1; bottom: 1.75rem; left: .75rem; display: flex; flex-wrap: wrap; gap: .6rem; padding: .4rem .55rem; border-radius: .35rem; background: color-mix(in srgb, var(--bus-v2-bg, #fff) 90%, transparent); color: var(--bus-v2-text, #243043); font-size: .82rem; }.bus-map__legend span { display: inline-flex; align-items: center; gap: .25rem; white-space: nowrap; }.bus-map__legend i { width: 1rem; height: 1rem; border: 1px solid var(--bus-v2-link, #2878c8); border-radius: 50%; background: var(--bus-v2-link, #2878c8) url('/bus.png') center / contain no-repeat; }.bus-map__legend i.shuttle { background-image: url('/sev.png'); }
.bus-map :deep(.bus-map__vehicle) { width: 2rem; height: 2rem; border: 2px solid var(--route-color); border-radius: 50%; padding: 0; background-color: var(--route-color); background-position: center; background-repeat: no-repeat; background-size: contain; cursor: pointer; box-shadow: 0 0 0 2px var(--route-color), 0 1px 4px rgba(0, 0, 0, .35); }
.bus-map__legend { position: absolute; z-index: 1; bottom: 1.75rem; left: .75rem; margin: 0; padding: .4rem .55rem; border-radius: .35rem; background: color-mix(in srgb, var(--bus-v2-bg, #fff) 90%, transparent); }
.bus-map :deep(.bus-map__vehicle) { width: 1.4rem; height: 1.4rem; border: 2px solid var(--route-color); border-radius: 50%; padding: 0; background-color: var(--route-color); background-position: center; background-repeat: no-repeat; background-size: contain; cursor: pointer; box-shadow: 0 0 0 2px var(--route-color), 0 1px 4px rgba(0, 0, 0, .35); }
.bus-map :deep(.bus-map__vehicle.status-delayed) { background-color: #f7a600; }
.bus-map :deep(.bus-map__vehicle.status-offline) { background-color: #9aa4b2; }
.bus-map :deep(.bus-map__interaction-lock), .bus-map :deep(.bus-map__interaction-allow) { background-image: none; font-size: 1rem; }
+11 −9

File changed.

Preview size limit exceeded, changes collapsed.

+34 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading