Loading docs/.vuepress/components/bus-v2/BusHomeV2.vue +4 −4 Changes for docs/.vuepress/components/bus-v2/BusHomeV2.vue: 4 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -50,7 +50,7 @@ <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 && !favoriteArrivals[stop.id]?.items?.length" class="muted">{{ busText('loading') }}</p> <p v-else-if="favoriteArrivals[stop.id]?.error && !favoriteArrivals[stop.id]?.items?.length" class="muted">{{ busText('unavailable') }}</p> <ul v-else class="arrival-list"><li v-for="(arrival, index) in favoriteArrivals[stop.id]?.items" :key="index"><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> <ul v-else class="arrival-list"><li v-for="(arrival, index) in favoriteArrivals[stop.id]?.items" :key="index"><i :style="{ background: arrival.route_color || '#2878c8' }" /><a class="arrival-link" :href="routeDirectionHref(arrival)">{{ arrivalName(arrival) }}</a><span>{{ arrivalText(arrival) }}</span><BusVehicleIconV2 v-if="showVehicleIcon(arrival)" :type="arrival.vehicle_type" role="img" :aria-label="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> Loading @@ -76,7 +76,7 @@ <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)"> <BusVehicleIconV2 v-if="showVehicleIcon(arrival)" :type="arrival.vehicle_type" role="img" :aria-label="vehicleLabel(arrival)" /> </li> <li v-if="!arrivals[stop.id]?.items?.length" class="muted">{{ busText('empty') }}</li> </ul> Loading @@ -96,6 +96,7 @@ </template> <script setup> import BusVehicleIconV2 from './BusVehicleIconV2.vue' import { computed, onBeforeUnmount, onMounted, ref } from 'vue' import { busApi } from './api.mjs' import { displayName, displayStopName, formatDistance, formatLocalDateTime, haversineMeters, isTerminalArrival, matchesSearch, realtimeArrivalText, sortArrivalsByEstimatedTime, unavailableReasonTextKey } from './core.mjs' Loading Loading @@ -173,7 +174,6 @@ const noticeTime = (notice) => formatLocalDateTime(notice.starts_at) const isNoticeOpen = (notice, defaultOpen = false) => noticeIsOpen(noticeOpenStates.value, notice.id, defaultOpen) const rememberNoticeOpen = (notice, event) => saveNoticeOpenState(noticeOpenStates.value, notice.id, event.target.open) 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()) Loading Loading @@ -323,7 +323,7 @@ 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 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; } .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-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(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); } Loading docs/.vuepress/components/bus-v2/BusRouteV2.vue +4 −4 Changes for docs/.vuepress/components/bus-v2/BusRouteV2.vue: 4 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -59,7 +59,7 @@ <p v-if="!currentDirection?.stops?.length" class="muted">{{ busText('empty') }}</p> <ol v-else> <li v-for="stop in currentDirection.stops" :key="stop.id" :class="{ selected: stop.id === selectedStopId }"> <div class="stop-line"><i aria-hidden="true" /><div v-if="atStop(stop).length || betweenStop(stop).length" class="vehicle-markers"><button v-if="atStop(stop).length" type="button" class="vehicle-marker at" :aria-expanded="expandedVehicleKey === `at-${stop.id}`" @click="toggleVehicles(`at-${stop.id}`)"><b v-if="atStop(stop).length > 1">{{ atStop(stop).length }}</b><i aria-hidden="true" class="vehicle-icon" :class="{ shuttle: String(atStop(stop)[0]?.vehicle_type).toUpperCase() === 'SHUTTLE' }" /><em v-if="hasSpecialService(atStop(stop))">S</em></button><button v-if="betweenStop(stop).length" type="button" class="vehicle-marker between" :aria-expanded="expandedVehicleKey === `between-${stop.id}`" @click="toggleVehicles(`between-${stop.id}`)"><b v-if="betweenStop(stop).length > 1">{{ betweenStop(stop).length }}</b><i aria-hidden="true" class="vehicle-icon" :class="{ shuttle: String(betweenStop(stop)[0]?.vehicle_type).toUpperCase() === 'SHUTTLE' }" /><em v-if="hasSpecialService(betweenStop(stop))">S</em></button></div></div> <div class="stop-line"><i aria-hidden="true" /><div v-if="atStop(stop).length || betweenStop(stop).length" class="vehicle-markers"><button v-if="atStop(stop).length" type="button" class="vehicle-marker at" :aria-expanded="expandedVehicleKey === `at-${stop.id}`" @click="toggleVehicles(`at-${stop.id}`)"><b v-if="atStop(stop).length > 1">{{ atStop(stop).length }}</b><BusVehicleIconV2 aria-hidden="true" class="vehicle-icon" :type="atStop(stop)[0]?.vehicle_type" /><em v-if="hasSpecialService(atStop(stop))">S</em></button><button v-if="betweenStop(stop).length" type="button" class="vehicle-marker between" :aria-expanded="expandedVehicleKey === `between-${stop.id}`" @click="toggleVehicles(`between-${stop.id}`)"><b v-if="betweenStop(stop).length > 1">{{ betweenStop(stop).length }}</b><BusVehicleIconV2 aria-hidden="true" class="vehicle-icon" :type="betweenStop(stop)[0]?.vehicle_type" /><em v-if="hasSpecialService(betweenStop(stop))">S</em></button></div></div> <div class="stop-main"><a :href="`${stopHref}${encodeURIComponent(stop.id)}`" @click.prevent="selectStop(stop.id)"><strong>{{ routeStopName(stop) }}</strong></a></div> <div v-if="expandedVehicleKey === `at-${stop.id}` || expandedVehicleKey === `between-${stop.id}`" class="vehicle-details"> <BusVehicleDetailV2 v-for="vehicle in expandedVehicleKey === `at-${stop.id}` ? atStop(stop) : betweenStop(stop)" :key="vehicle.id" :vehicle="vehicle" :routes="[route]" :stops="stops" :language="busLanguage" /> Loading @@ -78,6 +78,7 @@ </template> <script setup> import BusVehicleIconV2 from './BusVehicleIconV2.vue' import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { busApi, publicApi } from './api.mjs' import { displayName, displayStopName, formatLocalDateTime, realtimeArrivalText, serviceTypeLabel, unavailableReasonTextKey } from './core.mjs' Loading Loading @@ -154,15 +155,14 @@ defineExpose({ load, refresh, selectDirection, selectStop }) .notices summary time { color: #687386; font-size: .8rem; font-weight: 400; white-space: nowrap; } .bus-detail { max-width: 900px; margin: 0 auto; color: var(--c-text, #243043); } .detail-head, .head-actions, .section-head, .route-summary { display: flex; align-items: center; gap: .75rem; } .detail-head, .section-head { justify-content: space-between; } .detail-head h1, .panel h3, .map-slot > h3 { margin: 0; padding-top: 0; } .detail-head h1 { font-size: 1.4rem; line-height: 1.25; } p { margin-top: 0; } .panel h3, .map-slot > h3 { font-size: 1rem; line-height: 1.3; } .eyebrow { margin-bottom: .2rem; color: #687386; font-size: .82rem; } .head-actions { align-self: flex-start; } .plain-button { border: 1px solid #aec5dc; border-radius: .35rem; padding: .4rem .65rem; background: transparent; color: inherit; font: inherit; cursor: pointer; } .panel { margin: 1rem 0; padding: 1rem; border: 1px solid var(--c-border, #dce2ea); border-radius: .6rem; background: var(--c-bg-soft, #fff); } .status { display: flex; align-items: center; gap: .5rem; } .error { color: #a32727; } .spinner { width: 1em; height: 1em; border: 2px solid #b7c9dd; border-top-color: #2672bc; border-radius: 50%; animation: spin .8s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } .route-summary { flex-wrap: wrap; padding-top: .5rem; padding-bottom: .5rem; border-left: .5rem solid var(--route-color); justify-content: space-between; } .route-summary div { display: flex; flex-wrap: wrap; min-width: 0; align-items: baseline; gap: .5rem; } .route-swatch { width: .9rem; height: .9rem; border-radius: 50%; background: var(--route-color); } .direction-tabs { display: flex; flex-wrap: wrap; gap: .5rem; margin: 1rem 0; } .direction-tabs button { border: 1px solid #cbd9e7; border-radius: 999px; padding: .45rem .8rem; background: transparent; color: inherit; font: inherit; cursor: pointer; } .direction-tabs .active { border-color: var(--c-brand, #2878c8); background: #eaf3fc; color: #175f9f; font-weight: 700; } .muted { color: #687386; } .selected-stop-title, .arrival-list li { display: flex; flex-wrap: wrap; align-items: baseline; gap: .2rem .5rem; } .selected-stop-title > *, .arrival-list li > * { white-space: nowrap; } .arrival-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .5rem; margin: .5rem 0; padding: 0; list-style: none; } .arrival-list li { padding: .55rem; border: 1px solid #e7edf4; border-radius: .35rem; } .arrival-list small { color: #687386; } .all-link { display: inline-block; margin-top: .75rem; color: #1765ac; } details + details { border-top: 1px solid #e7edf4; } summary { padding: .65rem 0; cursor: pointer; font-weight: 600; } .markdown { margin: 1rem 0 0; padding-bottom: .25rem; } .markdown :deep(p) { margin: 1rem 0 0; } .markdown :deep(:first-child) { margin-top: 0; } .markdown :deep(:last-child) { margin-bottom: 0; } .route-stops { max-height: 18rem; overflow-y: auto; } .route-stops ol { margin: 0; padding: 0; list-style: none; } .route-stops li { position: relative; display: grid; grid-template-columns: 1.6rem minmax(0, 1fr); min-height: 2.25rem; } .stop-line { position: relative; display: flex; justify-content: center; } .stop-line::before { content: ''; position: absolute; z-index: 0; top: 0; bottom: 0; width: 4px; background: var(--route-color); } .stop-line > i { z-index: 1; width: .75rem; height: .75rem; margin-top: .65rem; border: 3px solid var(--route-color); border-radius: 50%; background: #fff; } .selected .stop-line > i { background: var(--route-color); box-shadow: 0 0 0 4px color-mix(in srgb, var(--route-color) 25%, transparent); } .stop-main { padding: .25rem .25rem .25rem .5rem; } .stop-main a { color: inherit; font: inherit; text-decoration: none; } .stop-main a:hover, .stop-main a:focus-visible { color: var(--route-color); text-decoration: underline; } .selected .stop-main strong { color: var(--route-color); } .vehicle-markers { position: absolute; z-index: 2; inset: 0; pointer-events: none; } .vehicle-marker { position: absolute; left: 50%; transform: translateX(-50%); border: 0; border-radius: 999px; padding: .2rem .3rem; background: #e8f2fb; font: inherit; font-size: .78rem; cursor: pointer; pointer-events: auto; white-space: nowrap; } .vehicle-marker.at { top: .25rem; } .vehicle-marker.between { top: 2.1rem; background: #fff4d9; } .vehicle-marker em { margin-left: .1rem; color: #9a5200; font-size: .7rem; font-style: normal; font-weight: 800; } .vehicle-marker b { margin-left: .15rem; } .vehicle-details { display: grid; grid-column: 2; gap: .4rem; margin: 0 .5rem .6rem; } .vehicle-details :deep(.vehicle-detail) { box-sizing: border-box; min-width: 0; box-shadow: none; } .map-slot { margin: 1rem 0; } @media (max-width: 560px) { .detail-head { align-items: flex-start; } .head-actions { flex-wrap: wrap; justify-content: flex-end; } .panel { margin: .75rem 0; } .arrival-list { grid-template-columns: 1fr; } } .route-stops .stop-line > i { background: var(--bus-v2-bg); } .route-stops .vehicle-marker { transform: translate(-63%, -50%); background: var(--bus-v2-link-soft); color: var(--bus-v2-text); } .route-stops .vehicle-marker { transform: translate(-64%, -50%); background: var(--bus-v2-link-soft); color: var(--bus-v2-text); } .route-stops .vehicle-marker.at { top: 1rem; } .route-stops .vehicle-marker.between { top: -0.1rem; background: color-mix(in srgb, #f7a600 18%, var(--bus-v2-bg)); } .route-stops .vehicle-marker { gap: .1rem; padding: 0; background: transparent; } .route-stops .vehicle-marker { display: flex; align-items: center; } .route-stops .vehicle-marker b { margin: 0 .15rem 0 0; } .route-stops .vehicle-marker.between { background: transparent; } .route-stops .vehicle-icon { z-index: auto; display: block; width: 1.5rem; height: 1.5rem; margin: 0; border-radius: 50%; background: #fff url('/bus.png') center / calc(100% - 2px) no-repeat; } .route-stops .vehicle-icon.shuttle { background-image: url('/sev.png'); } .route-stops .vehicle-icon { display: block; width: 1.2rem; height: 1.2rem; } .head-actions { gap: .3rem; } .plain-button, .selected-stop-title a { color: inherit; text-decoration: none; } .selected-stop-title a:hover, .selected-stop-title a:focus-visible { color: var(--bus-v2-link); text-decoration: underline; } Loading docs/.vuepress/components/bus-v2/BusStopArrivalsV2.vue +2 −3 Changes for docs/.vuepress/components/bus-v2/BusStopArrivalsV2.vue: 2 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -2,10 +2,11 @@ <p v-if="props.state?.loading && !props.state?.items?.length" class="muted">{{ busText('loading') }}</p> <p v-else-if="props.state?.error && !props.state?.items?.length" class="muted">{{ busText('unavailable') }}</p> <p v-else-if="!items.length" class="muted">{{ busText('empty') }}</p> <ul v-else class="arrival-list"><li v-for="(arrival, index) in items" :key="index"><i :style="{ background: arrival.route_color || '#2878c8' }" /><a :href="routeDirectionHref(arrival)"><strong>{{ routeName(arrival) }}</strong><small>{{ directionName(arrival) }}</small></a><span>{{ arrivalText(arrival) }}<small v-if="arrivalMeta(arrival)">{{ arrivalMeta(arrival) }}</small></span><img v-if="showVehicleIcon(arrival)" :src="vehicleIcon(arrival)" :alt="vehicleLabel(arrival)"></li></ul> <ul v-else class="arrival-list"><li v-for="(arrival, index) in items" :key="index"><i :style="{ background: arrival.route_color || '#2878c8' }" /><a :href="routeDirectionHref(arrival)"><strong>{{ routeName(arrival) }}</strong><small>{{ directionName(arrival) }}</small></a><span>{{ arrivalText(arrival) }}<small v-if="arrivalMeta(arrival)">{{ arrivalMeta(arrival) }}</small></span><BusVehicleIconV2 v-if="showVehicleIcon(arrival)" :type="arrival.vehicle_type" role="img" :aria-label="vehicleLabel(arrival)" /></li></ul> </template> <script setup> import BusVehicleIconV2 from './BusVehicleIconV2.vue' import { computed } from 'vue' import { closestArrivalsByRoute, realtimeArrivalText, unavailableReasonTextKey } from './core.mjs' import { busLanguage, busText } from './i18n.mjs' Loading @@ -16,7 +17,6 @@ const routeName = (item) => item[busLanguage.value === 'en' ? 'route_name_en' : const directionName = (item) => item[busLanguage.value === 'en' ? 'direction_name_en' : 'direction_name_zh'] || item.direction_name_zh || item.direction_name_en || '' const arrivalKey = (item) => `${item.route_direction_id}-${item.trip_id || item.planned_arrival_at || item.updated_at || item.eta_minutes}` const routeDirectionHref = (item) => `${props.routeHref}${encodeURIComponent(item.route_id)}&direction=${encodeURIComponent(item.route_direction_id)}` const vehicleIcon = (item) => String(item.vehicle_type).toUpperCase() === 'SHUTTLE' ? '/sev.png' : '/bus.png' const vehicleLabel = (item) => String(item.vehicle_type).toUpperCase() === 'SHUTTLE' ? (busLanguage.value === 'zh' ? '电瓶车' : 'EV Shuttle') : (busLanguage.value === 'zh' ? '巴士' : 'Bus') const showVehicleIcon = (item) => !['LAST_SERVICE_PASSED', 'NOT_OPERATING'].includes(String(item.unavailable_reason).toUpperCase()) function time(value) { return value ? new Date(value).toLocaleTimeString(busLanguage.value === 'zh' ? 'zh-CN' : 'en', { hour: '2-digit', minute: '2-digit', hour12: false }) : '' } Loading @@ -34,5 +34,4 @@ function arrivalMeta(item) { return item.source === 'real_time' ? `${busLanguage .arrival-list a:hover, .arrival-list a:focus-visible { text-decoration: underline; } .arrival-list small { color: var(--bus-v2-muted); } .arrival-list span { display: grid; justify-items: end; gap: .15rem; text-align: right; } .arrival-list img { box-sizing: border-box; width: 1rem; height: 1rem; padding: 1px; border-radius: 50%; background: #fff; } </style> docs/.vuepress/components/bus-v2/BusVehicleIconV2.vue 0 → 100644 +12 −0 Changes for docs/.vuepress/components/bus-v2/BusVehicleIconV2.vue: 12 added lines, 0 removed lines. Original line number Diff line number Diff line <template> <svg class="bus-vehicle-icon" :class="{ shuttle: String(type).toUpperCase() === 'SHUTTLE' }" viewBox="0 0 16 16"><circle cx="8" cy="8" r="7.5" /></svg> </template> <script setup> defineProps({ type: String }) </script> <style scoped> .bus-vehicle-icon { display: inline-block; flex-shrink: 0; width: .8rem; height: .8rem; fill: var(--bus-v2-bus-fill); stroke: #925a00; } .bus-vehicle-icon.shuttle { fill: var(--bus-v2-shuttle-fill); stroke: #28633f; } </style> docs/.vuepress/components/bus-v2/BusVehicleLegendV2.vue +3 −3 Changes for docs/.vuepress/components/bus-v2/BusVehicleLegendV2.vue: 3 added lines, 3 removed lines. Original line number Diff line number Diff line <template> <div class="vehicle-legend" :aria-label="language === 'zh' ? '车型图例' : 'Vehicle legend'"> <span><i v-if="textMarkers" class="vehicle-legend__marker" :style="{ color: vehicleTextColor('BUS') }" aria-hidden="true">1</i><img v-else src="/bus.png" alt="">{{ language === 'zh' ? '巴士' : 'Bus' }}</span> <span><i v-if="textMarkers" class="vehicle-legend__marker shuttle" :style="{ color: vehicleTextColor('SHUTTLE') }" aria-hidden="true">1</i><img v-else src="/sev.png" alt="">{{ language === 'zh' ? '电瓶车' : 'EV Shuttle' }}</span> <span><i v-if="textMarkers" class="vehicle-legend__marker" :style="{ color: vehicleTextColor('BUS') }" aria-hidden="true">1</i><BusVehicleIconV2 v-else type="BUS" aria-hidden="true" />{{ language === 'zh' ? '巴士' : 'Bus' }}</span> <span><i v-if="textMarkers" class="vehicle-legend__marker shuttle" :style="{ color: vehicleTextColor('SHUTTLE') }" aria-hidden="true">1</i><BusVehicleIconV2 v-else type="SHUTTLE" aria-hidden="true" />{{ language === 'zh' ? '电瓶车' : 'EV Shuttle' }}</span> </div> </template> <script setup> import BusVehicleIconV2 from './BusVehicleIconV2.vue' import { vehicleTextColor } from './core.mjs' defineProps({ language: { type: String, default: 'zh' }, textMarkers: Boolean }) </script> <style scoped> .vehicle-legend { display: flex; flex-wrap: wrap; gap: .6rem; margin-top: .5rem; color: var(--bus-v2-muted); font-size: .82rem; }.vehicle-legend span { display: inline-flex; align-items: center; gap: .25rem; white-space: nowrap; } .vehicle-legend img { box-sizing: border-box; width: 1rem; height: 1rem; padding: 1px; border-radius: 50%; background: #fff; } .vehicle-legend__marker { display: inline-flex; align-items: center; justify-content: center; width: 1.25rem; height: 1.25rem; border-radius: 50%; background: var(--bus-v2-bus-fill); font: 700 .75rem/1 "Microsoft YaHei UI", "Microsoft YaHei", "PingFang SC", sans-serif; } .vehicle-legend__marker.shuttle { background: var(--bus-v2-shuttle-fill); } </style> Loading
docs/.vuepress/components/bus-v2/BusHomeV2.vue +4 −4 Changes for docs/.vuepress/components/bus-v2/BusHomeV2.vue: 4 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -50,7 +50,7 @@ <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 && !favoriteArrivals[stop.id]?.items?.length" class="muted">{{ busText('loading') }}</p> <p v-else-if="favoriteArrivals[stop.id]?.error && !favoriteArrivals[stop.id]?.items?.length" class="muted">{{ busText('unavailable') }}</p> <ul v-else class="arrival-list"><li v-for="(arrival, index) in favoriteArrivals[stop.id]?.items" :key="index"><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> <ul v-else class="arrival-list"><li v-for="(arrival, index) in favoriteArrivals[stop.id]?.items" :key="index"><i :style="{ background: arrival.route_color || '#2878c8' }" /><a class="arrival-link" :href="routeDirectionHref(arrival)">{{ arrivalName(arrival) }}</a><span>{{ arrivalText(arrival) }}</span><BusVehicleIconV2 v-if="showVehicleIcon(arrival)" :type="arrival.vehicle_type" role="img" :aria-label="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> Loading @@ -76,7 +76,7 @@ <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)"> <BusVehicleIconV2 v-if="showVehicleIcon(arrival)" :type="arrival.vehicle_type" role="img" :aria-label="vehicleLabel(arrival)" /> </li> <li v-if="!arrivals[stop.id]?.items?.length" class="muted">{{ busText('empty') }}</li> </ul> Loading @@ -96,6 +96,7 @@ </template> <script setup> import BusVehicleIconV2 from './BusVehicleIconV2.vue' import { computed, onBeforeUnmount, onMounted, ref } from 'vue' import { busApi } from './api.mjs' import { displayName, displayStopName, formatDistance, formatLocalDateTime, haversineMeters, isTerminalArrival, matchesSearch, realtimeArrivalText, sortArrivalsByEstimatedTime, unavailableReasonTextKey } from './core.mjs' Loading Loading @@ -173,7 +174,6 @@ const noticeTime = (notice) => formatLocalDateTime(notice.starts_at) const isNoticeOpen = (notice, defaultOpen = false) => noticeIsOpen(noticeOpenStates.value, notice.id, defaultOpen) const rememberNoticeOpen = (notice, event) => saveNoticeOpenState(noticeOpenStates.value, notice.id, event.target.open) 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()) Loading Loading @@ -323,7 +323,7 @@ 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 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; } .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-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(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); } Loading
docs/.vuepress/components/bus-v2/BusRouteV2.vue +4 −4 Changes for docs/.vuepress/components/bus-v2/BusRouteV2.vue: 4 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -59,7 +59,7 @@ <p v-if="!currentDirection?.stops?.length" class="muted">{{ busText('empty') }}</p> <ol v-else> <li v-for="stop in currentDirection.stops" :key="stop.id" :class="{ selected: stop.id === selectedStopId }"> <div class="stop-line"><i aria-hidden="true" /><div v-if="atStop(stop).length || betweenStop(stop).length" class="vehicle-markers"><button v-if="atStop(stop).length" type="button" class="vehicle-marker at" :aria-expanded="expandedVehicleKey === `at-${stop.id}`" @click="toggleVehicles(`at-${stop.id}`)"><b v-if="atStop(stop).length > 1">{{ atStop(stop).length }}</b><i aria-hidden="true" class="vehicle-icon" :class="{ shuttle: String(atStop(stop)[0]?.vehicle_type).toUpperCase() === 'SHUTTLE' }" /><em v-if="hasSpecialService(atStop(stop))">S</em></button><button v-if="betweenStop(stop).length" type="button" class="vehicle-marker between" :aria-expanded="expandedVehicleKey === `between-${stop.id}`" @click="toggleVehicles(`between-${stop.id}`)"><b v-if="betweenStop(stop).length > 1">{{ betweenStop(stop).length }}</b><i aria-hidden="true" class="vehicle-icon" :class="{ shuttle: String(betweenStop(stop)[0]?.vehicle_type).toUpperCase() === 'SHUTTLE' }" /><em v-if="hasSpecialService(betweenStop(stop))">S</em></button></div></div> <div class="stop-line"><i aria-hidden="true" /><div v-if="atStop(stop).length || betweenStop(stop).length" class="vehicle-markers"><button v-if="atStop(stop).length" type="button" class="vehicle-marker at" :aria-expanded="expandedVehicleKey === `at-${stop.id}`" @click="toggleVehicles(`at-${stop.id}`)"><b v-if="atStop(stop).length > 1">{{ atStop(stop).length }}</b><BusVehicleIconV2 aria-hidden="true" class="vehicle-icon" :type="atStop(stop)[0]?.vehicle_type" /><em v-if="hasSpecialService(atStop(stop))">S</em></button><button v-if="betweenStop(stop).length" type="button" class="vehicle-marker between" :aria-expanded="expandedVehicleKey === `between-${stop.id}`" @click="toggleVehicles(`between-${stop.id}`)"><b v-if="betweenStop(stop).length > 1">{{ betweenStop(stop).length }}</b><BusVehicleIconV2 aria-hidden="true" class="vehicle-icon" :type="betweenStop(stop)[0]?.vehicle_type" /><em v-if="hasSpecialService(betweenStop(stop))">S</em></button></div></div> <div class="stop-main"><a :href="`${stopHref}${encodeURIComponent(stop.id)}`" @click.prevent="selectStop(stop.id)"><strong>{{ routeStopName(stop) }}</strong></a></div> <div v-if="expandedVehicleKey === `at-${stop.id}` || expandedVehicleKey === `between-${stop.id}`" class="vehicle-details"> <BusVehicleDetailV2 v-for="vehicle in expandedVehicleKey === `at-${stop.id}` ? atStop(stop) : betweenStop(stop)" :key="vehicle.id" :vehicle="vehicle" :routes="[route]" :stops="stops" :language="busLanguage" /> Loading @@ -78,6 +78,7 @@ </template> <script setup> import BusVehicleIconV2 from './BusVehicleIconV2.vue' import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { busApi, publicApi } from './api.mjs' import { displayName, displayStopName, formatLocalDateTime, realtimeArrivalText, serviceTypeLabel, unavailableReasonTextKey } from './core.mjs' Loading Loading @@ -154,15 +155,14 @@ defineExpose({ load, refresh, selectDirection, selectStop }) .notices summary time { color: #687386; font-size: .8rem; font-weight: 400; white-space: nowrap; } .bus-detail { max-width: 900px; margin: 0 auto; color: var(--c-text, #243043); } .detail-head, .head-actions, .section-head, .route-summary { display: flex; align-items: center; gap: .75rem; } .detail-head, .section-head { justify-content: space-between; } .detail-head h1, .panel h3, .map-slot > h3 { margin: 0; padding-top: 0; } .detail-head h1 { font-size: 1.4rem; line-height: 1.25; } p { margin-top: 0; } .panel h3, .map-slot > h3 { font-size: 1rem; line-height: 1.3; } .eyebrow { margin-bottom: .2rem; color: #687386; font-size: .82rem; } .head-actions { align-self: flex-start; } .plain-button { border: 1px solid #aec5dc; border-radius: .35rem; padding: .4rem .65rem; background: transparent; color: inherit; font: inherit; cursor: pointer; } .panel { margin: 1rem 0; padding: 1rem; border: 1px solid var(--c-border, #dce2ea); border-radius: .6rem; background: var(--c-bg-soft, #fff); } .status { display: flex; align-items: center; gap: .5rem; } .error { color: #a32727; } .spinner { width: 1em; height: 1em; border: 2px solid #b7c9dd; border-top-color: #2672bc; border-radius: 50%; animation: spin .8s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } .route-summary { flex-wrap: wrap; padding-top: .5rem; padding-bottom: .5rem; border-left: .5rem solid var(--route-color); justify-content: space-between; } .route-summary div { display: flex; flex-wrap: wrap; min-width: 0; align-items: baseline; gap: .5rem; } .route-swatch { width: .9rem; height: .9rem; border-radius: 50%; background: var(--route-color); } .direction-tabs { display: flex; flex-wrap: wrap; gap: .5rem; margin: 1rem 0; } .direction-tabs button { border: 1px solid #cbd9e7; border-radius: 999px; padding: .45rem .8rem; background: transparent; color: inherit; font: inherit; cursor: pointer; } .direction-tabs .active { border-color: var(--c-brand, #2878c8); background: #eaf3fc; color: #175f9f; font-weight: 700; } .muted { color: #687386; } .selected-stop-title, .arrival-list li { display: flex; flex-wrap: wrap; align-items: baseline; gap: .2rem .5rem; } .selected-stop-title > *, .arrival-list li > * { white-space: nowrap; } .arrival-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .5rem; margin: .5rem 0; padding: 0; list-style: none; } .arrival-list li { padding: .55rem; border: 1px solid #e7edf4; border-radius: .35rem; } .arrival-list small { color: #687386; } .all-link { display: inline-block; margin-top: .75rem; color: #1765ac; } details + details { border-top: 1px solid #e7edf4; } summary { padding: .65rem 0; cursor: pointer; font-weight: 600; } .markdown { margin: 1rem 0 0; padding-bottom: .25rem; } .markdown :deep(p) { margin: 1rem 0 0; } .markdown :deep(:first-child) { margin-top: 0; } .markdown :deep(:last-child) { margin-bottom: 0; } .route-stops { max-height: 18rem; overflow-y: auto; } .route-stops ol { margin: 0; padding: 0; list-style: none; } .route-stops li { position: relative; display: grid; grid-template-columns: 1.6rem minmax(0, 1fr); min-height: 2.25rem; } .stop-line { position: relative; display: flex; justify-content: center; } .stop-line::before { content: ''; position: absolute; z-index: 0; top: 0; bottom: 0; width: 4px; background: var(--route-color); } .stop-line > i { z-index: 1; width: .75rem; height: .75rem; margin-top: .65rem; border: 3px solid var(--route-color); border-radius: 50%; background: #fff; } .selected .stop-line > i { background: var(--route-color); box-shadow: 0 0 0 4px color-mix(in srgb, var(--route-color) 25%, transparent); } .stop-main { padding: .25rem .25rem .25rem .5rem; } .stop-main a { color: inherit; font: inherit; text-decoration: none; } .stop-main a:hover, .stop-main a:focus-visible { color: var(--route-color); text-decoration: underline; } .selected .stop-main strong { color: var(--route-color); } .vehicle-markers { position: absolute; z-index: 2; inset: 0; pointer-events: none; } .vehicle-marker { position: absolute; left: 50%; transform: translateX(-50%); border: 0; border-radius: 999px; padding: .2rem .3rem; background: #e8f2fb; font: inherit; font-size: .78rem; cursor: pointer; pointer-events: auto; white-space: nowrap; } .vehicle-marker.at { top: .25rem; } .vehicle-marker.between { top: 2.1rem; background: #fff4d9; } .vehicle-marker em { margin-left: .1rem; color: #9a5200; font-size: .7rem; font-style: normal; font-weight: 800; } .vehicle-marker b { margin-left: .15rem; } .vehicle-details { display: grid; grid-column: 2; gap: .4rem; margin: 0 .5rem .6rem; } .vehicle-details :deep(.vehicle-detail) { box-sizing: border-box; min-width: 0; box-shadow: none; } .map-slot { margin: 1rem 0; } @media (max-width: 560px) { .detail-head { align-items: flex-start; } .head-actions { flex-wrap: wrap; justify-content: flex-end; } .panel { margin: .75rem 0; } .arrival-list { grid-template-columns: 1fr; } } .route-stops .stop-line > i { background: var(--bus-v2-bg); } .route-stops .vehicle-marker { transform: translate(-63%, -50%); background: var(--bus-v2-link-soft); color: var(--bus-v2-text); } .route-stops .vehicle-marker { transform: translate(-64%, -50%); background: var(--bus-v2-link-soft); color: var(--bus-v2-text); } .route-stops .vehicle-marker.at { top: 1rem; } .route-stops .vehicle-marker.between { top: -0.1rem; background: color-mix(in srgb, #f7a600 18%, var(--bus-v2-bg)); } .route-stops .vehicle-marker { gap: .1rem; padding: 0; background: transparent; } .route-stops .vehicle-marker { display: flex; align-items: center; } .route-stops .vehicle-marker b { margin: 0 .15rem 0 0; } .route-stops .vehicle-marker.between { background: transparent; } .route-stops .vehicle-icon { z-index: auto; display: block; width: 1.5rem; height: 1.5rem; margin: 0; border-radius: 50%; background: #fff url('/bus.png') center / calc(100% - 2px) no-repeat; } .route-stops .vehicle-icon.shuttle { background-image: url('/sev.png'); } .route-stops .vehicle-icon { display: block; width: 1.2rem; height: 1.2rem; } .head-actions { gap: .3rem; } .plain-button, .selected-stop-title a { color: inherit; text-decoration: none; } .selected-stop-title a:hover, .selected-stop-title a:focus-visible { color: var(--bus-v2-link); text-decoration: underline; } Loading
docs/.vuepress/components/bus-v2/BusStopArrivalsV2.vue +2 −3 Changes for docs/.vuepress/components/bus-v2/BusStopArrivalsV2.vue: 2 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -2,10 +2,11 @@ <p v-if="props.state?.loading && !props.state?.items?.length" class="muted">{{ busText('loading') }}</p> <p v-else-if="props.state?.error && !props.state?.items?.length" class="muted">{{ busText('unavailable') }}</p> <p v-else-if="!items.length" class="muted">{{ busText('empty') }}</p> <ul v-else class="arrival-list"><li v-for="(arrival, index) in items" :key="index"><i :style="{ background: arrival.route_color || '#2878c8' }" /><a :href="routeDirectionHref(arrival)"><strong>{{ routeName(arrival) }}</strong><small>{{ directionName(arrival) }}</small></a><span>{{ arrivalText(arrival) }}<small v-if="arrivalMeta(arrival)">{{ arrivalMeta(arrival) }}</small></span><img v-if="showVehicleIcon(arrival)" :src="vehicleIcon(arrival)" :alt="vehicleLabel(arrival)"></li></ul> <ul v-else class="arrival-list"><li v-for="(arrival, index) in items" :key="index"><i :style="{ background: arrival.route_color || '#2878c8' }" /><a :href="routeDirectionHref(arrival)"><strong>{{ routeName(arrival) }}</strong><small>{{ directionName(arrival) }}</small></a><span>{{ arrivalText(arrival) }}<small v-if="arrivalMeta(arrival)">{{ arrivalMeta(arrival) }}</small></span><BusVehicleIconV2 v-if="showVehicleIcon(arrival)" :type="arrival.vehicle_type" role="img" :aria-label="vehicleLabel(arrival)" /></li></ul> </template> <script setup> import BusVehicleIconV2 from './BusVehicleIconV2.vue' import { computed } from 'vue' import { closestArrivalsByRoute, realtimeArrivalText, unavailableReasonTextKey } from './core.mjs' import { busLanguage, busText } from './i18n.mjs' Loading @@ -16,7 +17,6 @@ const routeName = (item) => item[busLanguage.value === 'en' ? 'route_name_en' : const directionName = (item) => item[busLanguage.value === 'en' ? 'direction_name_en' : 'direction_name_zh'] || item.direction_name_zh || item.direction_name_en || '' const arrivalKey = (item) => `${item.route_direction_id}-${item.trip_id || item.planned_arrival_at || item.updated_at || item.eta_minutes}` const routeDirectionHref = (item) => `${props.routeHref}${encodeURIComponent(item.route_id)}&direction=${encodeURIComponent(item.route_direction_id)}` const vehicleIcon = (item) => String(item.vehicle_type).toUpperCase() === 'SHUTTLE' ? '/sev.png' : '/bus.png' const vehicleLabel = (item) => String(item.vehicle_type).toUpperCase() === 'SHUTTLE' ? (busLanguage.value === 'zh' ? '电瓶车' : 'EV Shuttle') : (busLanguage.value === 'zh' ? '巴士' : 'Bus') const showVehicleIcon = (item) => !['LAST_SERVICE_PASSED', 'NOT_OPERATING'].includes(String(item.unavailable_reason).toUpperCase()) function time(value) { return value ? new Date(value).toLocaleTimeString(busLanguage.value === 'zh' ? 'zh-CN' : 'en', { hour: '2-digit', minute: '2-digit', hour12: false }) : '' } Loading @@ -34,5 +34,4 @@ function arrivalMeta(item) { return item.source === 'real_time' ? `${busLanguage .arrival-list a:hover, .arrival-list a:focus-visible { text-decoration: underline; } .arrival-list small { color: var(--bus-v2-muted); } .arrival-list span { display: grid; justify-items: end; gap: .15rem; text-align: right; } .arrival-list img { box-sizing: border-box; width: 1rem; height: 1rem; padding: 1px; border-radius: 50%; background: #fff; } </style>
docs/.vuepress/components/bus-v2/BusVehicleIconV2.vue 0 → 100644 +12 −0 Changes for docs/.vuepress/components/bus-v2/BusVehicleIconV2.vue: 12 added lines, 0 removed lines. Original line number Diff line number Diff line <template> <svg class="bus-vehicle-icon" :class="{ shuttle: String(type).toUpperCase() === 'SHUTTLE' }" viewBox="0 0 16 16"><circle cx="8" cy="8" r="7.5" /></svg> </template> <script setup> defineProps({ type: String }) </script> <style scoped> .bus-vehicle-icon { display: inline-block; flex-shrink: 0; width: .8rem; height: .8rem; fill: var(--bus-v2-bus-fill); stroke: #925a00; } .bus-vehicle-icon.shuttle { fill: var(--bus-v2-shuttle-fill); stroke: #28633f; } </style>
docs/.vuepress/components/bus-v2/BusVehicleLegendV2.vue +3 −3 Changes for docs/.vuepress/components/bus-v2/BusVehicleLegendV2.vue: 3 added lines, 3 removed lines. Original line number Diff line number Diff line <template> <div class="vehicle-legend" :aria-label="language === 'zh' ? '车型图例' : 'Vehicle legend'"> <span><i v-if="textMarkers" class="vehicle-legend__marker" :style="{ color: vehicleTextColor('BUS') }" aria-hidden="true">1</i><img v-else src="/bus.png" alt="">{{ language === 'zh' ? '巴士' : 'Bus' }}</span> <span><i v-if="textMarkers" class="vehicle-legend__marker shuttle" :style="{ color: vehicleTextColor('SHUTTLE') }" aria-hidden="true">1</i><img v-else src="/sev.png" alt="">{{ language === 'zh' ? '电瓶车' : 'EV Shuttle' }}</span> <span><i v-if="textMarkers" class="vehicle-legend__marker" :style="{ color: vehicleTextColor('BUS') }" aria-hidden="true">1</i><BusVehicleIconV2 v-else type="BUS" aria-hidden="true" />{{ language === 'zh' ? '巴士' : 'Bus' }}</span> <span><i v-if="textMarkers" class="vehicle-legend__marker shuttle" :style="{ color: vehicleTextColor('SHUTTLE') }" aria-hidden="true">1</i><BusVehicleIconV2 v-else type="SHUTTLE" aria-hidden="true" />{{ language === 'zh' ? '电瓶车' : 'EV Shuttle' }}</span> </div> </template> <script setup> import BusVehicleIconV2 from './BusVehicleIconV2.vue' import { vehicleTextColor } from './core.mjs' defineProps({ language: { type: String, default: 'zh' }, textMarkers: Boolean }) </script> <style scoped> .vehicle-legend { display: flex; flex-wrap: wrap; gap: .6rem; margin-top: .5rem; color: var(--bus-v2-muted); font-size: .82rem; }.vehicle-legend span { display: inline-flex; align-items: center; gap: .25rem; white-space: nowrap; } .vehicle-legend img { box-sizing: border-box; width: 1rem; height: 1rem; padding: 1px; border-radius: 50%; background: #fff; } .vehicle-legend__marker { display: inline-flex; align-items: center; justify-content: center; width: 1.25rem; height: 1.25rem; border-radius: 50%; background: var(--bus-v2-bus-fill); font: 700 .75rem/1 "Microsoft YaHei UI", "Microsoft YaHei", "PingFang SC", sans-serif; } .vehicle-legend__marker.shuttle { background: var(--bus-v2-shuttle-fill); } </style>