Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions frontend/src/__tests__/i18n.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { describe, it, expect, afterEach } from 'vitest'
import { nextTick } from 'vue'
import { i18n } from '@/i18n'

describe('i18n', () => {
afterEach(() => {
i18n.global.locale.value = 'en'
})

it('names the interface language on the page', async () => {
expect(document.documentElement.lang).toBe('en')

i18n.global.locale.value = 'nl'
await nextTick()

expect(document.documentElement.lang).toBe('nl')
})
})
2 changes: 2 additions & 0 deletions frontend/src/assets/carDiagram.css
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
box-shadow: var(--shadow-card);
padding: 1.25rem;
}

Expand Down Expand Up @@ -181,6 +182,7 @@
position: absolute;
font-size: 0.68rem;
font-weight: 600;
font-variant-numeric: tabular-nums;
padding: 2px 5px;
border-radius: 4px;
background: var(--color-surface);
Expand Down
1 change: 1 addition & 0 deletions frontend/src/assets/login.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
box-shadow: var(--shadow-card);
padding: 2rem 2rem 1.75rem;
}

Expand Down
34 changes: 25 additions & 9 deletions frontend/src/assets/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
--radius: 10px;
--demo-banner-height: 2.2rem;

/* Dark cards stand off the page by their surface alone; light ones need a hint of depth, or a
white card on the pale page blends into it. */
--shadow-card: none;

/* One size for every field, text box, native select and multiselect alike, so a form or a panel
that mixes them lines up. */
--field-py: 0.375rem;
Expand All @@ -50,6 +54,7 @@
--color-text-muted: #5a6a7e;
--color-primary-light: #1d4ed8;
--color-danger-text: #dc2626;
--shadow-card: 0 1px 2px rgb(16 24 40 / 5%), 0 1px 3px rgb(16 24 40 / 8%);
--field-caret: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 512'%3E%3Cpath fill='%235a6a7e' d='M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z'/%3E%3C/svg%3E");
}

Expand Down Expand Up @@ -378,10 +383,11 @@ body,
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
box-shadow: var(--shadow-card);
padding: 0.5rem;
display: flex;
align-items: center;
gap: 1rem;
gap: 0.75rem;
min-width: 0;
}

Expand Down Expand Up @@ -418,30 +424,38 @@ body,
overflow: hidden;
}

/* Label, value and note wrap rather than end in an ellipsis: a tile is narrow on a phone and in
the wide desktop grid alike, and a cut-off word has no tooltip to fall back on under a finger.
The grid stretches every tile in a row to the tallest, so the row stays even. A single word
too long for the tile (Dutch compounds, mostly) is hyphenated by the page's language, and
only ends in an ellipsis where the browser has no hyphenation for it. */
.status-card__label {
font-size: 0.72rem;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
white-space: nowrap;
text-wrap: balance;
hyphens: auto;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}

/* Balanced, so "Off · 21.0 °C" breaks before the reading rather than leaving its unit alone. */
.status-card__value {
font-size: 1.05rem;
font-weight: 600;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
font-variant-numeric: tabular-nums;
text-wrap: balance;
overflow-wrap: break-word;
}

.status-card__note {
font-size: 0.78rem;
color: var(--color-text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
overflow-wrap: break-word;
}

.status-card__unit {
Expand All @@ -466,7 +480,7 @@ body,
}

.status-card__chevron {
font-size: 0.65rem;
font-size: 0.8rem;
color: var(--color-text-muted);
margin-left: auto;
flex-shrink: 0;
Expand Down Expand Up @@ -866,6 +880,7 @@ body,
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
box-shadow: var(--shadow-card);
padding: 1.25rem;
display: flex;
flex-direction: column;
Expand Down Expand Up @@ -2056,6 +2071,7 @@ textarea.form-control {
padding: 0.25rem 0.75rem;
color: var(--color-text-muted);
font-size: 0.78rem;
font-variant-numeric: tabular-nums;
overflow: hidden;
white-space: nowrap;
}
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/assets/map.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
border-radius: var(--radius);
overflow: hidden;
border: 1px solid var(--color-border);
box-shadow: var(--shadow-card);
}

.trip-sidebar {
Expand All @@ -33,6 +34,7 @@
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
box-shadow: var(--shadow-card);
padding: 0 1rem 1rem;
overflow-y: auto;
display: flex;
Expand Down Expand Up @@ -423,11 +425,13 @@

.trip-list__time {
font-size: 0.85rem;
font-variant-numeric: tabular-nums;
flex-shrink: 0;
}

.trip-list__meta {
font-size: 0.75rem;
font-variant-numeric: tabular-nums;
color: var(--color-text-muted);
}

Expand Down
1 change: 1 addition & 0 deletions frontend/src/assets/statistics.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
box-shadow: var(--shadow-card);
padding: 1.25rem;
overflow: visible;
}
22 changes: 18 additions & 4 deletions frontend/src/assets/tripLog.css
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-surface);
box-shadow: var(--shadow-card);
}

.trip-log-bulk__actions {
Expand Down Expand Up @@ -67,6 +68,7 @@
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-surface);
box-shadow: var(--shadow-card);
transition: opacity 0.15s;
}

Expand All @@ -78,6 +80,7 @@
grid-area: when;
display: flex;
flex-direction: column;
font-variant-numeric: tabular-nums;
}

.trip-log-item__date {
Expand Down Expand Up @@ -110,6 +113,7 @@
flex-direction: column;
align-items: flex-end;
white-space: nowrap;
font-variant-numeric: tabular-nums;
}

.trip-log-item__km {
Expand Down Expand Up @@ -146,12 +150,14 @@
--chip-color: var(--color-success);
}

/* ── Phones ───────────────────────────────────────────────── */
/* ── Tablets and phones ───────────────────────────────────── */

@media (width <= 767px) {
/* Beside the sidebar, a tablet leaves the content column barely wider than a phone: four totals
in a row cut their labels short, and the route squeezed between date and distance cuts most
addresses down to a street name. */
@media (width <= 1023px) {
.trip-log-totals {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.5rem;
}

.trip-log-item {
Expand All @@ -162,7 +168,7 @@
grid-template-columns: minmax(0, 1fr) auto;
}

/* Half a phone's width cuts most addresses short, so they wrap here instead. */
/* With the route on a row of its own, a long address wraps rather than ending in an ellipsis. */
.trip-log-item__route {
flex-wrap: wrap;
row-gap: 0.1rem;
Expand All @@ -171,6 +177,14 @@
.trip-log-item__place {
white-space: normal;
}
}

/* ── Phones ───────────────────────────────────────────────── */

@media (width <= 767px) {
.trip-log-totals {
gap: 0.5rem;
}

/* The three purposes share the row equally, and the notes take the next one. */
.trip-log-item__purpose {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/FindMyCarCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ async function request(target: boolean) {
<ExpandableStatusCard
icon="car-burst"
:title="t('control.findMyCar')"
:value="active ? t('control.findMyCarActive') : '-'"
:value="active ? t('control.findMyCarActive') : t('common.off')"
:variant="active ? 'warning' : undefined"
>
<p class="card-info-desc">{{ t('control.findMyCarConfirm') }}</p>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/LightsCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const summary = computed((): string | null => {
icon="lightbulb"
:title="t('vehicle.lights.title')"
:value="summary"
:variant="activeLights.length > 0 ? 'success' : 'danger'"
:variant="activeLights.length > 0 ? 'success' : undefined"
>
<div class="detail-list">
<DetailListItem
Expand Down
20 changes: 8 additions & 12 deletions frontend/src/components/MaintenanceSummaryCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,14 @@ watch(
{ immediate: true },
)

const variant = computed((): 'success' | 'warning' | 'danger' => {
if (store.overdueItems.length > 0) return 'danger'
if (store.dueSoonItems.length > 0) return 'warning'
return 'success'
// The card names the worst state in the words the maintenance page uses, rather than a bare count
// of items needing attention; the tooltip lists which ones.
const state = computed(() => {
if (store.overdueItems.length > 0) return { variant: 'danger', status: 'overdue' } as const
if (store.dueSoonItems.length > 0) return { variant: 'warning', status: 'dueSoon' } as const
return { variant: 'success', status: 'ok' } as const
})

const value = computed(() =>
store.attentionItems.length === 0
? t('maintenance.status.ok')
: String(store.attentionItems.length),
)

const subtitle = computed(() => {
if (store.attentionItems.length === 0) return t('maintenance.dashboard.allOk')
const names = store.attentionItems.slice(0, 2).map((i) => i.name)
Expand All @@ -51,9 +47,9 @@ function goToPage() {
<StatusCard
icon="screwdriver-wrench"
:label="t('maintenance.title')"
:value="value"
:value="t(`maintenance.status.${state.status}`)"
:subtitle="subtitle"
:variant="variant"
:variant="state.variant"
clickable
@click="goToPage"
/>
Expand Down
29 changes: 29 additions & 0 deletions frontend/src/components/__tests__/FindMyCarCard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { createI18n } from 'vue-i18n'
import { ref } from 'vue'
import en from '@/locales/en.json'
import FindMyCarCard from '../FindMyCarCard.vue'

vi.mock('@/composables/useVehicleCommand', () => ({
useVehicleCommand: () => ({
sending: ref(null),
lastResult: ref(null),
isPending: () => false,
send: vi.fn<(...args: unknown[]) => Promise<boolean>>().mockResolvedValue(true),
}),
}))

const i18n = createI18n({ legacy: false, locale: 'en', messages: { en } })

describe('FindMyCarCard', () => {
it('reads Off while the horn is quiet, rather than a dash that looks like missing data', () => {
const card = mount(FindMyCarCard, {
props: { vin: 'FAKEVN00000000001' },
global: { plugins: [i18n], stubs: { DetailModal: true, FontAwesomeIcon: true } },
}).find('.status-card')

expect(card.find('.status-card__value').text()).toBe('Off')
expect(card.classes()).not.toContain('status-card--warning')
})
})
31 changes: 31 additions & 0 deletions frontend/src/components/__tests__/LightsCard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import { createI18n } from 'vue-i18n'
import en from '@/locales/en.json'
import LightsCard from '../LightsCard.vue'

const i18n = createI18n({ legacy: false, locale: 'en', messages: { en } })

function mountCard(props: Record<string, unknown> = {}) {
return mount(LightsCard, {
props: { mainBeam: false, dippedBeam: false, side: false, ...props },
global: { plugins: [i18n], stubs: { DetailModal: true, FontAwesomeIcon: true } },
})
}

describe('LightsCard', () => {
it('shows lights that are off as a plain tile, not as a problem', () => {
const card = mountCard().find('.status-card')

expect(card.find('.status-card__value').text()).toBe('Off')
expect(card.classes()).not.toContain('status-card--danger')
expect(card.classes()).not.toContain('status-card--success')
})

it('names the lights that are on', () => {
const card = mountCard({ dippedBeam: true, side: true }).find('.status-card')

expect(card.find('.status-card__value').text()).toBe('Low beam · Sidelights')
expect(card.classes()).toContain('status-card--success')
})
})
Loading
Loading