Extract BackLink component into App layout

Move back navigation (chevron + "fete" brand) from per-view
definitions into a shared BackLink component rendered in App.vue.
Shown on all pages except home. Hero overlay gets pointer-events:
none so the link stays clickable on the event detail page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 23:06:03 +01:00
parent 13b01dfba8
commit f972a41e45
5 changed files with 56 additions and 56 deletions

View File

@@ -65,7 +65,7 @@ test.describe('US1: Watch event from detail page', () => {
await expect(bookmark).toHaveAttribute('aria-label', 'Stop watching this event') await expect(bookmark).toHaveAttribute('aria-label', 'Stop watching this event')
// Navigate to event list via back link // Navigate to event list via back link
await page.locator('.detail__back').click() await page.getByLabel('Back to home').click()
// Event appears with "Watching" label // Event appears with "Watching" label
await expect(page.getByText('Summer BBQ')).toBeVisible() await expect(page.getByText('Summer BBQ')).toBeVisible()
@@ -89,7 +89,7 @@ test.describe('US2: Un-watch event from detail page', () => {
await expect(bookmark).toHaveAttribute('aria-label', 'Watch this event') await expect(bookmark).toHaveAttribute('aria-label', 'Watch this event')
// Navigate to event list via back link (avoid page.goto re-running addInitScript) // Navigate to event list via back link (avoid page.goto re-running addInitScript)
await page.locator('.detail__back').click() await page.getByLabel('Back to home').click()
// Event is gone // Event is gone
await expect(page.getByText('Summer BBQ')).not.toBeVisible() await expect(page.getByText('Summer BBQ')).not.toBeVisible()
@@ -109,7 +109,7 @@ test.describe('US3: Bookmark reflects attending status', () => {
await expect(bookmark).not.toBeVisible() await expect(bookmark).not.toBeVisible()
// Navigate to list via back link // Navigate to list via back link
await page.locator('.detail__back').click() await page.getByLabel('Back to home').click()
await expect(page.getByText('Attending')).toBeVisible() await expect(page.getByText('Attending')).toBeVisible()
await expect(page.getByText('Watching')).not.toBeVisible() await expect(page.getByText('Watching')).not.toBeVisible()
}) })
@@ -137,7 +137,7 @@ test.describe('US4: RSVP cancellation preserves watch status', () => {
await expect(bookmark).toHaveAttribute('aria-label', 'Stop watching this event') await expect(bookmark).toHaveAttribute('aria-label', 'Stop watching this event')
// Navigate to list via back link // Navigate to list via back link
await page.locator('.detail__back').click() await page.getByLabel('Back to home').click()
await expect(page.getByText('Watching')).toBeVisible() await expect(page.getByText('Watching')).toBeVisible()
await expect(page.getByText('Attending')).not.toBeVisible() await expect(page.getByText('Attending')).not.toBeVisible()
}) })
@@ -211,7 +211,7 @@ test.describe('US7: Watcher upgrades to attendee', () => {
await expect(bookmark).not.toBeVisible() await expect(bookmark).not.toBeVisible()
// Navigate to list via back link // Navigate to list via back link
await page.locator('.detail__back').click() await page.getByLabel('Back to home').click()
await expect(page.getByText('Attending')).toBeVisible() await expect(page.getByText('Attending')).toBeVisible()
await expect(page.getByText('Watching')).not.toBeVisible() await expect(page.getByText('Watching')).not.toBeVisible()
}) })

View File

@@ -1,9 +1,26 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<header v-if="route.name !== 'home'" class="app-header">
<BackLink />
</header>
<RouterView /> <RouterView />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { RouterView } from 'vue-router' import { RouterView, useRoute } from 'vue-router'
import BackLink from '@/components/BackLink.vue'
const route = useRoute()
</script> </script>
<style scoped>
.app-header {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 10;
padding-top: var(--spacing-lg);
}
</style>

View File

@@ -0,0 +1,28 @@
<template>
<RouterLink to="/" class="back-link" aria-label="Back to home">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="15 18 9 12 15 6" />
</svg>
<span class="back-link__brand">fete</span>
</RouterLink>
</template>
<script setup lang="ts">
import { RouterLink } from 'vue-router'
</script>
<style scoped>
.back-link {
display: inline-flex;
align-items: center;
gap: 0.15rem;
color: var(--color-text-on-gradient);
text-decoration: none;
line-height: 1;
}
.back-link__brand {
font-size: 1.3rem;
font-weight: 700;
}
</style>

View File

@@ -1,9 +1,6 @@
<template> <template>
<main class="create"> <main class="create">
<header class="create__header">
<RouterLink to="/" class="create__back" aria-label="Back to home">&larr;</RouterLink>
<h1 class="create__title">Create</h1> <h1 class="create__title">Create</h1>
</header>
<form class="create__form" novalidate @submit.prevent="handleSubmit"> <form class="create__form" novalidate @submit.prevent="handleSubmit">
<div class="form-group"> <div class="form-group">
@@ -76,7 +73,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { reactive, ref, watch } from 'vue' import { reactive, ref, watch } from 'vue'
import { RouterLink, useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { api } from '@/api/client' import { api } from '@/api/client'
import { useEventStorage } from '@/composables/useEventStorage' import { useEventStorage } from '@/composables/useEventStorage'
@@ -194,20 +191,7 @@ async function handleSubmit() {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--spacing-lg); gap: var(--spacing-lg);
padding-top: var(--spacing-lg); padding-top: calc(var(--spacing-lg) + 2.5rem);
}
.create__header {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.create__back {
color: var(--color-text-on-gradient);
font-size: 1.5rem;
text-decoration: none;
line-height: 1;
} }
.create__title { .create__title {

View File

@@ -8,10 +8,6 @@
alt="" alt=""
/> />
<div class="detail__hero-overlay" /> <div class="detail__hero-overlay" />
<header class="detail__header">
<RouterLink to="/" class="detail__back" aria-label="Back to home">&larr;</RouterLink>
<span class="detail__brand">fete</span>
</header>
</div> </div>
<div class="detail__body"> <div class="detail__body">
@@ -168,7 +164,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { RouterLink, useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { api } from '@/api/client' import { api } from '@/api/client'
import { useEventStorage } from '@/composables/useEventStorage' import { useEventStorage } from '@/composables/useEventStorage'
import AttendeeList from '@/components/AttendeeList.vue' import AttendeeList from '@/components/AttendeeList.vue'
@@ -437,32 +433,7 @@ onMounted(fetchEvent)
var(--color-glass-overlay) 0%, var(--color-glass-overlay) 0%,
transparent 50% transparent 50%
); );
} pointer-events: none;
.detail__header {
position: absolute;
top: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-lg) var(--content-padding);
padding-top: env(safe-area-inset-top, var(--spacing-lg));
z-index: 1;
}
.detail__back {
color: var(--color-text-on-gradient);
font-size: 1.5rem;
text-decoration: none;
line-height: 1;
}
.detail__brand {
font-size: 1.3rem;
font-weight: 700;
color: var(--color-text-on-gradient);
} }
.detail__body { .detail__body {