diff --git a/CLAUDE.md b/CLAUDE.md
index ffc6b4a..0dce372 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -53,6 +53,8 @@ The following skills are available and should be used for their respective purpo
## Active Technologies
- Java 25 (backend), TypeScript 5.9 (frontend) + Spring Boot 3.5.x, Vue 3, Vue Router 5, openapi-fetch, openapi-typescript (007-view-event)
- PostgreSQL (JPA via Spring Data, Liquibase migrations) (007-view-event)
+- TypeScript 5.9 (frontend only) + Vue 3, Vue Router 5 (existing — no additions) (010-event-list-grouping)
+- localStorage via `useEventStorage.ts` composable (existing — no changes) (010-event-list-grouping)
## Recent Changes
- 007-view-event: Added Java 25 (backend), TypeScript 5.9 (frontend) + Spring Boot 3.5.x, Vue 3, Vue Router 5, openapi-fetch, openapi-typescript
diff --git a/frontend/e2e/home-events.spec.ts b/frontend/e2e/home-events.spec.ts
index 3b71b04..d8a4cfc 100644
--- a/frontend/e2e/home-events.spec.ts
+++ b/frontend/e2e/home-events.spec.ts
@@ -191,6 +191,133 @@ test.describe('FAB: Create Event Button', () => {
})
})
+test.describe('Temporal Grouping: Section Headers', () => {
+ test('events are distributed under correct section headers', async ({ page }) => {
+ // Use dates relative to "now" to ensure correct section assignment
+ const now = new Date()
+ const todayEvent: StoredEvent = {
+ eventToken: 'today-1',
+ title: 'Today Standup',
+ dateTime: new Date(now.getFullYear(), now.getMonth(), now.getDate(), 18, 0, 0).toISOString(),
+ expiryDate: '',
+ }
+ const laterEvent: StoredEvent = {
+ eventToken: 'later-1',
+ title: 'Future Conference',
+ dateTime: new Date(now.getFullYear() + 1, 0, 15, 10, 0, 0).toISOString(),
+ expiryDate: '',
+ }
+ await page.addInitScript(seedEvents([todayEvent, laterEvent, pastEvent]))
+ await page.goto('/')
+
+ // Verify section headers appear
+ await expect(page.getByRole('heading', { name: 'Today', level: 2 })).toBeVisible()
+ await expect(page.getByRole('heading', { name: 'Later', level: 2 })).toBeVisible()
+ await expect(page.getByRole('heading', { name: 'Past', level: 2 })).toBeVisible()
+
+ // Events are in the correct sections
+ const sections = page.locator('.event-section')
+ const todaySection = sections.filter({ has: page.getByRole('heading', { name: 'Today', level: 2 }) })
+ await expect(todaySection.getByText('Today Standup')).toBeVisible()
+
+ const laterSection = sections.filter({ has: page.getByRole('heading', { name: 'Later', level: 2 }) })
+ await expect(laterSection.getByText('Future Conference')).toBeVisible()
+
+ const pastSection = sections.filter({ has: page.getByRole('heading', { name: 'Past', level: 2 }) })
+ await expect(pastSection.getByText('New Year Party')).toBeVisible()
+ })
+
+ test('empty sections are not rendered', async ({ page }) => {
+ // Only a past event — no Today, This Week, or Later sections
+ await page.addInitScript(seedEvents([pastEvent]))
+ await page.goto('/')
+
+ await expect(page.getByRole('heading', { name: 'Past', level: 2 })).toBeVisible()
+ await expect(page.getByRole('heading', { name: 'Today', level: 2 })).toHaveCount(0)
+ await expect(page.getByRole('heading', { name: 'This Week', level: 2 })).toHaveCount(0)
+ await expect(page.getByRole('heading', { name: 'Next Week', level: 2 })).toHaveCount(0)
+ await expect(page.getByRole('heading', { name: 'Later', level: 2 })).toHaveCount(0)
+ })
+
+ test('Today section header has emphasis CSS class', async ({ page }) => {
+ const now = new Date()
+ const todayEvent: StoredEvent = {
+ eventToken: 'today-emph',
+ title: 'Emphasis Test',
+ dateTime: new Date(now.getFullYear(), now.getMonth(), now.getDate(), 20, 0, 0).toISOString(),
+ expiryDate: '',
+ }
+ await page.addInitScript(seedEvents([todayEvent]))
+ await page.goto('/')
+
+ const todayHeader = page.getByRole('heading', { name: 'Today', level: 2 })
+ await expect(todayHeader).toHaveClass(/section-header--emphasized/)
+ })
+})
+
+test.describe('Temporal Grouping: Date Subheaders', () => {
+ test('no date subheader in Today section', async ({ page }) => {
+ const now = new Date()
+ const todayEvent: StoredEvent = {
+ eventToken: 'today-sub',
+ title: 'No Subheader Test',
+ dateTime: new Date(now.getFullYear(), now.getMonth(), now.getDate(), 19, 0, 0).toISOString(),
+ expiryDate: '',
+ }
+ await page.addInitScript(seedEvents([todayEvent]))
+ await page.goto('/')
+
+ const todaySection = page.locator('.event-section').filter({
+ has: page.getByRole('heading', { name: 'Today', level: 2 }),
+ })
+ await expect(todaySection.locator('.date-subheader')).toHaveCount(0)
+ })
+
+ test('date subheaders appear in Later section', async ({ page }) => {
+ await page.addInitScript(seedEvents([futureEvent1, futureEvent2]))
+ await page.goto('/')
+
+ const laterSection = page.locator('.event-section').filter({
+ has: page.getByRole('heading', { name: 'Later', level: 2 }),
+ })
+ // Both future events are on different dates, so expect subheaders
+ const subheaders = laterSection.locator('.date-subheader')
+ await expect(subheaders).toHaveCount(2)
+ })
+
+ test('date subheaders appear in Past section', async ({ page }) => {
+ await page.addInitScript(seedEvents([pastEvent]))
+ await page.goto('/')
+
+ const pastSection = page.locator('.event-section').filter({
+ has: page.getByRole('heading', { name: 'Past', level: 2 }),
+ })
+ await expect(pastSection.locator('.date-subheader')).toHaveCount(1)
+ })
+})
+
+test.describe('Temporal Grouping: Time Display', () => {
+ test('future event cards show clock time', async ({ page }) => {
+ await page.addInitScript(seedEvents([futureEvent1]))
+ await page.goto('/')
+
+ const timeLabel = page.locator('.event-card__time')
+ const text = await timeLabel.first().textContent()
+ // Should show clock time (e.g., "18:00" or "6:00 PM"), not relative time
+ expect(text).toMatch(/\d{1,2}[:.]\d{2}/)
+ })
+
+ test('past event cards show relative time', async ({ page }) => {
+ await page.addInitScript(seedEvents([pastEvent]))
+ await page.goto('/')
+
+ const timeLabel = page.locator('.event-card__time')
+ const text = await timeLabel.first().textContent()
+ // Should show relative time like "X years ago" or "last year"
+ expect(text).toMatch(/ago|last|yesterday/)
+ })
+})
+
test.describe('US1: View My Events', () => {
test('displays all stored events with title and relative time', async ({ page }) => {
await page.addInitScript(seedEvents([futureEvent1, futureEvent2, pastEvent]))
diff --git a/frontend/src/components/DateSubheader.vue b/frontend/src/components/DateSubheader.vue
new file mode 100644
index 0000000..b7b2a23
--- /dev/null
+++ b/frontend/src/components/DateSubheader.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/EventCard.vue b/frontend/src/components/EventCard.vue
index 6792723..e788e32 100644
--- a/frontend/src/components/EventCard.vue
+++ b/frontend/src/components/EventCard.vue
@@ -9,7 +9,7 @@
>
{{ title }}
- {{ relativeTime }}
+ {{ displayTime }}
{{ eventRole === 'organizer' ? 'Organizer' : 'Attendee' }}
@@ -35,12 +35,21 @@ const props = defineProps<{
relativeTime: string
isPast: boolean
eventRole?: 'organizer' | 'attendee'
+ timeDisplayMode?: 'clock' | 'relative'
+ dateTime?: string
}>()
const emit = defineEmits<{
delete: [eventToken: string]
}>()
+const displayTime = computed(() => {
+ if (props.timeDisplayMode === 'clock' && props.dateTime) {
+ return new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit' }).format(new Date(props.dateTime))
+ }
+ return props.relativeTime
+})
+
const SWIPE_THRESHOLD = 80
const startX = ref(0)
diff --git a/frontend/src/components/EventList.vue b/frontend/src/components/EventList.vue
index 44fac59..a4f19b0 100644
--- a/frontend/src/components/EventList.vue
+++ b/frontend/src/components/EventList.vue
@@ -1,15 +1,30 @@
-
-
-
-
+
+
import { computed, ref } from 'vue'
import { useEventStorage, isValidStoredEvent } from '../composables/useEventStorage'
+import { useEventGrouping } from '../composables/useEventGrouping'
import { formatRelativeTime } from '../composables/useRelativeTime'
import EventCard from './EventCard.vue'
+import SectionHeader from './SectionHeader.vue'
+import DateSubheader from './DateSubheader.vue'
import ConfirmDialog from './ConfirmDialog.vue'
import type { StoredEvent } from '../composables/useEventStorage'
@@ -49,29 +67,26 @@ function cancelDelete() {
pendingDeleteToken.value = null
}
-function isPast(dateTime: string): boolean {
- return new Date(dateTime) < new Date()
-}
-
function getRole(event: StoredEvent): 'organizer' | 'attendee' | undefined {
if (event.organizerToken) return 'organizer'
if (event.rsvpToken) return 'attendee'
return undefined
}
-const sortedEvents = computed(() => {
+const groupedSections = computed(() => {
const valid = getStoredEvents().filter(isValidStoredEvent)
- const now = new Date()
- const upcoming = valid.filter((e) => new Date(e.dateTime) >= now)
- const past = valid.filter((e) => new Date(e.dateTime) < now)
- upcoming.sort((a, b) => new Date(a.dateTime).getTime() - new Date(b.dateTime).getTime())
- past.sort((a, b) => new Date(b.dateTime).getTime() - new Date(a.dateTime).getTime())
- return [...upcoming, ...past]
+ return useEventGrouping(valid)
})
diff --git a/frontend/src/components/__tests__/DateSubheader.spec.ts b/frontend/src/components/__tests__/DateSubheader.spec.ts
new file mode 100644
index 0000000..69a81d7
--- /dev/null
+++ b/frontend/src/components/__tests__/DateSubheader.spec.ts
@@ -0,0 +1,17 @@
+import { describe, it, expect } from 'vitest'
+import { mount } from '@vue/test-utils'
+import DateSubheader from '../DateSubheader.vue'
+
+describe('DateSubheader', () => {
+ it('renders the date label as an h3', () => {
+ const wrapper = mount(DateSubheader, { props: { label: 'Wed, 12 Mar' } })
+ const h3 = wrapper.find('h3')
+ expect(h3.exists()).toBe(true)
+ expect(h3.text()).toBe('Wed, 12 Mar')
+ })
+
+ it('applies the date-subheader class', () => {
+ const wrapper = mount(DateSubheader, { props: { label: 'Fri, 14 Mar' } })
+ expect(wrapper.find('.date-subheader').exists()).toBe(true)
+ })
+})
diff --git a/frontend/src/components/__tests__/EventCard.spec.ts b/frontend/src/components/__tests__/EventCard.spec.ts
index 6827533..382b92a 100644
--- a/frontend/src/components/__tests__/EventCard.spec.ts
+++ b/frontend/src/components/__tests__/EventCard.spec.ts
@@ -73,4 +73,28 @@ describe('EventCard', () => {
await wrapper.find('.event-card__delete').trigger('click')
expect(wrapper.emitted('delete')).toEqual([['abc-123']])
})
+
+ it('displays clock time when timeDisplayMode is clock', () => {
+ const wrapper = mountCard({
+ timeDisplayMode: 'clock',
+ dateTime: '2026-03-11T18:30:00',
+ })
+ const timeText = wrapper.find('.event-card__time').text()
+ // Locale-dependent: could be "18:30" or "06:30 PM"
+ expect(timeText).toMatch(/(?:18.30|6.30\s*PM)/i)
+ })
+
+ it('displays relative time when timeDisplayMode is relative', () => {
+ const wrapper = mountCard({
+ relativeTime: '3 days ago',
+ timeDisplayMode: 'relative',
+ dateTime: '2026-03-08T10:00:00',
+ })
+ expect(wrapper.find('.event-card__time').text()).toBe('3 days ago')
+ })
+
+ it('falls back to relativeTime when timeDisplayMode is not set', () => {
+ const wrapper = mountCard({ relativeTime: 'in 3 days' })
+ expect(wrapper.find('.event-card__time').text()).toBe('in 3 days')
+ })
})
diff --git a/frontend/src/components/__tests__/EventList.spec.ts b/frontend/src/components/__tests__/EventList.spec.ts
index d571954..4fab9d9 100644
--- a/frontend/src/components/__tests__/EventList.spec.ts
+++ b/frontend/src/components/__tests__/EventList.spec.ts
@@ -1,4 +1,4 @@
-import { describe, it, expect, vi, beforeEach } from 'vitest'
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { mount } from '@vue/test-utils'
import { createRouter, createMemoryHistory } from 'vue-router'
import EventList from '../EventList.vue'
@@ -11,10 +11,15 @@ const router = createRouter({
],
})
+// Fixed "now": Wednesday, 2026-03-11 12:00
+const NOW = new Date(2026, 2, 11, 12, 0, 0)
+
const mockEvents = [
- { eventToken: 'past-1', title: 'Past Event', dateTime: '2025-01-01T10:00:00Z', expiryDate: '' },
- { eventToken: 'future-1', title: 'Future Event', dateTime: '2027-06-15T10:00:00Z', expiryDate: '' },
- { eventToken: 'future-2', title: 'Soon Event', dateTime: '2027-01-01T10:00:00Z', expiryDate: '' },
+ { eventToken: 'past-1', title: 'Past Event', dateTime: '2026-03-01T10:00:00', expiryDate: '' },
+ { eventToken: 'later-1', title: 'Later Event', dateTime: '2027-06-15T10:00:00', expiryDate: '' },
+ { eventToken: 'today-1', title: 'Today Event', dateTime: '2026-03-11T18:00:00', expiryDate: '' },
+ { eventToken: 'week-1', title: 'This Week Event', dateTime: '2026-03-13T10:00:00', expiryDate: '' },
+ { eventToken: 'nextweek-1', title: 'Next Week Event', dateTime: '2026-03-16T10:00:00', expiryDate: '' },
]
vi.mock('../../composables/useEventStorage', () => ({
@@ -33,9 +38,12 @@ vi.mock('../../composables/useEventStorage', () => ({
vi.mock('../../composables/useRelativeTime', () => ({
formatRelativeTime: (dateTime: string) => {
- if (dateTime.startsWith('2025')) return '1 year ago'
+ if (dateTime.includes('03-01')) return '10 days ago'
if (dateTime.includes('06-15')) return 'in 1 year'
- return 'in 10 months'
+ if (dateTime.includes('03-11')) return 'in 6 hours'
+ if (dateTime.includes('03-13')) return 'in 2 days'
+ if (dateTime.includes('03-16')) return 'in 5 days'
+ return 'sometime'
},
}))
@@ -48,32 +56,85 @@ function mountList() {
describe('EventList', () => {
beforeEach(() => {
vi.useFakeTimers()
- vi.setSystemTime(new Date('2026-03-08T12:00:00Z'))
+ vi.setSystemTime(NOW)
})
- it('renders all valid events', () => {
+ afterEach(() => {
+ vi.useRealTimers()
+ })
+
+ it('renders section headers for each non-empty section', () => {
+ const wrapper = mountList()
+ const headers = wrapper.findAll('.section-header')
+ expect(headers).toHaveLength(5)
+ expect(headers[0]!.text()).toBe('Today')
+ expect(headers[1]!.text()).toBe('This Week')
+ expect(headers[2]!.text()).toBe('Next Week')
+ expect(headers[3]!.text()).toBe('Later')
+ expect(headers[4]!.text()).toBe('Past')
+ })
+
+ it('renders events within their correct sections', () => {
+ const wrapper = mountList()
+ const sections = wrapper.findAll('.event-section')
+ expect(sections).toHaveLength(5)
+
+ expect(sections[0]!.text()).toContain('Today Event')
+ expect(sections[1]!.text()).toContain('This Week Event')
+ expect(sections[2]!.text()).toContain('Next Week Event')
+ expect(sections[3]!.text()).toContain('Later Event')
+ expect(sections[4]!.text()).toContain('Past Event')
+ })
+
+ it('renders all valid events as cards', () => {
const wrapper = mountList()
const cards = wrapper.findAll('.event-card')
- expect(cards).toHaveLength(3)
- })
-
- it('sorts upcoming events before past events', () => {
- const wrapper = mountList()
- const titles = wrapper.findAll('.event-card__title').map((el) => el.text())
- // Upcoming events first (sorted ascending), then past events
- expect(titles[0]).toBe('Soon Event')
- expect(titles[1]).toBe('Future Event')
- expect(titles[2]).toBe('Past Event')
+ expect(cards).toHaveLength(5)
})
it('marks past events with isPast class', () => {
const wrapper = mountList()
- const cards = wrapper.findAll('.event-card')
- expect(cards).toHaveLength(3)
- // Last card should be past
- expect(cards[2]!.classes()).toContain('event-card--past')
- // First two should not be past
+ const pastSection = wrapper.findAll('.event-section')[4]!
+ const pastCards = pastSection.findAll('.event-card')
+ expect(pastCards).toHaveLength(1)
+ expect(pastCards[0]!.classes()).toContain('event-card--past')
+ })
+
+ it('does not mark non-past events with isPast class', () => {
+ const wrapper = mountList()
+ const todaySection = wrapper.findAll('.event-section')[0]!
+ const cards = todaySection.findAll('.event-card')
expect(cards[0]!.classes()).not.toContain('event-card--past')
- expect(cards[1]!.classes()).not.toContain('event-card--past')
+ })
+
+ it('sections have aria-label attributes', () => {
+ const wrapper = mountList()
+ const sections = wrapper.findAll('section')
+ expect(sections[0]!.attributes('aria-label')).toBe('Today')
+ expect(sections[1]!.attributes('aria-label')).toBe('This Week')
+ expect(sections[2]!.attributes('aria-label')).toBe('Next Week')
+ expect(sections[3]!.attributes('aria-label')).toBe('Later')
+ expect(sections[4]!.attributes('aria-label')).toBe('Past')
+ })
+
+ it('does not render date subheader in "Today" section', () => {
+ const wrapper = mountList()
+ const todaySection = wrapper.findAll('.event-section')[0]!
+ expect(todaySection.find('.date-subheader').exists()).toBe(false)
+ })
+
+ it('renders date subheaders in non-today sections', () => {
+ const wrapper = mountList()
+ const thisWeekSection = wrapper.findAll('.event-section')[1]!
+ expect(thisWeekSection.find('.date-subheader').exists()).toBe(true)
+
+ const nextWeekSection = wrapper.findAll('.event-section')[2]!
+ expect(nextWeekSection.find('.date-subheader').exists()).toBe(true)
+
+ const laterSection = wrapper.findAll('.event-section')[3]!
+ expect(laterSection.find('.date-subheader').exists()).toBe(true)
+
+ const pastSection = wrapper.findAll('.event-section')[4]!
+ expect(pastSection.find('.date-subheader').exists()).toBe(true)
})
})
diff --git a/frontend/src/components/__tests__/SectionHeader.spec.ts b/frontend/src/components/__tests__/SectionHeader.spec.ts
new file mode 100644
index 0000000..7ab334b
--- /dev/null
+++ b/frontend/src/components/__tests__/SectionHeader.spec.ts
@@ -0,0 +1,27 @@
+import { describe, it, expect } from 'vitest'
+import { mount } from '@vue/test-utils'
+import SectionHeader from '../SectionHeader.vue'
+
+describe('SectionHeader', () => {
+ it('renders the section label as an h2', () => {
+ const wrapper = mount(SectionHeader, { props: { label: 'Today' } })
+ const h2 = wrapper.find('h2')
+ expect(h2.exists()).toBe(true)
+ expect(h2.text()).toBe('Today')
+ })
+
+ it('does not apply emphasized class by default', () => {
+ const wrapper = mount(SectionHeader, { props: { label: 'Later' } })
+ expect(wrapper.find('.section-header--emphasized').exists()).toBe(false)
+ })
+
+ it('applies emphasized class when emphasized prop is true', () => {
+ const wrapper = mount(SectionHeader, { props: { label: 'Today', emphasized: true } })
+ expect(wrapper.find('.section-header--emphasized').exists()).toBe(true)
+ })
+
+ it('does not apply emphasized class when emphasized prop is false', () => {
+ const wrapper = mount(SectionHeader, { props: { label: 'Past', emphasized: false } })
+ expect(wrapper.find('.section-header--emphasized').exists()).toBe(false)
+ })
+})
diff --git a/frontend/src/components/__tests__/useEventGrouping.spec.ts b/frontend/src/components/__tests__/useEventGrouping.spec.ts
new file mode 100644
index 0000000..92a7750
--- /dev/null
+++ b/frontend/src/components/__tests__/useEventGrouping.spec.ts
@@ -0,0 +1,158 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
+import { useEventGrouping } from '../../composables/useEventGrouping'
+import type { StoredEvent } from '../../composables/useEventStorage'
+
+function makeEvent(overrides: Partial & { dateTime: string }): StoredEvent {
+ return {
+ eventToken: `evt-${Math.random().toString(36).slice(2, 8)}`,
+ title: 'Test Event',
+ expiryDate: '',
+ ...overrides,
+ }
+}
+
+describe('useEventGrouping', () => {
+ // Fixed "now": Wednesday, 2026-03-11 12:00 local
+ const NOW = new Date(2026, 2, 11, 12, 0, 0)
+
+ beforeEach(() => {
+ vi.useFakeTimers()
+ vi.setSystemTime(NOW)
+ })
+
+ afterEach(() => {
+ vi.useRealTimers()
+ })
+
+ it('returns empty array when no events', () => {
+ const sections = useEventGrouping([], NOW)
+ expect(sections).toEqual([])
+ })
+
+ it('classifies a today event into "today" section', () => {
+ const event = makeEvent({ dateTime: '2026-03-11T18:30:00' })
+ const sections = useEventGrouping([event], NOW)
+ expect(sections).toHaveLength(1)
+ expect(sections[0]!.key).toBe('today')
+ expect(sections[0]!.label).toBe('Today')
+ expect(sections[0]!.dateGroups[0]!.events).toHaveLength(1)
+ })
+
+ it('classifies events into all five sections', () => {
+ const events = [
+ makeEvent({ title: 'Today', dateTime: '2026-03-11T10:00:00' }),
+ makeEvent({ title: 'This Week', dateTime: '2026-03-13T10:00:00' }), // Friday (same week)
+ makeEvent({ title: 'Next Week', dateTime: '2026-03-16T10:00:00' }), // Monday next week
+ makeEvent({ title: 'Later', dateTime: '2026-03-30T10:00:00' }), // far future
+ makeEvent({ title: 'Past', dateTime: '2026-03-09T10:00:00' }), // Monday (past)
+ ]
+ const sections = useEventGrouping(events, NOW)
+ expect(sections).toHaveLength(5)
+ expect(sections[0]!.key).toBe('today')
+ expect(sections[1]!.key).toBe('thisWeek')
+ expect(sections[2]!.key).toBe('nextWeek')
+ expect(sections[3]!.key).toBe('later')
+ expect(sections[4]!.key).toBe('past')
+ })
+
+ it('omits empty sections', () => {
+ const events = [
+ makeEvent({ title: 'Today', dateTime: '2026-03-11T10:00:00' }),
+ makeEvent({ title: 'Past', dateTime: '2026-03-01T10:00:00' }),
+ ]
+ const sections = useEventGrouping(events, NOW)
+ expect(sections).toHaveLength(2)
+ expect(sections[0]!.key).toBe('today')
+ expect(sections[1]!.key).toBe('past')
+ })
+
+ it('sorts upcoming events ascending by time', () => {
+ const events = [
+ makeEvent({ title: 'Later', dateTime: '2026-03-11T20:00:00' }),
+ makeEvent({ title: 'Earlier', dateTime: '2026-03-11T08:00:00' }),
+ ]
+ const sections = useEventGrouping(events, NOW)
+ const todayEvents = sections[0]!.dateGroups[0]!.events
+ expect(todayEvents[0]!.title).toBe('Earlier')
+ expect(todayEvents[1]!.title).toBe('Later')
+ })
+
+ it('sorts past events descending by time (most recent first)', () => {
+ const events = [
+ makeEvent({ title: 'Older', dateTime: '2026-03-01T10:00:00' }),
+ makeEvent({ title: 'Newer', dateTime: '2026-03-09T10:00:00' }),
+ ]
+ const sections = useEventGrouping(events, NOW)
+ const pastEvents = sections[0]!.dateGroups
+ expect(pastEvents[0]!.events[0]!.title).toBe('Newer')
+ expect(pastEvents[1]!.events[0]!.title).toBe('Older')
+ })
+
+ it('groups events by date within a section', () => {
+ const events = [
+ makeEvent({ title: 'Fri AM', dateTime: '2026-03-13T09:00:00' }),
+ makeEvent({ title: 'Fri PM', dateTime: '2026-03-13T18:00:00' }),
+ makeEvent({ title: 'Sat', dateTime: '2026-03-14T12:00:00' }),
+ ]
+ const sections = useEventGrouping(events, NOW)
+ expect(sections[0]!.key).toBe('thisWeek')
+ const dateGroups = sections[0]!.dateGroups
+ expect(dateGroups).toHaveLength(2) // Friday and Saturday
+ expect(dateGroups[0]!.events).toHaveLength(2) // Two Friday events
+ expect(dateGroups[1]!.events).toHaveLength(1) // One Saturday event
+ })
+
+ it('sets showSubheader=false for "today" section', () => {
+ const event = makeEvent({ dateTime: '2026-03-11T18:00:00' })
+ const sections = useEventGrouping([event], NOW)
+ expect(sections[0]!.dateGroups[0]!.showSubheader).toBe(false)
+ })
+
+ it('sets showSubheader=true for non-today sections', () => {
+ const events = [
+ makeEvent({ dateTime: '2026-03-13T10:00:00' }), // thisWeek
+ makeEvent({ dateTime: '2026-03-30T10:00:00' }), // later (beyond next week)
+ makeEvent({ dateTime: '2026-03-01T10:00:00' }), // past
+ ]
+ const sections = useEventGrouping(events, NOW)
+ for (const section of sections) {
+ for (const group of section.dateGroups) {
+ expect(group.showSubheader).toBe(true)
+ }
+ }
+ })
+
+ it('sets emphasized=true only for "today" section', () => {
+ const events = [
+ makeEvent({ dateTime: '2026-03-11T18:00:00' }),
+ makeEvent({ dateTime: '2026-03-30T10:00:00' }),
+ ]
+ const sections = useEventGrouping(events, NOW)
+ expect(sections[0]!.emphasized).toBe(true) // today
+ expect(sections[1]!.emphasized).toBe(false) // later
+ })
+
+ it('on Sunday, tomorrow (Monday) goes to "nextWeek" not "thisWeek"', () => {
+ // Sunday 2026-03-15
+ const sunday = new Date(2026, 2, 15, 12, 0, 0)
+ const mondayEvent = makeEvent({ title: 'Monday', dateTime: '2026-03-16T10:00:00' })
+ const sections = useEventGrouping([mondayEvent], sunday)
+ expect(sections).toHaveLength(1)
+ expect(sections[0]!.key).toBe('nextWeek')
+ })
+
+ it('on Sunday, today events still appear under "today"', () => {
+ const sunday = new Date(2026, 2, 15, 12, 0, 0)
+ const todayEvent = makeEvent({ dateTime: '2026-03-15T18:00:00' })
+ const sections = useEventGrouping([todayEvent], sunday)
+ expect(sections[0]!.key).toBe('today')
+ })
+
+ it('dateGroup labels are formatted via Intl', () => {
+ const event = makeEvent({ dateTime: '2026-03-13T10:00:00' }) // Friday
+ const sections = useEventGrouping([event], NOW)
+ const label = sections[0]!.dateGroups[0]!.label
+ // The exact format depends on locale, but should contain the day number
+ expect(label).toContain('13')
+ })
+})
diff --git a/frontend/src/composables/useEventGrouping.ts b/frontend/src/composables/useEventGrouping.ts
new file mode 100644
index 0000000..047d9c8
--- /dev/null
+++ b/frontend/src/composables/useEventGrouping.ts
@@ -0,0 +1,149 @@
+import type { StoredEvent } from './useEventStorage'
+
+export type SectionKey = 'today' | 'thisWeek' | 'nextWeek' | 'later' | 'past'
+
+export interface DateGroup {
+ dateKey: string
+ label: string
+ events: StoredEvent[]
+ showSubheader: boolean
+}
+
+export interface EventSection {
+ key: SectionKey
+ label: string
+ dateGroups: DateGroup[]
+ emphasized: boolean
+}
+
+const SECTION_ORDER: SectionKey[] = ['today', 'thisWeek', 'nextWeek', 'later', 'past']
+
+const SECTION_LABELS: Record = {
+ today: 'Today',
+ thisWeek: 'This Week',
+ nextWeek: 'Next Week',
+ later: 'Later',
+ past: 'Past',
+}
+
+function startOfDay(date: Date): Date {
+ const d = new Date(date)
+ d.setHours(0, 0, 0, 0)
+ return d
+}
+
+function endOfDay(date: Date): Date {
+ const d = new Date(date)
+ d.setHours(23, 59, 59, 999)
+ return d
+}
+
+function endOfWeek(date: Date): Date {
+ const d = new Date(date)
+ const dayOfWeek = d.getDay() // 0=Sun, 1=Mon, ..., 6=Sat
+ // ISO week: Monday is first day. End of week = Sunday.
+ // If today is Sunday (0), end of week is today.
+ // Otherwise, days until Sunday = 7 - dayOfWeek
+ const daysUntilSunday = dayOfWeek === 0 ? 0 : 7 - dayOfWeek
+ d.setDate(d.getDate() + daysUntilSunday)
+ return endOfDay(d)
+}
+
+function endOfNextWeek(date: Date): Date {
+ const thisWeekEnd = endOfWeek(date)
+ const d = new Date(thisWeekEnd)
+ d.setDate(d.getDate() + 7)
+ return endOfDay(d)
+}
+
+function toDateKey(date: Date): string {
+ const y = date.getFullYear()
+ const m = String(date.getMonth() + 1).padStart(2, '0')
+ const d = String(date.getDate()).padStart(2, '0')
+ return `${y}-${m}-${d}`
+}
+
+function formatDateLabel(date: Date): string {
+ return new Intl.DateTimeFormat(undefined, {
+ weekday: 'short',
+ day: 'numeric',
+ month: 'short',
+ }).format(date)
+}
+
+function classifyEvent(eventDate: Date, todayStart: Date, todayEnd: Date, weekEnd: Date, nextWeekEnd: Date): SectionKey {
+ if (eventDate < todayStart) return 'past'
+ if (eventDate <= todayEnd) return 'today'
+ if (eventDate <= weekEnd) return 'thisWeek'
+ if (eventDate <= nextWeekEnd) return 'nextWeek'
+ return 'later'
+}
+
+export function useEventGrouping(events: StoredEvent[], now: Date = new Date()): EventSection[] {
+ const todayStart = startOfDay(now)
+ const todayEnd = endOfDay(now)
+ const weekEnd = endOfWeek(now)
+ const nextWeekEnd = endOfNextWeek(now)
+
+ // Classify events into sections
+ const buckets: Record = {
+ today: [],
+ thisWeek: [],
+ nextWeek: [],
+ later: [],
+ past: [],
+ }
+
+ for (const event of events) {
+ const eventDate = new Date(event.dateTime)
+ const section = classifyEvent(eventDate, todayStart, todayEnd, weekEnd, nextWeekEnd)
+ buckets[section].push(event)
+ }
+
+ // Build sections
+ const sections: EventSection[] = []
+
+ for (const key of SECTION_ORDER) {
+ const sectionEvents = buckets[key]
+ if (sectionEvents.length === 0) continue
+
+ // Sort events
+ const ascending = key !== 'past'
+ sectionEvents.sort((a, b) => {
+ const diff = new Date(a.dateTime).getTime() - new Date(b.dateTime).getTime()
+ return ascending ? diff : -diff
+ })
+
+ // Group by date
+ const dateGroupMap = new Map()
+ for (const event of sectionEvents) {
+ const dateKey = toDateKey(new Date(event.dateTime))
+ const group = dateGroupMap.get(dateKey)
+ if (group) {
+ group.push(event)
+ } else {
+ dateGroupMap.set(dateKey, [event])
+ }
+ }
+
+ // Convert to DateGroup array (order preserved from sorted events)
+ const dateGroups: DateGroup[] = []
+ for (const [dateKey, groupEvents] of dateGroupMap) {
+ dateGroups.push({
+ dateKey,
+ label: formatDateLabel(new Date(groupEvents[0]!.dateTime)),
+ events: groupEvents,
+ showSubheader: key !== 'today',
+ })
+ }
+
+ sections.push({
+ key,
+ label: SECTION_LABELS[key],
+ dateGroups,
+ emphasized: key === 'today',
+ })
+ }
+
+ return sections
+}
diff --git a/specs/010-event-list-grouping/checklists/requirements.md b/specs/010-event-list-grouping/checklists/requirements.md
new file mode 100644
index 0000000..e2e45c5
--- /dev/null
+++ b/specs/010-event-list-grouping/checklists/requirements.md
@@ -0,0 +1,35 @@
+# Specification Quality Checklist: Event List Temporal Grouping
+
+**Purpose**: Validate specification completeness and quality before proceeding to planning
+**Created**: 2026-03-08
+**Feature**: [spec.md](../spec.md)
+
+## Content Quality
+
+- [x] No implementation details (languages, frameworks, APIs)
+- [x] Focused on user value and business needs
+- [x] Written for non-technical stakeholders
+- [x] All mandatory sections completed
+
+## Requirement Completeness
+
+- [x] No [NEEDS CLARIFICATION] markers remain
+- [x] Requirements are testable and unambiguous
+- [x] Success criteria are measurable
+- [x] Success criteria are technology-agnostic (no implementation details)
+- [x] All acceptance scenarios are defined
+- [x] Edge cases are identified
+- [x] Scope is clearly bounded
+- [x] Dependencies and assumptions identified
+
+## Feature Readiness
+
+- [x] All functional requirements have clear acceptance criteria
+- [x] User scenarios cover primary flows
+- [x] Feature meets measurable outcomes defined in Success Criteria
+- [x] No implementation details leak into specification
+
+## Notes
+
+- All items pass. Spec is ready for `/speckit.clarify` or `/speckit.plan`.
+- One minor note: the Assumptions section mentions `Intl` API and `localStorage` — these are context references to existing behavior, not prescriptive implementation details.
diff --git a/specs/010-event-list-grouping/data-model.md b/specs/010-event-list-grouping/data-model.md
new file mode 100644
index 0000000..fc0280b
--- /dev/null
+++ b/specs/010-event-list-grouping/data-model.md
@@ -0,0 +1,91 @@
+# Data Model: Event List Temporal Grouping
+
+**Feature**: 010-event-list-grouping | **Date**: 2026-03-08
+
+## Existing Entities (no changes)
+
+### StoredEvent
+
+**Location**: `frontend/src/composables/useEventStorage.ts`
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `eventToken` | `string` | UUID v4, unique identifier |
+| `organizerToken` | `string?` | UUID v4, present if user is organizer |
+| `title` | `string` | Event title |
+| `dateTime` | `string` | ISO 8601 with UTC offset (e.g., `"2026-03-15T20:00:00+01:00"`) |
+| `expiryDate` | `string` | ISO 8601 expiry date |
+| `rsvpToken` | `string?` | Present if user has RSVP'd |
+| `rsvpName` | `string?` | Name used for RSVP |
+
+**Note**: No changes to `StoredEvent`. The `dateTime` field is the sole input for all grouping and sorting logic.
+
+## New Types (frontend only)
+
+### SectionKey
+
+```typescript
+type SectionKey = 'today' | 'thisWeek' | 'later' | 'past'
+```
+
+Enum-like union type for the four temporal sections. Ordering is fixed: today → thisWeek → later → past.
+
+### EventSection
+
+```typescript
+interface EventSection {
+ key: SectionKey
+ label: string // Display label: "Today", "This Week", "Later", "Past"
+ dateGroups: DateGroup[]
+ emphasized: boolean // true only for 'today' section
+}
+```
+
+Represents one temporal section in the grouped list. Sections with no events are omitted entirely (never constructed).
+
+### DateGroup
+
+```typescript
+interface DateGroup {
+ dateKey: string // YYYY-MM-DD (for keying/dedup)
+ label: string // Formatted via Intl.DateTimeFormat, e.g., "Wed, 12 Mar"
+ events: StoredEvent[] // Events on this date, sorted by time
+ showSubheader: boolean // false for "Today" section (FR-005)
+}
+```
+
+Groups events within a section by their specific calendar date. The `showSubheader` flag controls whether the date subheader is rendered (always false in "Today" section per FR-005).
+
+## Grouping Algorithm
+
+```
+Input: StoredEvent[], now: Date
+Output: EventSection[]
+
+1. Compute boundaries:
+ - startOfToday = today at 00:00:00 local
+ - endOfToday = today at 23:59:59.999 local
+ - endOfWeek = next Sunday at 23:59:59.999 local (or today if today is Sunday)
+
+2. Classify each event by dateTime:
+ - dateTime < startOfToday → "past"
+ - startOfToday ≤ dateTime ≤ endOfToday → "today"
+ - endOfToday < dateTime ≤ endOfWeek → "thisWeek"
+ - dateTime > endOfWeek → "later"
+
+3. Within each section, group by calendar date (YYYY-MM-DD)
+
+4. Sort:
+ - today/thisWeek/later: date groups ascending, events within group ascending by time
+ - past: date groups descending, events within group descending by time
+
+5. Emit only non-empty sections in fixed order: today, thisWeek, later, past
+```
+
+## State Transitions
+
+None. Events are static data in localStorage. Temporal classification is computed on each render based on current time. No event mutation occurs.
+
+## Validation Rules
+
+No new validation. Existing `isValidStoredEvent()` in `useEventStorage.ts` already validates the `dateTime` field as a parseable ISO 8601 string.
diff --git a/specs/010-event-list-grouping/plan.md b/specs/010-event-list-grouping/plan.md
new file mode 100644
index 0000000..8f54f22
--- /dev/null
+++ b/specs/010-event-list-grouping/plan.md
@@ -0,0 +1,72 @@
+# Implementation Plan: Event List Temporal Grouping
+
+**Branch**: `010-event-list-grouping` | **Date**: 2026-03-08 | **Spec**: `specs/010-event-list-grouping/spec.md`
+**Input**: Feature specification from `/specs/010-event-list-grouping/spec.md`
+
+## Summary
+
+Extend the existing flat event list with temporal section grouping (Today, This Week, Later, Past). The feature is purely client-side: the existing `EventList.vue` computed property that separates events into upcoming/past is refactored into a four-section grouping with section headers, date subheaders, and context-aware time formatting. No backend changes, no new dependencies.
+
+## Technical Context
+
+**Language/Version**: TypeScript 5.9 (frontend only)
+**Primary Dependencies**: Vue 3, Vue Router 5 (existing — no additions)
+**Storage**: localStorage via `useEventStorage.ts` composable (existing — no changes)
+**Testing**: Vitest (unit), Playwright + MSW (E2E)
+**Target Platform**: PWA, mobile-first, all modern browsers
+**Project Type**: Web application (frontend enhancement)
+**Performance Goals**: Grouping computation < 1ms for 100 events (trivial — single array pass)
+**Constraints**: Client-side only, no additional network requests, offline-capable
+**Scale/Scope**: Typically < 50 events per user in localStorage
+
+## Constitution Check
+
+*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
+
+| Principle | Status | Notes |
+|-----------|--------|-------|
+| I. Privacy by Design | PASS | No new data collection. Grouping uses existing `dateTime` field. No external services. |
+| II. Test-Driven Methodology | PASS | Unit tests for grouping logic + E2E tests for all user stories planned. TDD enforced. |
+| III. API-First Development | N/A | No API changes — purely frontend enhancement. |
+| IV. Simplicity & Quality | PASS | Minimal new code: one composable for grouping, template changes in EventList. No over-engineering. |
+| V. Dependency Discipline | PASS | No new dependencies. Uses browser `Intl` API and existing `Date` methods. |
+| VI. Accessibility | PASS | Section headers use semantic HTML (``/``), ARIA landmarks, keyboard navigable. WCAG AA contrast enforced. |
+
+**Gate result: PASS** — no violations.
+
+## Project Structure
+
+### Documentation (this feature)
+
+```text
+specs/010-event-list-grouping/
+├── plan.md # This file
+├── research.md # Phase 0 output
+├── data-model.md # Phase 1 output
+├── spec.md # Feature specification
+└── tasks.md # Phase 2 output (via /speckit.tasks)
+```
+
+### Source Code (repository root)
+
+```text
+frontend/
+├── src/
+│ ├── components/
+│ │ ├── EventList.vue # MODIFY — add section grouping to template + computed
+│ │ ├── EventCard.vue # MODIFY — add time format mode prop
+│ │ ├── SectionHeader.vue # NEW — temporal section header component
+│ │ └── DateSubheader.vue # NEW — date subheader component
+│ ├── composables/
+│ │ ├── useEventGrouping.ts # NEW — grouping logic (pure function)
+│ │ ├── useRelativeTime.ts # EXISTING — no changes
+│ │ └── useEventStorage.ts # EXISTING — no changes
+│ └── components/__tests__/
+│ ├── EventList.spec.ts # MODIFY — update for grouped structure
+│ ├── EventCard.spec.ts # MODIFY — add time format tests
+│ └── useEventGrouping.spec.ts # NEW — unit tests for grouping logic
+├── e2e/
+│ └── home-events.spec.ts # MODIFY — add temporal grouping E2E tests
+```
+
+**Structure Decision**: Frontend-only changes. Two new small components (SectionHeader, DateSubheader) and one new composable (useEventGrouping). Existing components modified minimally.
diff --git a/specs/010-event-list-grouping/research.md b/specs/010-event-list-grouping/research.md
new file mode 100644
index 0000000..864df70
--- /dev/null
+++ b/specs/010-event-list-grouping/research.md
@@ -0,0 +1,118 @@
+# Research: Event List Temporal Grouping
+
+**Feature**: 010-event-list-grouping | **Date**: 2026-03-08
+
+## 1. Week Boundary Calculation
+
+**Decision**: Use ISO 8601 week convention (Monday = first day of week). "This Week" spans from tomorrow through Sunday of the current week.
+
+**Rationale**: The spec explicitly states "ISO convention where Monday is the first day of the week" (Assumptions section). The browser's `Date.getDay()` returns 0 for Sunday, 1 for Monday — straightforward to compute end-of-week as next Sunday 23:59:59.
+
+**Implementation**: Compare event date against:
+- `startOfToday` and `endOfToday` for "Today"
+- `startOfTomorrow` and `endOfSunday` for "This Week"
+- `after endOfSunday` for "Later"
+- `before startOfToday` for "Past"
+
+Edge case (spec scenario 4): On Sunday, "This Week" is empty (tomorrow is already next week Monday), so events for Monday appear under "Later". This falls out naturally from the algorithm.
+
+**Alternatives considered**:
+- Using a date library (date-fns, luxon): Rejected — dependency discipline (Constitution V). Native `Date` + `Intl` is sufficient for this logic.
+- Locale-dependent week start: Rejected — spec mandates ISO convention explicitly.
+
+## 2. Date Formatting for Subheaders
+
+**Decision**: Use `Intl.DateTimeFormat` with `{ weekday: 'short', day: 'numeric', month: 'short' }` to produce labels like "Wed, 12 Mar".
+
+**Rationale**: Consistent with existing use of `Intl.RelativeTimeFormat` in `useRelativeTime.ts`. Respects user locale for month/weekday names. No external dependency needed.
+
+**Alternatives considered**:
+- Hardcoded English day/month names: Rejected — the project already uses `Intl` APIs for locale awareness.
+- Full date format (e.g., "Wednesday, March 12, 2026"): Rejected — too long for mobile cards.
+
+## 3. Time Display on Event Cards
+
+**Decision**: Add a `timeDisplayMode` prop to `EventCard.vue` with two modes:
+- `'clock'`: Shows formatted time (e.g., "18:30") using `Intl.DateTimeFormat` with `{ hour: '2-digit', minute: '2-digit' }`
+- `'relative'`: Shows relative time (e.g., "3 days ago") using existing `formatRelativeTime()`
+
+**Rationale**: Spec requires different time representations per section: clock time for Today/This Week/Later, relative time for Past. A prop-driven approach keeps EventCard stateless regarding section context.
+
+**Alternatives considered**:
+- EventCard determining its own display mode: Rejected — card shouldn't know about sections; parent owns that context.
+- Passing a pre-formatted string: Viable but less type-safe. A mode enum is clearer.
+
+## 4. Grouping Data Structure
+
+**Decision**: The `useEventGrouping` composable returns an array of section objects:
+
+```typescript
+interface EventSection {
+ key: 'today' | 'thisWeek' | 'later' | 'past'
+ label: string // "Today", "This Week", "Later", "Past"
+ events: GroupedEvent[]
+}
+
+interface DateGroup {
+ date: string // ISO date string (YYYY-MM-DD) for keying
+ label: string // Formatted date label (e.g., "Wed, 12 Mar")
+ events: StoredEvent[]
+}
+
+interface GroupedEvent extends StoredEvent {
+ dateGroup: string // ISO date for sub-grouping
+}
+```
+
+Actually, simpler: the composable returns sections, each containing date groups, each containing events.
+
+```typescript
+interface EventSection {
+ key: 'today' | 'thisWeek' | 'later' | 'past'
+ label: string
+ dateGroups: DateGroup[]
+}
+
+interface DateGroup {
+ dateKey: string // YYYY-MM-DD
+ label: string // Formatted: "Wed, 12 Mar"
+ events: StoredEvent[]
+}
+```
+
+**Rationale**: Two-level grouping (section → date → events) matches the spec's hierarchy. Empty sections are simply omitted from the returned array (FR-002). The "Today" section still has one DateGroup but the template skips rendering its subheader (FR-005).
+
+**Alternatives considered**:
+- Flat list with section markers: Harder to template, mixes data and presentation.
+- Map/Record structure: Arrays preserve ordering guarantees (Today → This Week → Later → Past).
+
+## 5. Visual Emphasis for "Today" Section
+
+**Decision**: Apply a CSS class `.section--today` to the Today section that uses:
+- Slightly larger section header (font-weight: 800, font-size: 1.1rem vs 700/1rem for others)
+- A subtle left border accent using the primary gradient pink (`#F06292`)
+
+**Rationale**: Consistent with Electric Dusk design system. Subtle enough not to distract but visually distinct. The existing past-event fade (opacity: 0.6, saturate: 0.5) already handles the other end of the spectrum.
+
+**Alternatives considered**:
+- Background highlight: Could clash with card backgrounds on mobile.
+- Icon/emoji prefix: Spec doesn't mention icons; keep it typography-driven per design system.
+
+## 6. Accessibility Considerations
+
+**Decision**:
+- Section headers are `` elements
+- Date subheaders are `` elements
+- The event list container keeps its existing `role="list"`
+- Each section is a `` element with `aria-label` matching the section label
+
+**Rationale**: Constitution VI requires semantic HTML and ARIA. The heading hierarchy (h2 > h3) provides screen reader navigation landmarks. The `` element with label allows assistive technology to announce section boundaries.
+
+## 7. Existing Test Updates
+
+**Decision**:
+- Existing `EventList.spec.ts` unit tests will be updated to account for the new grouped structure (sections instead of flat list)
+- Existing `home-events.spec.ts` E2E tests will be extended with new scenarios for temporal grouping
+- New `useEventGrouping.spec.ts` tests the pure grouping function in isolation
+
+**Rationale**: TDD (Constitution II). The grouping logic is a pure function — ideal for thorough unit testing with various date combinations and edge cases.
diff --git a/specs/010-event-list-grouping/spec.md b/specs/010-event-list-grouping/spec.md
new file mode 100644
index 0000000..8d3e4a5
--- /dev/null
+++ b/specs/010-event-list-grouping/spec.md
@@ -0,0 +1,138 @@
+# Feature Specification: Event List Temporal Grouping
+
+**Feature Branch**: `010-event-list-grouping`
+**Created**: 2026-03-08
+**Status**: Draft
+**Input**: User description: "Extend the event list with temporal grouping so users know if an event is today, this week, or further in the future."
+
+## User Scenarios & Testing *(mandatory)*
+
+### User Story 1 - Temporal Section Headers (Priority: P1)
+
+As a user viewing my event list, I want events grouped under clear date-based section headers so I can instantly see what's happening today, this week, and later without reading individual dates.
+
+The list displays events under these temporal sections (in order):
+
+1. **Today** — events happening today
+2. **This Week** — events from tomorrow through end of current week (Sunday)
+3. **Later** — upcoming events beyond this week
+4. **Past** — events that have already occurred
+
+Each section only appears if it contains at least one event. Empty sections are hidden entirely.
+
+**Why this priority**: The core value of this feature — temporal orientation at a glance. Without section headers, the rest of the feature has no foundation.
+
+**Independent Test**: Can be fully tested by adding events with various dates to localStorage and verifying they appear under the correct section headers.
+
+**Acceptance Scenarios**:
+
+1. **Given** a user has events today, tomorrow, next week, and last week, **When** they view the event list, **Then** they see four sections: "Today", "This Week", "Later", and "Past" with events correctly distributed.
+2. **Given** a user has only events for today, **When** they view the event list, **Then** only the "Today" section is visible — no empty sections appear.
+3. **Given** a user has no events at all, **When** they view the event list, **Then** the empty state is shown (as currently implemented).
+4. **Given** it is Sunday and an event is scheduled for Monday, **When** the user views the list, **Then** the Monday event appears under "Later" (not "This Week"), because the current week ends on Sunday.
+
+---
+
+### User Story 2 - Date Subheaders Within Sections (Priority: P2)
+
+Within each section (except "Today"), events are further grouped by their specific date with a subheader showing the formatted date (e.g., "Sat, 17 Sep"). This mirrors the inspiration layout where individual dates appear as smaller headings under the main temporal section.
+
+Within the "Today" section, no date subheader is needed since all events share the same date.
+
+**Why this priority**: Adds finer-grained orientation within sections — especially important when "This Week" or "Later" contain multiple events across different days.
+
+**Independent Test**: Can be tested by adding multiple events on different days within the same temporal section and verifying date subheaders appear.
+
+**Acceptance Scenarios**:
+
+1. **Given** a user has events on Wednesday and Friday of this week, **When** they view the "This Week" section, **Then** events are grouped under date subheaders like "Wed, 12 Mar" and "Fri, 14 Mar".
+2. **Given** a user has three events today, **When** they view the "Today" section, **Then** no date subheader appears — events are listed directly under the "Today" header.
+3. **Given** two events on the same future date, **When** the user views the list, **Then** both appear under a single date subheader for that day, sorted by time.
+
+---
+
+### User Story 3 - Enhanced Event Card Information (Priority: P2)
+
+Each event card within the grouped list shows time information relevant to its context:
+
+- **Today's events**: Show the time (e.g., "18:30") prominently, since the date is implied by the section.
+- **Future events**: Show the time (e.g., "18:30") — the date is provided by the subheader.
+- **Past events**: Continue showing relative time (e.g., "3 days ago") as currently implemented, since exact time matters less.
+
+The existing role badges (Organizer/Attendee) and event title remain as-is.
+
+**Why this priority**: Completes the information design — users need different time representations depending on temporal context.
+
+**Independent Test**: Can be tested by checking that event cards display the correct time format based on which section they appear in.
+
+**Acceptance Scenarios**:
+
+1. **Given** an event today at 18:30, **When** the user views the "Today" section, **Then** the card shows "18:30" (not "in 3 hours").
+2. **Given** an event on Friday at 10:00, **When** the user views it under "This Week", **Then** the card shows "10:00".
+3. **Given** a past event from 3 days ago, **When** the user views the "Past" section, **Then** the card shows "3 days ago" as it does currently.
+
+---
+
+### User Story 4 - Today Section Visual Emphasis (Priority: P3)
+
+The "Today" section header and its event cards receive subtle visual emphasis to draw the user's attention to what's happening now. This could be a slightly larger section header, bolder typography, or a subtle highlight — consistent with the Electric Dusk design system.
+
+Past events continue to appear visually faded (reduced opacity/saturation) as currently implemented.
+
+**Why this priority**: Nice visual polish that reinforces the temporal hierarchy, but the feature works without it.
+
+**Independent Test**: Can be verified visually by checking that the "Today" section stands out compared to other sections.
+
+**Acceptance Scenarios**:
+
+1. **Given** events exist for today and later, **When** the user views the list, **Then** the "Today" section is visually more prominent than other sections.
+2. **Given** only past events exist, **When** the user views the list, **Then** the "Past" section uses the existing faded treatment without any special emphasis.
+
+---
+
+### Edge Cases
+
+- What happens when the user's device clock is set incorrectly? Events may appear in the wrong section — this is acceptable, no special handling needed.
+- What happens at midnight when "today" changes? The grouping updates on next page load or navigation; real-time re-sorting is not required.
+- What happens with an event at exactly midnight (00:00)? It belongs to the day it falls on — same as any other time.
+- What happens when a section has many events (10+)? All events are shown; no pagination or truncation within sections.
+
+## Requirements *(mandatory)*
+
+### Functional Requirements
+
+- **FR-001**: System MUST group events into temporal sections: "Today", "This Week", "Later", and "Past".
+- **FR-002**: System MUST hide sections that contain no events.
+- **FR-003**: System MUST display section headers with the temporal label (e.g., "Today", "This Week").
+- **FR-004**: System MUST display date subheaders within "This Week", "Later", and "Past" sections when events span multiple days.
+- **FR-005**: System MUST NOT display a date subheader within the "Today" section.
+- **FR-006**: System MUST sort events within each section by time ascending (earliest first) for upcoming events and by time descending (most recent first) for past events.
+- **FR-007**: System MUST display clock time (e.g., "18:30") on event cards in "Today", "This Week", and "Later" sections.
+- **FR-008**: System MUST display relative time (e.g., "3 days ago") on event cards in the "Past" section.
+- **FR-009**: System MUST visually emphasize the "Today" section compared to other sections.
+- **FR-010**: System MUST continue to fade past events visually (as currently implemented).
+- **FR-011**: System MUST preserve existing functionality: role badges, swipe-to-delete, delete confirmation, empty state.
+- **FR-012**: "This Week" MUST include events from tomorrow through the end of the current calendar week (Sunday).
+
+### Key Entities
+
+- **Temporal Section**: A grouping label ("Today", "This Week", "Later", "Past") that organizes events by their relationship to the current date.
+- **Date Subheader**: A formatted date label (e.g., "Sat, 17 Sep") that groups events within a temporal section by their specific date.
+- **StoredEvent**: Existing entity — no changes to its structure are required. The `dateTime` field is used for all grouping and sorting logic.
+
+## Success Criteria *(mandatory)*
+
+### Measurable Outcomes
+
+- **SC-001**: Users can identify how many events they have today within 2 seconds of viewing the list.
+- **SC-002**: Every event in the list is assigned to exactly one temporal section — no event appears in multiple sections or is missing.
+- **SC-003**: Section ordering is always consistent: Today > This Week > Later > Past.
+- **SC-004**: The feature works entirely client-side with no additional network requests beyond what currently exists.
+- **SC-005**: All existing event list functionality (delete, navigation, role badges) continues to work unchanged.
+
+## Assumptions
+
+- The user's locale and timezone are used for determining "today" and formatting dates/times (via the browser's `Intl` API, consistent with existing approach).
+- "Week" follows ISO convention where Monday is the first day of the week. "This Week" runs from tomorrow through Sunday.
+- The design system (Electric Dusk + Sora) applies to all new visual elements. The inspiration screenshot's color theme is explicitly NOT adopted.
+- No backend changes are needed — this is a purely frontend enhancement to the existing client-side event list.
diff --git a/specs/010-event-list-grouping/tasks.md b/specs/010-event-list-grouping/tasks.md
new file mode 100644
index 0000000..dcc0244
--- /dev/null
+++ b/specs/010-event-list-grouping/tasks.md
@@ -0,0 +1,189 @@
+# Tasks: Event List Temporal Grouping
+
+**Input**: Design documents from `/specs/010-event-list-grouping/`
+**Prerequisites**: plan.md, spec.md, research.md, data-model.md
+
+**Tests**: Included — spec.md references TDD (Constitution II), and research.md explicitly plans unit + E2E test updates.
+
+**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
+
+## Format: `[ID] [P?] [Story] Description`
+
+- **[P]**: Can run in parallel (different files, no dependencies)
+- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3)
+- Include exact file paths in descriptions
+
+---
+
+## Phase 1: Setup
+
+**Purpose**: No new project setup needed — this is a frontend-only enhancement to an existing codebase. Phase 1 is empty.
+
+*(No tasks — existing project structure is sufficient.)*
+
+---
+
+## Phase 2: Foundational (Blocking Prerequisites)
+
+**Purpose**: Create the core grouping composable and its types — all user stories depend on this logic.
+
+**CRITICAL**: No user story work can begin until this phase is complete.
+
+- [ ] T001 Define `SectionKey`, `EventSection`, and `DateGroup` types in `frontend/src/composables/useEventGrouping.ts`
+- [ ] T002 Implement `useEventGrouping` composable with section classification, date grouping, and sorting logic in `frontend/src/composables/useEventGrouping.ts`
+- [ ] T003 Write unit tests for `useEventGrouping` covering all four sections, empty-section omission, sort order, and Sunday edge case in `frontend/src/components/__tests__/useEventGrouping.spec.ts`
+
+**Checkpoint**: Grouping logic is fully tested and ready for consumption by UI components.
+
+---
+
+## Phase 3: User Story 1 — Temporal Section Headers (Priority: P1) MVP
+
+**Goal**: Events appear grouped under "Today", "This Week", "Later", and "Past" section headers. Empty sections are hidden.
+
+**Independent Test**: Add events with various dates to localStorage, verify they appear under correct section headers.
+
+### Tests for User Story 1
+
+> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
+
+- [ ] T004 [P] [US1] Write unit tests for `SectionHeader.vue` rendering section label and emphasis flag in `frontend/src/components/__tests__/SectionHeader.spec.ts`
+- [ ] T005 [P] [US1] Update `EventList.spec.ts` tests to expect grouped section structure instead of flat list in `frontend/src/components/__tests__/EventList.spec.ts`
+
+### Implementation for User Story 1
+
+- [ ] T006 [P] [US1] Create `SectionHeader.vue` component with section label (``) and `aria-label` in `frontend/src/components/SectionHeader.vue`
+- [ ] T007 [US1] Refactor `EventList.vue` template to use `useEventGrouping`, render `` per temporal group with `SectionHeader`, and hide empty sections in `frontend/src/components/EventList.vue`
+- [ ] T008 [US1] Update E2E tests in `home-events.spec.ts` to verify section headers appear with correct events distributed across "Today", "This Week", "Later", "Past" in `frontend/e2e/home-events.spec.ts`
+
+**Checkpoint**: Event list shows temporal section headers. All four acceptance scenarios pass.
+
+---
+
+## Phase 4: User Story 2 — Date Subheaders Within Sections (Priority: P2)
+
+**Goal**: Within each section (except "Today"), events are further grouped by date with formatted subheaders like "Wed, 12 Mar".
+
+**Independent Test**: Add multiple events on different days within one section, verify date subheaders appear.
+
+### Tests for User Story 2
+
+> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
+
+- [ ] T009 [P] [US2] Write unit tests for `DateSubheader.vue` rendering formatted date label in `frontend/src/components/__tests__/DateSubheader.spec.ts`
+- [ ] T010 [P] [US2] Add unit tests to `EventList.spec.ts` verifying date subheaders appear within sections and are absent in "Today" in `frontend/src/components/__tests__/EventList.spec.ts`
+
+### Implementation for User Story 2
+
+- [ ] T011 [P] [US2] Create `DateSubheader.vue` component with formatted date (``) using `Intl.DateTimeFormat` in `frontend/src/components/DateSubheader.vue`
+- [ ] T012 [US2] Update `EventList.vue` template to render `DateSubheader` within each section's date groups, skipping subheader for "Today" section (`showSubheader` flag) in `frontend/src/components/EventList.vue`
+- [ ] T013 [US2] Add E2E test scenarios for date subheaders: multiple days within a section, no subheader in "Today" in `frontend/e2e/home-events.spec.ts`
+
+**Checkpoint**: Date subheaders render correctly within sections. "Today" section has no subheader.
+
+---
+
+## Phase 5: User Story 3 — Enhanced Event Card Time Display (Priority: P2)
+
+**Goal**: Event cards show clock time ("18:30") in Today/This Week/Later sections and relative time ("3 days ago") in Past section.
+
+**Independent Test**: Check event cards display the correct time format based on which section they appear in.
+
+### Tests for User Story 3
+
+> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
+
+- [ ] T014 [P] [US3] Add unit tests to `EventCard.spec.ts` for `timeDisplayMode` prop: `'clock'` renders formatted time, `'relative'` renders relative time in `frontend/src/components/__tests__/EventCard.spec.ts`
+
+### Implementation for User Story 3
+
+- [ ] T015 [US3] Add `timeDisplayMode` prop (`'clock' | 'relative'`) to `EventCard.vue`, render clock time via `Intl.DateTimeFormat({ hour: '2-digit', minute: '2-digit' })` or existing `formatRelativeTime()` in `frontend/src/components/EventCard.vue`
+- [ ] T016 [US3] Update `EventList.vue` to pass `timeDisplayMode="clock"` for today/thisWeek/later sections and `timeDisplayMode="relative"` for past section in `frontend/src/components/EventList.vue`
+- [ ] T017 [US3] Add E2E test scenarios verifying clock time in future sections and relative time in past section in `frontend/e2e/home-events.spec.ts`
+
+**Checkpoint**: Time display adapts to section context. All three acceptance scenarios pass.
+
+---
+
+## Phase 6: User Story 4 — Today Section Visual Emphasis (Priority: P3)
+
+**Goal**: The "Today" section header is visually more prominent (bolder, slightly larger, accent border) than other sections.
+
+**Independent Test**: Visual verification that "Today" stands out compared to other section headers.
+
+- [ ] T018 [US4] Add `.section--today` CSS class to `SectionHeader.vue` with `font-weight: 800`, `font-size: 1.1rem`, and left border accent (`#F06292`) — triggered by `emphasized` prop in `frontend/src/components/SectionHeader.vue`
+- [ ] T019 [US4] Verify `EventList.vue` passes `emphasized: true` for the "Today" section (already set via `EventSection.emphasized` from data model) in `frontend/src/components/EventList.vue`
+- [ ] T020 [US4] Add visual E2E assertion checking that the "Today" section header has the emphasis CSS class applied in `frontend/e2e/home-events.spec.ts`
+
+**Checkpoint**: "Today" section is visually distinct. Past events remain faded.
+
+---
+
+## Phase 7: Polish & Cross-Cutting Concerns
+
+**Purpose**: Final validation and regression checks.
+
+- [ ] T021 Run full unit test suite (`cd frontend && npm run test:unit`) and fix any regressions
+- [ ] T022 Run full E2E test suite and verify all existing functionality (swipe-to-delete, role badges, empty state, navigation) still works in `frontend/e2e/home-events.spec.ts`
+- [ ] T023 Verify accessibility: section headers are ``, date subheaders are ``, sections have `aria-label`, keyboard navigation works
+
+---
+
+## Dependencies & Execution Order
+
+### Phase Dependencies
+
+- **Setup (Phase 1)**: Empty — no work needed
+- **Foundational (Phase 2)**: No dependencies — can start immediately
+- **US1 (Phase 3)**: Depends on Phase 2 (grouping composable)
+- **US2 (Phase 4)**: Depends on Phase 3 (needs section structure in EventList)
+- **US3 (Phase 5)**: Depends on Phase 3 (needs section context for time mode)
+- **US4 (Phase 6)**: Depends on Phase 3 (needs SectionHeader component)
+- **Polish (Phase 7)**: Depends on all user stories being complete
+
+### User Story Dependencies
+
+- **US1 (P1)**: Can start after Foundational — no dependencies on other stories
+- **US2 (P2)**: Depends on US1 (needs section structure in template to add subheaders)
+- **US3 (P2)**: Depends on US1 (needs section context to determine time mode), independent of US2
+- **US4 (P3)**: Depends on US1 (needs SectionHeader component), independent of US2/US3
+
+### Parallel Opportunities
+
+- **Phase 2**: T001 must precede T002; T003 can run after T002
+- **Phase 3**: T004 and T005 in parallel; T006 in parallel with tests; T007 after T006
+- **Phase 4**: T009 and T010 in parallel; T011 in parallel with tests; T012 after T011
+- **Phase 5**: T014 can start as soon as US1 is done; T015 after T014; T016 after T015
+- **Phase 6**: T018 can run in parallel with Phase 5 (different files)
+- **US3 and US4** can run in parallel after US1 completes
+
+---
+
+## Parallel Example: After US1 Completes
+
+```bash
+# These can run in parallel (different files, no dependencies):
+Task: T009 [US2] Write DateSubheader unit tests
+Task: T014 [US3] Write EventCard time mode unit tests
+Task: T018 [US4] Add .section--today CSS to SectionHeader.vue
+```
+
+---
+
+## Implementation Strategy
+
+### MVP First (User Story 1 Only)
+
+1. Complete Phase 2: Foundational (grouping composable + tests)
+2. Complete Phase 3: User Story 1 (section headers in EventList)
+3. **STOP and VALIDATE**: Test US1 independently
+4. Deploy/demo if ready — list is already grouped with headers
+
+### Incremental Delivery
+
+1. Phase 2 → Grouping logic ready
+2. Add US1 → Section headers visible → Deploy/Demo (MVP!)
+3. Add US2 → Date subheaders within sections → Deploy/Demo
+4. Add US3 → Context-aware time display → Deploy/Demo
+5. Add US4 → Visual polish for "Today" → Deploy/Demo
+6. Each story adds value without breaking previous stories