9 Commits

Author SHA1 Message Date
51ab99fc61 Introduce --color-danger-solid-* CSS variables and replace hardcoded values
All checks were successful
CI / backend-test (push) Successful in 55s
CI / frontend-test (push) Successful in 27s
CI / frontend-e2e (push) Successful in 1m30s
CI / build-and-publish (push) Successful in 58s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 23:24:58 +01:00
d52f51d6e1 Match cancel-event confirm button color with ConfirmDialog style
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 23:22:06 +01:00
c1760ae376 Apply consistent label color to cancel-event bottom sheet
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 23:19:26 +01:00
6d51327e56 Add touch drag-to-dismiss gesture to BottomSheet
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 23:15:30 +01:00
96044ae1ed Change create-event page title to "Great, a Party!"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 23:08:45 +01:00
f972a41e45 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>
2026-03-12 23:06:03 +01:00
13b01dfba8 Add exclamation mark to RSVP CTA button ("I'm attending!")
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 22:36:44 +01:00
fd8724db8f Rename role badges to present participle (Organizing, Attending)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 22:34:36 +01:00
8885dbd722 Soften RSVP cancellation dialog wording
Replace harsh "permanently cancelled" language with friendlier
"The organizer will no longer see you as attending" and rename
buttons from "Cancel attendance" to "Cancel RSVP".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 22:32:19 +01:00
16 changed files with 149 additions and 95 deletions

View File

@@ -43,7 +43,7 @@ test.describe('US1: Cancel RSVP from Event Detail View', () => {
await expect(statusBar).toBeVisible() await expect(statusBar).toBeVisible()
// Cancel button hidden initially // Cancel button hidden initially
await expect(page.getByRole('button', { name: 'Cancel attendance' })).not.toBeVisible() await expect(page.getByRole('button', { name: 'Cancel RSVP' })).not.toBeVisible()
}) })
test('tapping status bar reveals cancel button', async ({ page, network }) => { test('tapping status bar reveals cancel button', async ({ page, network }) => {
@@ -57,7 +57,7 @@ test.describe('US1: Cancel RSVP from Event Detail View', () => {
await page.getByRole('button', { name: /You're attending/ }).click() await page.getByRole('button', { name: /You're attending/ }).click()
// Cancel button appears // Cancel button appears
await expect(page.getByRole('button', { name: 'Cancel attendance' })).toBeVisible() await expect(page.getByRole('button', { name: 'Cancel RSVP' })).toBeVisible()
}) })
test('confirm cancellation → localStorage cleared, count decremented, bar reset', async ({ page, network }) => { test('confirm cancellation → localStorage cleared, count decremented, bar reset', async ({ page, network }) => {
@@ -70,13 +70,13 @@ test.describe('US1: Cancel RSVP from Event Detail View', () => {
await page.addInitScript(seedEvents([rsvpSeed()])) await page.addInitScript(seedEvents([rsvpSeed()]))
await page.goto(`/events/${fullEvent.eventToken}`) await page.goto(`/events/${fullEvent.eventToken}`)
// Expand → Cancel attendance → Confirm in dialog // Expand → Cancel RSVP → Confirm in dialog
await page.getByRole('button', { name: /You're attending/ }).click() await page.getByRole('button', { name: /You're attending/ }).click()
await page.locator('.rsvp-bar__cancel').click() await page.locator('.rsvp-bar__cancel').click()
// Confirm dialog // Confirm dialog
await expect(page.getByText('Your attendance will be permanently cancelled.')).toBeVisible() await expect(page.getByText('The organizer will no longer see you as attending.')).toBeVisible()
await page.getByRole('alertdialog').getByRole('button', { name: 'Cancel attendance' }).click() await page.getByRole('alertdialog').getByRole('button', { name: 'Cancel RSVP' }).click()
// Bar resets to CTA state // Bar resets to CTA state
await expect(page.getByRole('button', { name: "I'm attending" })).toBeVisible() await expect(page.getByRole('button', { name: "I'm attending" })).toBeVisible()
@@ -108,7 +108,7 @@ test.describe('US1: Cancel RSVP from Event Detail View', () => {
// Expand → Cancel → Confirm in dialog // Expand → Cancel → Confirm in dialog
await page.getByRole('button', { name: /You're attending/ }).click() await page.getByRole('button', { name: /You're attending/ }).click()
await page.locator('.rsvp-bar__cancel').click() await page.locator('.rsvp-bar__cancel').click()
await page.getByRole('alertdialog').getByRole('button', { name: 'Cancel attendance' }).click() await page.getByRole('alertdialog').getByRole('button', { name: 'Cancel RSVP' }).click()
// Error message // Error message
await expect(page.getByText('Could not cancel RSVP. Please try again.')).toBeVisible() await expect(page.getByText('Could not cancel RSVP. Please try again.')).toBeVisible()
@@ -136,7 +136,7 @@ test.describe('US1: Cancel RSVP from Event Detail View', () => {
// Cancel first // Cancel first
await page.getByRole('button', { name: /You're attending/ }).click() await page.getByRole('button', { name: /You're attending/ }).click()
await page.locator('.rsvp-bar__cancel').click() await page.locator('.rsvp-bar__cancel').click()
await page.getByRole('alertdialog').getByRole('button', { name: 'Cancel attendance' }).click() await page.getByRole('alertdialog').getByRole('button', { name: 'Cancel RSVP' }).click()
// CTA should be back // CTA should be back
await expect(page.getByRole('button', { name: "I'm attending" })).toBeVisible() await expect(page.getByRole('button', { name: "I'm attending" })).toBeVisible()
@@ -244,7 +244,7 @@ test.describe('US3: Cancel RSVP with Stale/Invalid Token', () => {
// Cancel flow // Cancel flow
await page.getByRole('button', { name: /You're attending/ }).click() await page.getByRole('button', { name: /You're attending/ }).click()
await page.locator('.rsvp-bar__cancel').click() await page.locator('.rsvp-bar__cancel').click()
await page.getByRole('alertdialog').getByRole('button', { name: 'Cancel attendance' }).click() await page.getByRole('alertdialog').getByRole('button', { name: 'Cancel RSVP' }).click()
// Treated as success — CTA returns // Treated as success — CTA returns
await expect(page.getByRole('button', { name: "I'm attending" })).toBeVisible() await expect(page.getByRole('button', { name: "I'm attending" })).toBeVisible()

View File

@@ -139,7 +139,7 @@ test.describe('US5: Visual Distinction for Event Roles', () => {
const card = page.locator('.event-card').filter({ hasText: 'Summer BBQ' }) const card = page.locator('.event-card').filter({ hasText: 'Summer BBQ' })
const badge = card.locator('.event-card__badge') const badge = card.locator('.event-card__badge')
await expect(badge).toBeVisible() await expect(badge).toBeVisible()
await expect(badge).toHaveText('Organizer') await expect(badge).toHaveText('Organizing')
await expect(badge).toHaveClass(/event-card__badge--organizer/) await expect(badge).toHaveClass(/event-card__badge--organizer/)
}) })
@@ -150,7 +150,7 @@ test.describe('US5: Visual Distinction for Event Roles', () => {
const card = page.locator('.event-card').filter({ hasText: 'Team Meeting' }) const card = page.locator('.event-card').filter({ hasText: 'Team Meeting' })
const badge = card.locator('.event-card__badge') const badge = card.locator('.event-card__badge')
await expect(badge).toBeVisible() await expect(badge).toBeVisible()
await expect(badge).toHaveText('Attendee') await expect(badge).toHaveText('Attending')
await expect(badge).toHaveClass(/event-card__badge--attendee/) await expect(badge).toHaveClass(/event-card__badge--attendee/)
}) })

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,8 +109,8 @@ 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('Attendee')).toBeVisible() await expect(page.getByText('Attending')).toBeVisible()
await expect(page.getByText('Watching')).not.toBeVisible() await expect(page.getByText('Watching')).not.toBeVisible()
}) })
}) })
@@ -129,7 +129,7 @@ test.describe('US4: RSVP cancellation preserves watch status', () => {
// Cancel RSVP // Cancel RSVP
await page.getByRole('button', { name: /You're attending/ }).click() await page.getByRole('button', { name: /You're attending/ }).click()
await page.locator('.rsvp-bar__cancel').click() await page.locator('.rsvp-bar__cancel').click()
await page.getByRole('alertdialog').getByRole('button', { name: 'Cancel attendance' }).click() await page.getByRole('alertdialog').getByRole('button', { name: 'Cancel RSVP' }).click()
// Bookmark reappears in CTA state, filled because event is still stored // Bookmark reappears in CTA state, filled because event is still stored
const bookmark = page.locator('.rsvp-bar__bookmark-inner') const bookmark = page.locator('.rsvp-bar__bookmark-inner')
@@ -137,9 +137,9 @@ 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('Attendee')).not.toBeVisible() await expect(page.getByText('Attending')).not.toBeVisible()
}) })
}) })
@@ -211,8 +211,8 @@ 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('Attendee')).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

@@ -25,6 +25,9 @@
--color-danger-bg-strong: rgba(220, 38, 38, 0.2); --color-danger-bg-strong: rgba(220, 38, 38, 0.2);
--color-danger-border: rgba(220, 38, 38, 0.3); --color-danger-border: rgba(220, 38, 38, 0.3);
--color-danger-border-strong: rgba(220, 38, 38, 0.4); --color-danger-border-strong: rgba(220, 38, 38, 0.4);
--color-danger-solid: #d32f2f;
--color-danger-solid-hover: #b71c1c;
--color-danger-solid-text: #fff;
/* Glass system */ /* Glass system */
--color-glass: rgba(255, 255, 255, 0.1); --color-glass: rgba(255, 255, 255, 0.1);
@@ -214,7 +217,7 @@ textarea.form-field {
/* Error message */ /* Error message */
.field-error { .field-error {
color: #fff; color: var(--color-danger-solid);
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 600; font-weight: 600;
padding-left: 0.25rem; padding-left: 0.25rem;
@@ -325,7 +328,8 @@ textarea.form-field {
gap: var(--spacing-md); gap: var(--spacing-md);
} }
.rsvp-form__label { .rsvp-form__label,
.cancel-form__label {
font-size: 0.85rem; font-size: 0.85rem;
font-weight: 700; font-weight: 700;
color: var(--color-text-on-gradient); color: var(--color-text-on-gradient);
@@ -333,7 +337,7 @@ textarea.form-field {
} }
.rsvp-form__field-error { .rsvp-form__field-error {
color: #d32f2f; color: var(--color-danger-solid);
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 600; font-weight: 600;
padding-left: 0.25rem; padding-left: 0.25rem;

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

@@ -2,7 +2,18 @@
<Teleport to="body"> <Teleport to="body">
<Transition name="sheet"> <Transition name="sheet">
<div v-if="open" class="sheet-backdrop" @click.self="$emit('close')" @keydown.escape="$emit('close')"> <div v-if="open" class="sheet-backdrop" @click.self="$emit('close')" @keydown.escape="$emit('close')">
<div class="sheet" role="dialog" aria-modal="true" :aria-label="label" ref="sheetEl" tabindex="-1"> <div
class="sheet"
role="dialog"
aria-modal="true"
:aria-label="label"
ref="sheetEl"
tabindex="-1"
:style="dragStyle"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
>
<div class="sheet__handle" aria-hidden="true" /> <div class="sheet__handle" aria-hidden="true" />
<slot /> <slot />
</div> </div>
@@ -12,14 +23,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, nextTick } from 'vue' import { ref, computed, watch, nextTick } from 'vue'
defineProps<{ defineProps<{
open: boolean open: boolean
label: string label: string
}>() }>()
defineEmits<{ const emit = defineEmits<{
close: [] close: []
}>() }>()
@@ -39,6 +50,45 @@ watch(
} }
}, },
) )
/* ── Drag-to-dismiss ── */
const DISMISS_THRESHOLD = 100
const dragY = ref(0)
const dragging = ref(false)
let startY = 0
const dragStyle = computed(() => {
if (!dragging.value || dragY.value <= 0) return undefined
return {
transform: `translateY(${dragY.value}px)`,
transition: 'none',
}
})
function onTouchStart(e: TouchEvent) {
const touch = e.touches[0]
if (!touch) return
startY = touch.clientY
dragging.value = true
dragY.value = 0
}
function onTouchMove(e: TouchEvent) {
if (!dragging.value) return
const touch = e.touches[0]
if (!touch) return
const delta = touch.clientY - startY
if (delta > 0) e.preventDefault()
dragY.value = Math.max(0, delta)
}
function onTouchEnd() {
if (dragY.value >= DISMISS_THRESHOLD) {
emit('close')
}
dragging.value = false
dragY.value = 0
}
</script> </script>
<style scoped> <style scoped>

View File

@@ -139,8 +139,8 @@ watch(
} }
.confirm-dialog__btn--confirm { .confirm-dialog__btn--confirm {
background: #d32f2f; background: var(--color-danger-solid);
color: #fff; color: var(--color-danger-solid-text);
} }
.confirm-dialog-enter-active, .confirm-dialog-enter-active,

View File

@@ -12,7 +12,7 @@
<span class="event-card__time">{{ displayTime }}</span> <span class="event-card__time">{{ displayTime }}</span>
</RouterLink> </RouterLink>
<span v-if="eventRole" class="event-card__badge" :class="`event-card__badge--${eventRole}`"> <span v-if="eventRole" class="event-card__badge" :class="`event-card__badge--${eventRole}`">
{{ eventRole === 'organizer' ? 'Organizer' : eventRole === 'attendee' ? 'Attendee' : 'Watching' }} {{ eventRole === 'organizer' ? 'Organizing' : eventRole === 'attendee' ? 'Attending' : 'Watching' }}
</span> </span>
<button <button
class="event-card__delete" class="event-card__delete"
@@ -175,7 +175,7 @@ function onTouchEnd() {
} }
.event-card__delete:hover { .event-card__delete:hover {
color: #d32f2f; color: var(--color-danger-solid);
background: rgba(211, 47, 47, 0.08); background: rgba(211, 47, 47, 0.08);
} }

View File

@@ -25,7 +25,7 @@
type="button" type="button"
@click="$emit('cancel')" @click="$emit('cancel')"
> >
Cancel attendance Cancel RSVP
</button> </button>
</Transition> </Transition>
</div> </div>
@@ -34,7 +34,7 @@
<div v-else class="rsvp-bar__row"> <div v-else class="rsvp-bar__row">
<div class="rsvp-bar__cta glow-border glow-border--animated"> <div class="rsvp-bar__cta glow-border glow-border--animated">
<button class="rsvp-bar__cta-inner glass-inner" type="button" @click="$emit('open')"> <button class="rsvp-bar__cta-inner glass-inner" type="button" @click="$emit('open')">
I'm attending I'm attending!
</button> </button>
</div> </div>
<div class="rsvp-bar__bookmark glow-border glow-border--animated"> <div class="rsvp-bar__bookmark glow-border glow-border--animated">

View File

@@ -55,12 +55,12 @@ describe('EventCard', () => {
it('renders organizer badge when eventRole is organizer', () => { it('renders organizer badge when eventRole is organizer', () => {
const wrapper = mountCard({ eventRole: 'organizer' }) const wrapper = mountCard({ eventRole: 'organizer' })
expect(wrapper.text()).toContain('Organizer') expect(wrapper.text()).toContain('Organizing')
}) })
it('renders attendee badge when eventRole is attendee', () => { it('renders attendee badge when eventRole is attendee', () => {
const wrapper = mountCard({ eventRole: 'attendee' }) const wrapper = mountCard({ eventRole: 'attendee' })
expect(wrapper.text()).toContain('Attendee') expect(wrapper.text()).toContain('Attending')
}) })
it('renders watcher badge when eventRole is watcher', () => { it('renders watcher badge when eventRole is watcher', () => {

View File

@@ -160,13 +160,13 @@ describe('EventList', () => {
const wrapper = mountList() const wrapper = mountList()
const badge = wrapper.find('.event-card__badge--organizer') const badge = wrapper.find('.event-card__badge--organizer')
expect(badge.exists()).toBe(true) expect(badge.exists()).toBe(true)
expect(badge.text()).toBe('Organizer') expect(badge.text()).toBe('Organizing')
}) })
it('assigns attendee role when event has rsvpToken', () => { it('assigns attendee role when event has rsvpToken', () => {
const wrapper = mountList() const wrapper = mountList()
const badge = wrapper.find('.event-card__badge--attendee') const badge = wrapper.find('.event-card__badge--attendee')
expect(badge.exists()).toBe(true) expect(badge.exists()).toBe(true)
expect(badge.text()).toBe('Attendee') expect(badge.text()).toBe('Attending')
}) })
}) })

View File

@@ -6,7 +6,7 @@ describe('RsvpBar', () => {
it('renders CTA button when hasRsvp is false', () => { it('renders CTA button when hasRsvp is false', () => {
const wrapper = mount(RsvpBar) const wrapper = mount(RsvpBar)
expect(wrapper.find('.rsvp-bar__cta').exists()).toBe(true) expect(wrapper.find('.rsvp-bar__cta').exists()).toBe(true)
expect(wrapper.find('.rsvp-bar__cta-inner').text()).toBe("I'm attending") expect(wrapper.find('.rsvp-bar__cta-inner').text()).toBe("I'm attending!")
expect(wrapper.find('.rsvp-bar__status').exists()).toBe(false) expect(wrapper.find('.rsvp-bar__status').exists()).toBe(false)
}) })

View File

@@ -1,9 +1,6 @@
<template> <template>
<main class="create"> <main class="create">
<header class="create__header"> <h1 class="create__title">Great, a Party!</h1>
<RouterLink to="/" class="create__back" aria-label="Back to home">&larr;</RouterLink>
<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">
@@ -129,9 +125,9 @@
<!-- Cancel confirmation dialog --> <!-- Cancel confirmation dialog -->
<ConfirmDialog <ConfirmDialog
:open="confirmCancelOpen" :open="confirmCancelOpen"
title="Cancel attendance?" title="Cancel RSVP?"
message="Your attendance will be permanently cancelled." message="The organizer will no longer see you as attending."
confirm-label="Cancel attendance" confirm-label="Cancel RSVP"
cancel-label="Keep" cancel-label="Keep"
@confirm="handleCancelRsvp" @confirm="handleCancelRsvp"
@cancel="confirmCancelOpen = false" @cancel="confirmCancelOpen = false"
@@ -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 {
@@ -706,15 +677,15 @@ onMounted(fetchEvent)
font-family: inherit; font-family: inherit;
font-size: 1rem; font-size: 1rem;
font-weight: 700; font-weight: 700;
color: var(--color-danger); color: var(--color-danger-solid-text);
background: var(--color-danger-bg-strong); background: var(--color-danger-solid);
border: 1px solid var(--color-danger-border); border: none;
cursor: pointer; cursor: pointer;
transition: background 0.15s ease; transition: background 0.15s ease;
} }
.cancel-form__confirm:hover { .cancel-form__confirm:hover {
background: var(--color-danger-bg-hover); background: var(--color-danger-solid-hover);
} }
.cancel-form__confirm:disabled { .cancel-form__confirm:disabled {

View File

@@ -198,7 +198,7 @@ describe('EventDetailView', () => {
await flushPromises() await flushPromises()
expect(wrapper.find('.rsvp-bar__cta').exists()).toBe(true) expect(wrapper.find('.rsvp-bar__cta').exists()).toBe(true)
expect(wrapper.find('.rsvp-bar__cta').text()).toBe("I'm attending") expect(wrapper.find('.rsvp-bar__cta').text()).toBe("I'm attending!")
wrapper.unmount() wrapper.unmount()
}) })