feat(ui): mobile-first redesign — compact hero, inline submit button, map-first with collapsible list
- Hero: remove full-width mobile submit, add inline "Go" button next to locate
- Prediction cards: tighter mobile padding (px-3 py-3)
- Search filters: right-aligned toolbar, remove "X stations found" count and map toggle
- Map: initialize view immediately to avoid tile wiggle, skip recenter on fresh init
- Station list: hidden by default, toggled via "Stations {count}" pill above map
- Typography: hide desktop h1 on mobile, scale down section headings and spacing
- Footer: remove uppercase styling from headings and copyright line
- Filter popover: auto-close on fuel/radius/sort/brand selection
fix(llm): retry submit_overlay when events_cited is missing, extend Fuel Finder timeout with retries
- LlmOverlayService: add `minItems: 1` to events_cited schema, detect missing citations
in submit response, inject tool_result error and retry once with explicit prompt
- Log full raw_result context when no verified citations, capturing direction/confidence/reasoning
- FuelPriceService: add 3×1s retry with 60s timeout to batch price requests (was 30s no retry)
- Tests: cover successful retry recovery and rejection when retry also omits citations
This commit is contained in:
@@ -204,6 +204,20 @@ function initMap() {
|
||||
// map-polish:7 — replace default attribution control with custom ⓘ button
|
||||
mapInstance = L.map(mapContainer.value, {zoomControl: false, attributionControl: false})
|
||||
|
||||
// Set the initial view immediately so tiles load at the correct spot from
|
||||
// frame 1 — avoids the "wiggle" caused by setView running after the
|
||||
// container is already laid out and tiles are mid-load.
|
||||
const initialZoom = getZoomForRadius(props.radiusMiles)
|
||||
const initialCenter = props.origin?.lat != null && props.origin?.lng != null
|
||||
? [props.origin.lat, props.origin.lng]
|
||||
: props.stations.length
|
||||
? [props.stations[0].lat, props.stations[0].lng]
|
||||
: null
|
||||
if (initialCenter) {
|
||||
mapInstance.setView(initialCenter, initialZoom)
|
||||
hasInitialView = true
|
||||
}
|
||||
|
||||
// map-polish:5 — Carto Positron tile (cleaner than raw OSM)
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
|
||||
subdomains: 'abcd',
|
||||
@@ -230,7 +244,7 @@ function initMap() {
|
||||
locateUser()
|
||||
}
|
||||
|
||||
function renderMarkers() {
|
||||
function renderMarkers({skipRecenter = false} = {}) {
|
||||
if (!mapInstance || !markersLayer) return
|
||||
|
||||
markersLayer.clearLayers()
|
||||
@@ -262,6 +276,8 @@ function renderMarkers() {
|
||||
bounds.push([station.lat, station.lng])
|
||||
})
|
||||
|
||||
if (skipRecenter) return
|
||||
|
||||
const zoom = getZoomForRadius(props.radiusMiles)
|
||||
const center = props.origin?.lat != null && props.origin?.lng != null
|
||||
? [props.origin.lat, props.origin.lng]
|
||||
@@ -290,9 +306,10 @@ function destroyMap() {
|
||||
|
||||
async function openMap() {
|
||||
await nextTick()
|
||||
const wasFreshInit = !mapInstance
|
||||
initMap()
|
||||
mapInstance?.invalidateSize()
|
||||
renderMarkers()
|
||||
renderMarkers({skipRecenter: wasFreshInit})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div ref="popoverRoot">
|
||||
<div class="flex flex-wrap items-center gap-2 md:gap-2.5 py-3 border-b border-zinc-200">
|
||||
<div class="flex flex-wrap items-center justify-end gap-2 md:gap-2.5 py-2 border-b border-zinc-200">
|
||||
<button
|
||||
:aria-expanded="open"
|
||||
:class="{ 'is-active': activeCount > 0 || open }"
|
||||
aria-controls="post-search-filters-panel"
|
||||
aria-haspopup="dialog"
|
||||
class="pill"
|
||||
class="pill !rounded-xl"
|
||||
type="button"
|
||||
@click="open = !open"
|
||||
>
|
||||
@@ -26,21 +26,6 @@
|
||||
></iconify-icon>
|
||||
</button>
|
||||
|
||||
<button
|
||||
:aria-expanded="mapOpen"
|
||||
:class="{ 'is-active': mapOpen }"
|
||||
aria-controls="leaflet-map-panel"
|
||||
class="pill"
|
||||
type="button"
|
||||
@click="emit('toggle-map')"
|
||||
>
|
||||
<iconify-icon :icon="mapOpen ? 'lucide:map' : 'lucide:map-off'" class="text-sm opacity-70"></iconify-icon>
|
||||
<span class="text-sm font-medium">{{ mapOpen ? 'Hide map' : 'Show map' }}</span>
|
||||
</button>
|
||||
|
||||
<span class="ml-auto text-sm text-zinc-500 font-medium">
|
||||
{{ stationCount }} station{{ stationCount !== 1 ? 's' : '' }} found
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -114,7 +99,7 @@
|
||||
class="pill-sm"
|
||||
role="radio"
|
||||
type="button"
|
||||
@click="emit('update:brandFilter', '')"
|
||||
@click="selectBrand('')"
|
||||
>
|
||||
All brands
|
||||
</button>
|
||||
@@ -126,7 +111,7 @@
|
||||
class="pill-sm"
|
||||
role="radio"
|
||||
type="button"
|
||||
@click="emit('update:brandFilter', brand)"
|
||||
@click="selectBrand(brand)"
|
||||
>
|
||||
{{ brand }}
|
||||
</button>
|
||||
@@ -178,11 +163,9 @@ const props = defineProps({
|
||||
initial: { type: Object, default: () => ({}) },
|
||||
brands: { type: Array, default: () => [] },
|
||||
brandFilter: { type: String, default: '' },
|
||||
mapOpen: { type: Boolean, default: true },
|
||||
stationCount: { type: Number, default: 0 },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['search', 'toggle-map', 'update:brandFilter'])
|
||||
const emit = defineEmits(['search', 'update:brandFilter'])
|
||||
|
||||
const postcode = ref('')
|
||||
const coords = ref(null)
|
||||
@@ -208,6 +191,7 @@ watch(() => props.initial, (v) => {
|
||||
|
||||
watch([fuelType, radius, sort], () => {
|
||||
if (hydrating) return
|
||||
open.value = false
|
||||
if (postcode.value.trim() || coords.value) emitSearch()
|
||||
})
|
||||
|
||||
@@ -232,6 +216,12 @@ function resetFilters() {
|
||||
radius.value = DEFAULTS.radius
|
||||
sort.value = DEFAULTS.sort
|
||||
if (props.brandFilter) emit('update:brandFilter', '')
|
||||
open.value = false
|
||||
}
|
||||
|
||||
function selectBrand(brand) {
|
||||
emit('update:brandFilter', brand)
|
||||
open.value = false
|
||||
}
|
||||
|
||||
function emitSearch() {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<!-- Free / guest: compact one-liner -->
|
||||
<div
|
||||
v-else-if="!isPaidTier"
|
||||
class="flex items-center gap-3 px-4 py-3 bg-white rounded-2xl border border-zinc-300"
|
||||
class="flex items-center gap-3 px-3 py-3 bg-white rounded-2xl border border-zinc-300"
|
||||
>
|
||||
<div :class="['shrink-0 w-10 h-10 rounded-full flex items-center justify-center', accentBg]">
|
||||
<iconify-icon :icon="genericIcon" class="text-xl text-white"></iconify-icon>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="prediction"
|
||||
class="p-4 sm:p-5 bg-white rounded-2xl border border-zinc-300"
|
||||
class="p-2 sm:p-5 bg-white rounded-2xl border border-zinc-300"
|
||||
>
|
||||
<div class="grid gap-4 lg:grid-cols-2 lg:gap-5">
|
||||
<div class="space-y-1.5">
|
||||
|
||||
@@ -21,6 +21,14 @@
|
||||
<iconify-icon :class="{ 'animate-spin': locating }" :icon="locating ? 'lucide:loader-circle' : 'lucide:locate-fixed'" class="text-lg"></iconify-icon>
|
||||
</button>
|
||||
|
||||
<!-- Mobile-only inline "Go" submit — sits next to the locate button -->
|
||||
<button
|
||||
class="md:hidden h-11 px-4 -ml-1 rounded-[10px] bg-primary text-white font-medium text-sm inline-flex items-center justify-center shrink-0 hover:opacity-90 transition"
|
||||
type="submit"
|
||||
>
|
||||
Go
|
||||
</button>
|
||||
|
||||
<!-- Desktop-only inline submit -->
|
||||
<button
|
||||
class="hidden md:inline-flex h-12 px-5 ml-1 rounded-xl bg-primary text-white font-medium text-[15px] items-center gap-2 hover:opacity-90 transition"
|
||||
@@ -31,15 +39,6 @@
|
||||
</button>
|
||||
</label>
|
||||
|
||||
<!-- Mobile-only full-width submit -->
|
||||
<button
|
||||
class="md:hidden w-full mt-2.5 h-14 rounded-2xl bg-primary text-white font-medium text-base inline-flex items-center justify-center gap-2 shadow-lg hover:opacity-90 transition"
|
||||
type="submit"
|
||||
>
|
||||
Check prices
|
||||
<iconify-icon class="text-lg" icon="lucide:arrow-right"></iconify-icon>
|
||||
</button>
|
||||
|
||||
<div class="mt-3 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-zinc-400 justify-center md:justify-start">
|
||||
<span class="hidden md:inline text-zinc-300">·</span>
|
||||
<span class="hidden md:inline font-mono">Try SW1A 1AA · M1 1AD · EH1 1YZ</span>
|
||||
|
||||
Reference in New Issue
Block a user