refactor: add design system with CSS tokens, utilities, and app shell
- Add design tokens (colors, spacing, radius, shadows, typography, transitions) - Add global reset, body/link/focus/typography base styles - Add utility classes (container, surface-card, btn variants, field, badge, message, divider) - Replace header ✉ symbol with inline SVG envelope icon - Update favicon to license-plate shaped mark with blue gradient and bold B - Rename brand from BilHälsning to Bilhej in header, footer, and HTML title - Rewrite footer tagline: focus on service, not privacy - Add theme-color meta tag for browser chrome
This commit is contained in:
parent
8cd7991603
commit
00327674ed
9 changed files with 653 additions and 68 deletions
|
|
@ -2,9 +2,14 @@
|
||||||
<html lang="sv">
|
<html lang="sv">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg?v=4" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>BilHälsning</title>
|
<meta name="description" content="Skicka ett brev till en fordonsägare. Ange registreringsnummer, skriv ditt meddelande, så postar vi det." />
|
||||||
|
<meta name="theme-color" content="#1d4ed8" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||||
|
<title>Bilhej — Skicka brev till fordonsägare</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 990 B |
|
|
@ -14,6 +14,6 @@ import AppFooter from '@/components/AppFooter.vue'
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.app__main {
|
.app__main {
|
||||||
min-height: calc(100vh - 12rem);
|
min-height: calc(100vh - 10rem);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,6 @@ describe('App', () => {
|
||||||
plugins: [router, createPinia()],
|
plugins: [router, createPinia()],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
expect(wrapper.text()).toContain('Skicka ett brev till en fordonsägare')
|
expect(wrapper.text()).toContain('Skicka ett brev')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,11 @@ function createTestRouter() {
|
||||||
name: 'orders',
|
name: 'orders',
|
||||||
component: { template: '<div>Orders</div>' },
|
component: { template: '<div>Orders</div>' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/admin',
|
||||||
|
name: 'admin',
|
||||||
|
component: { template: '<div>Admin</div>' },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -47,7 +52,7 @@ describe('AppHeader', () => {
|
||||||
const wrapper = mount(AppHeader, {
|
const wrapper = mount(AppHeader, {
|
||||||
global: { plugins: [router, createPinia()] },
|
global: { plugins: [router, createPinia()] },
|
||||||
})
|
})
|
||||||
expect(wrapper.text()).toContain('BilHälsning')
|
expect(wrapper.text()).toContain('Bilhej')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('has a link to home', () => {
|
it('has a link to home', () => {
|
||||||
|
|
@ -113,38 +118,39 @@ describe('AppHeader', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('when authenticated', () => {
|
describe('when authenticated', () => {
|
||||||
function mountAuthenticated() {
|
function mountAuthenticated(role = 'user') {
|
||||||
const jwt = makeJwt({ sub: 'test@bilhalsning.se', role: 'user' })
|
const jwt = makeJwt({ sub: 'test@bilhalsning.se', role })
|
||||||
localStorage.setItem('auth_token', jwt)
|
localStorage.setItem('auth_token', jwt)
|
||||||
const pinia = createPinia()
|
const pinia = createPinia()
|
||||||
setActivePinia(pinia)
|
setActivePinia(pinia)
|
||||||
const router = createTestRouter()
|
const router = createTestRouter()
|
||||||
return mount(AppHeader, {
|
const wrapper = mount(AppHeader, {
|
||||||
global: { plugins: [router, pinia] },
|
global: { plugins: [router, pinia] },
|
||||||
})
|
})
|
||||||
|
return { wrapper, router }
|
||||||
}
|
}
|
||||||
|
|
||||||
it('shows user email', () => {
|
it('shows user email', () => {
|
||||||
const wrapper = mountAuthenticated()
|
const { wrapper } = mountAuthenticated()
|
||||||
expect(wrapper.text()).toContain('test@bilhalsning.se')
|
expect(wrapper.text()).toContain('test@bilhalsning.se')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows logout button', () => {
|
it('shows logout button', () => {
|
||||||
const wrapper = mountAuthenticated()
|
const { wrapper } = mountAuthenticated()
|
||||||
const logoutButton = wrapper.find('button')
|
const logoutButton = wrapper.find('button')
|
||||||
expect(logoutButton.exists()).toBe(true)
|
expect(logoutButton.exists()).toBe(true)
|
||||||
expect(logoutButton.text()).toBe('Logga ut')
|
expect(logoutButton.text()).toBe('Logga ut')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not show login link', () => {
|
it('does not show login link', () => {
|
||||||
const wrapper = mountAuthenticated()
|
const { wrapper } = mountAuthenticated()
|
||||||
const links = wrapper.findAll('a')
|
const links = wrapper.findAll('a')
|
||||||
const loginLink = links.find((a) => a.attributes('href') === '/logga-in')
|
const loginLink = links.find((a) => a.attributes('href') === '/logga-in')
|
||||||
expect(loginLink).toBeUndefined()
|
expect(loginLink).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not show register link', () => {
|
it('does not show register link', () => {
|
||||||
const wrapper = mountAuthenticated()
|
const { wrapper } = mountAuthenticated()
|
||||||
const links = wrapper.findAll('a')
|
const links = wrapper.findAll('a')
|
||||||
const registerLink = links.find(
|
const registerLink = links.find(
|
||||||
(a) => a.attributes('href') === '/registrera',
|
(a) => a.attributes('href') === '/registrera',
|
||||||
|
|
@ -153,21 +159,47 @@ describe('AppHeader', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows orders link', () => {
|
it('shows orders link', () => {
|
||||||
const wrapper = mountAuthenticated()
|
const { wrapper } = mountAuthenticated()
|
||||||
const links = wrapper.findAll('a')
|
const links = wrapper.findAll('a')
|
||||||
const ordersLink = links.find((a) => a.attributes('href') === '/orders')
|
const ordersLink = links.find((a) => a.attributes('href') === '/orders')
|
||||||
expect(ordersLink).toBeTruthy()
|
expect(ordersLink).toBeTruthy()
|
||||||
expect(ordersLink?.text()).toBe('Mina beställningar')
|
expect(ordersLink?.text()).toBe('Mina beställningar')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('calls logout when clicking logout button', async () => {
|
it('does not show admin link for regular user', () => {
|
||||||
const wrapper = mountAuthenticated()
|
const { wrapper } = mountAuthenticated('user')
|
||||||
|
const links = wrapper.findAll('a')
|
||||||
|
const adminLink = links.find((a) => a.attributes('href') === '/admin')
|
||||||
|
expect(adminLink).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows admin link for admin user', () => {
|
||||||
|
const { wrapper } = mountAuthenticated('admin')
|
||||||
|
const links = wrapper.findAll('a')
|
||||||
|
const adminLink = links.find((a) => a.attributes('href') === '/admin')
|
||||||
|
expect(adminLink).toBeTruthy()
|
||||||
|
expect(adminLink?.text()).toBe('Admin')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls logout and redirects to home when clicking logout button', async () => {
|
||||||
|
const { wrapper, router } = mountAuthenticated()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
expect(auth.isAuthenticated).toBe(true)
|
expect(auth.isAuthenticated).toBe(true)
|
||||||
|
|
||||||
|
await router.push('/orders')
|
||||||
|
await router.isReady()
|
||||||
|
|
||||||
|
const navigationDone = new Promise<void>((resolve) => {
|
||||||
|
const remove = router.afterEach(() => {
|
||||||
|
remove()
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
await wrapper.find('button').trigger('click')
|
await wrapper.find('button').trigger('click')
|
||||||
|
await navigationDone
|
||||||
|
|
||||||
expect(auth.isAuthenticated).toBe(false)
|
expect(auth.isAuthenticated).toBe(false)
|
||||||
|
expect(router.currentRoute.value.path).toBe('/')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
409
frontend/src/assets/styles/base.css
Normal file
409
frontend/src/assets/styles/base.css
Normal file
|
|
@ -0,0 +1,409 @@
|
||||||
|
/* ── Reset ────────────────────────────────────────────────────────────── */
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
button,
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
font: inherit;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Design Tokens ───────────────────────────────────────────────────── */
|
||||||
|
:root {
|
||||||
|
/* ink / text */
|
||||||
|
--color-ink: #111827;
|
||||||
|
--color-muted: #667085;
|
||||||
|
--color-soft: #9ca3af;
|
||||||
|
|
||||||
|
/* surfaces */
|
||||||
|
--color-paper: #fdfaf5;
|
||||||
|
--color-surface: #ffffff;
|
||||||
|
--color-surface-tint: #f5f0ff;
|
||||||
|
|
||||||
|
/* brand */
|
||||||
|
--color-primary: #1d4ed8;
|
||||||
|
--color-primary-dark: #1e3a8a;
|
||||||
|
--color-primary-soft: #dbeafe;
|
||||||
|
--color-primary-ring: rgba(29, 78, 216, 0.22);
|
||||||
|
|
||||||
|
/* accent */
|
||||||
|
--color-accent: #0f766e;
|
||||||
|
--color-accent-soft: #ccfbf1;
|
||||||
|
|
||||||
|
/* semantic */
|
||||||
|
--color-success: #15803d;
|
||||||
|
--color-success-soft: #f0fdf4;
|
||||||
|
--color-warning: #b45309;
|
||||||
|
--color-warning-soft: #fffbeb;
|
||||||
|
--color-danger: #b91c1c;
|
||||||
|
--color-danger-soft: #fef2f2;
|
||||||
|
|
||||||
|
/* borders & dividers */
|
||||||
|
--color-border: #e5e7eb;
|
||||||
|
--color-border-light: #f3f4f6;
|
||||||
|
|
||||||
|
/* spacing */
|
||||||
|
--space-xs: 0.25rem;
|
||||||
|
--space-sm: 0.5rem;
|
||||||
|
--space-md: 1rem;
|
||||||
|
--space-lg: 1.5rem;
|
||||||
|
--space-xl: 2rem;
|
||||||
|
--space-2xl: 3rem;
|
||||||
|
--space-3xl: 4rem;
|
||||||
|
|
||||||
|
/* radius */
|
||||||
|
--radius-sm: 0.375rem;
|
||||||
|
--radius-md: 0.5rem;
|
||||||
|
--radius-lg: 0.75rem;
|
||||||
|
--radius-xl: 1rem;
|
||||||
|
--radius-full: 9999px;
|
||||||
|
|
||||||
|
/* shadows */
|
||||||
|
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
|
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.07),
|
||||||
|
0 2px 4px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
--shadow-lg: 0 10px 25px -5px rgba(0, 0, 0, 0.08),
|
||||||
|
0 8px 10px -6px rgba(0, 0, 0, 0.05);
|
||||||
|
--shadow-xl: 0 20px 60px rgba(0, 0, 0, 0.12);
|
||||||
|
--shadow-card: 0 1px 3px rgba(0, 0, 0, 0.06),
|
||||||
|
0 1px 2px rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
/* typography */
|
||||||
|
--font-sans: 'Inter', system-ui, -apple-system, BlinkMacSystemFont,
|
||||||
|
'Segoe UI', Roboto, sans-serif;
|
||||||
|
--font-serif: Georgia, 'Times New Roman', serif;
|
||||||
|
--font-mono: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
|
||||||
|
|
||||||
|
/* transitions */
|
||||||
|
--transition-fast: 150ms ease;
|
||||||
|
--transition-base: 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Body ────────────────────────────────────────────────────────────── */
|
||||||
|
body {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--color-ink);
|
||||||
|
background: var(--color-paper);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Typography ──────────────────────────────────────────────────────── */
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
|
line-height: 1.25;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 1.375rem;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: var(--color-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Links ───────────────────────────────────────────────────────────── */
|
||||||
|
a[href] {
|
||||||
|
color: var(--color-primary);
|
||||||
|
transition: color var(--transition-fast);
|
||||||
|
}
|
||||||
|
a[href]:hover {
|
||||||
|
color: var(--color-primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Buttons as links ────────────────────────────────────────────────── */
|
||||||
|
.btn[href],
|
||||||
|
.btn[href]:hover {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
.btn--primary[href],
|
||||||
|
.btn--primary[href]:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.btn--success[href],
|
||||||
|
.btn--success[href]:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.btn--accent[href],
|
||||||
|
.btn--accent[href]:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Focus ───────────────────────────────────────────────────────────── */
|
||||||
|
:focus-visible {
|
||||||
|
outline: 2px solid var(--color-primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Container ───────────────────────────────────────────────────────── */
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 72rem;
|
||||||
|
margin-inline: auto;
|
||||||
|
padding-inline: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container--narrow {
|
||||||
|
max-width: 36rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container--wide {
|
||||||
|
max-width: 80rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Surface card ────────────────────────────────────────────────────── */
|
||||||
|
.surface-card {
|
||||||
|
background: var(--color-surface);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--space-lg);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── buttons ─────────────────────────────────────────────────────────── */
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
font-weight: 600;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background var(--transition-fast),
|
||||||
|
transform var(--transition-fast), box-shadow var(--transition-fast);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--primary {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.btn--primary:hover:not(:disabled) {
|
||||||
|
background: var(--color-primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--success {
|
||||||
|
background: var(--color-success);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.btn--success:hover:not(:disabled) {
|
||||||
|
background: #166534;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--accent {
|
||||||
|
background: var(--color-accent);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.btn--accent:hover:not(:disabled) {
|
||||||
|
background: #0284c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--ghost {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-ink);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
.btn--ghost:hover:not(:disabled) {
|
||||||
|
background: var(--color-border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--sm {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--lg {
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
font-size: 1.0625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Form fields ─────────────────────────────────────────────────────── */
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field__label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field__input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
background: var(--color-surface);
|
||||||
|
border: 2px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
outline: none;
|
||||||
|
transition: border-color var(--transition-fast),
|
||||||
|
box-shadow var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field__input:focus {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: 0 0 0 3px var(--color-primary-ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field__input--error {
|
||||||
|
border-color: var(--color-danger);
|
||||||
|
}
|
||||||
|
.field__input--error:focus {
|
||||||
|
box-shadow: 0 0 0 3px rgba(185, 28, 28, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field__error {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--color-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field__hint {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--color-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Badge ───────────────────────────────────────────────────────────── */
|
||||||
|
.badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.2rem 0.75rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge--muted {
|
||||||
|
background: var(--color-border-light);
|
||||||
|
color: var(--color-muted);
|
||||||
|
}
|
||||||
|
.badge--primary {
|
||||||
|
background: var(--color-primary-soft);
|
||||||
|
color: var(--color-primary-dark);
|
||||||
|
}
|
||||||
|
.badge--success {
|
||||||
|
background: var(--color-success-soft);
|
||||||
|
color: var(--color-success);
|
||||||
|
}
|
||||||
|
.badge--warning {
|
||||||
|
background: var(--color-warning-soft);
|
||||||
|
color: var(--color-warning);
|
||||||
|
}
|
||||||
|
.badge--danger {
|
||||||
|
background: var(--color-danger-soft);
|
||||||
|
color: var(--color-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Message boxes ───────────────────────────────────────────────────── */
|
||||||
|
.message {
|
||||||
|
padding: var(--space-md) var(--space-lg);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message--error {
|
||||||
|
background: var(--color-danger-soft);
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
color: var(--color-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message--info {
|
||||||
|
background: var(--color-primary-soft);
|
||||||
|
border: 1px solid #ddd6fe;
|
||||||
|
color: var(--color-primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message--success {
|
||||||
|
background: var(--color-success-soft);
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
color: var(--color-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Divider ─────────────────────────────────────────────────────────── */
|
||||||
|
.divider {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
margin: var(--space-lg) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Eyebrow ─────────────────────────────────────────────────────────── */
|
||||||
|
.eyebrow {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Utility ─────────────────────────────────────────────────────────── */
|
||||||
|
.visually-hidden {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.text-muted {
|
||||||
|
color: var(--color-muted);
|
||||||
|
}
|
||||||
|
.text-sm {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
.text-xs {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
@ -4,39 +4,67 @@ import { RouterLink } from 'vue-router'
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<footer class="app-footer">
|
<footer class="app-footer">
|
||||||
<nav class="app-footer__links">
|
<div class="app-footer__inner">
|
||||||
<RouterLink to="/om" class="app-footer__link">Om oss</RouterLink>
|
<p class="app-footer__tagline">
|
||||||
<RouterLink to="/kontakt" class="app-footer__link">Kontakt</RouterLink>
|
Bilhej hjälper dig att skicka brev till bilägare via
|
||||||
<RouterLink to="/integritetspolicy" class="app-footer__link"
|
registreringsnummer.
|
||||||
>Integritetspolicy</RouterLink
|
</p>
|
||||||
>
|
<nav class="app-footer__links">
|
||||||
<RouterLink to="/villkor" class="app-footer__link">Villkor</RouterLink>
|
<RouterLink to="/om" class="app-footer__link">Om oss</RouterLink>
|
||||||
</nav>
|
<RouterLink to="/kontakt" class="app-footer__link">Kontakt</RouterLink>
|
||||||
|
<RouterLink to="/integritetspolicy" class="app-footer__link"
|
||||||
|
>Integritetspolicy</RouterLink
|
||||||
|
>
|
||||||
|
<RouterLink to="/villkor" class="app-footer__link">Villkor</RouterLink>
|
||||||
|
</nav>
|
||||||
|
<p class="app-footer__copy">
|
||||||
|
© {{ new Date().getFullYear() }} Bilhej
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.app-footer {
|
.app-footer {
|
||||||
background: #f7fafc;
|
background: var(--color-surface);
|
||||||
border-top: 1px solid #e2e8f0;
|
border-top: 1px solid var(--color-border);
|
||||||
padding: 1.5rem;
|
}
|
||||||
|
|
||||||
|
.app-footer__inner {
|
||||||
|
max-width: 72rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: var(--space-xl) var(--space-lg);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-footer__tagline {
|
||||||
|
color: var(--color-muted);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin: 0 0 var(--space-lg) 0;
|
||||||
|
}
|
||||||
|
|
||||||
.app-footer__links {
|
.app-footer__links {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 2rem;
|
gap: var(--space-xl);
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: var(--space-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-footer__link {
|
.app-footer__link {
|
||||||
color: #718096;
|
color: var(--color-soft);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
|
transition: color var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-footer__link:hover {
|
.app-footer__link:hover {
|
||||||
color: #1a202c;
|
color: var(--color-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-footer__copy {
|
||||||
|
color: var(--color-soft);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,83 +1,170 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RouterLink } from 'vue-router'
|
import { RouterLink, useRouter } from 'vue-router'
|
||||||
import { useAuthStore } from '@/stores/authStore'
|
import { useAuthStore } from '@/stores/authStore'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
function handleLogout() {
|
||||||
|
auth.logout()
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
<RouterLink to="/" class="app-header__logo">BilHälsning</RouterLink>
|
<div class="app-header__inner">
|
||||||
<nav class="app-header__nav">
|
<RouterLink to="/" class="app-header__logo">
|
||||||
<RouterLink to="/" class="app-header__link">Hem</RouterLink>
|
<svg
|
||||||
<template v-if="!auth.isAuthenticated">
|
class="app-header__logo-icon"
|
||||||
<RouterLink to="/logga-in" class="app-header__link"
|
viewBox="0 0 24 24"
|
||||||
>Logga in</RouterLink
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<RouterLink to="/registrera" class="app-header__link"
|
<rect
|
||||||
>Registrera</RouterLink
|
x="2"
|
||||||
>
|
y="5"
|
||||||
</template>
|
width="20"
|
||||||
<template v-else>
|
height="14"
|
||||||
<RouterLink to="/orders" class="app-header__link"
|
rx="2"
|
||||||
>Mina beställningar</RouterLink
|
stroke="currentColor"
|
||||||
>
|
stroke-width="2"
|
||||||
<span class="app-header__email">{{ auth.email }}</span>
|
/>
|
||||||
<button class="app-header__logout" @click="auth.logout()">
|
<path
|
||||||
Logga ut
|
d="M2 7l10 6 10-6"
|
||||||
</button>
|
stroke="currentColor"
|
||||||
</template>
|
stroke-width="2"
|
||||||
</nav>
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Bilhej
|
||||||
|
</RouterLink>
|
||||||
|
<nav class="app-header__nav">
|
||||||
|
<RouterLink to="/" class="app-header__link">Hem</RouterLink>
|
||||||
|
<template v-if="!auth.isAuthenticated">
|
||||||
|
<RouterLink to="/logga-in" class="app-header__link"
|
||||||
|
>Logga in</RouterLink
|
||||||
|
>
|
||||||
|
<RouterLink to="/registrera" class="app-header__link"
|
||||||
|
>Registrera</RouterLink
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<RouterLink
|
||||||
|
v-if="auth.isAdmin"
|
||||||
|
to="/admin"
|
||||||
|
class="app-header__link app-header__link--admin"
|
||||||
|
>Admin</RouterLink
|
||||||
|
>
|
||||||
|
<RouterLink to="/orders" class="app-header__link"
|
||||||
|
>Mina beställningar</RouterLink
|
||||||
|
>
|
||||||
|
<span class="app-header__email">{{ auth.email }}</span>
|
||||||
|
<button class="app-header__logout" @click="handleLogout">
|
||||||
|
Logga ut
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.app-header {
|
.app-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background: rgba(253, 250, 245, 0.85);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header__inner {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 1rem 1.5rem;
|
max-width: 72rem;
|
||||||
border-bottom: 1px solid #e2e8f0;
|
margin: 0 auto;
|
||||||
background: #fff;
|
padding: 0.875rem var(--space-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-header__logo {
|
.app-header__logo {
|
||||||
font-size: 1.25rem;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
font-size: 1.125rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1a202c;
|
color: var(--color-ink);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-header__logo-icon {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.app-header__nav {
|
.app-header__nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-header__link {
|
.app-header__link {
|
||||||
color: #4a5568;
|
padding: 0.4rem 0.875rem;
|
||||||
text-decoration: none;
|
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-muted);
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
transition:
|
||||||
|
color var(--transition-fast),
|
||||||
|
background var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-header__link:hover {
|
.app-header__link:hover,
|
||||||
color: #1a202c;
|
.app-header__link.router-link-active {
|
||||||
|
color: var(--color-primary-dark);
|
||||||
|
background: var(--color-primary-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header__link--admin {
|
||||||
|
background: var(--color-primary-soft);
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header__link--admin:hover {
|
||||||
|
background: #e9d5ff;
|
||||||
|
color: var(--color-primary-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-header__email {
|
.app-header__email {
|
||||||
color: #4a5568;
|
color: var(--color-muted);
|
||||||
font-size: 0.875rem;
|
font-size: 0.8125rem;
|
||||||
|
padding: 0 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-header__logout {
|
.app-header__logout {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: 1px solid var(--color-border);
|
||||||
color: #4a5568;
|
color: var(--color-muted);
|
||||||
font-size: 0.875rem;
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0;
|
padding: 0.35rem 0.875rem;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
transition:
|
||||||
|
color var(--transition-fast),
|
||||||
|
border-color var(--transition-fast),
|
||||||
|
background var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-header__logout:hover {
|
.app-header__logout:hover {
|
||||||
color: #1a202c;
|
color: var(--color-danger);
|
||||||
|
border-color: var(--color-danger);
|
||||||
|
background: var(--color-danger-soft);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { createApp } from 'vue'
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
import './assets/styles/base.css'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue