feat(schedule): add a single-day view to the Schedule tab (#9058)

* feat(schedule): allow 'day' as a persisted time-view mode

* feat(schedule): compute single-day range and header for day view

* feat(schedule): add day-view toggle and single-day labels

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JzGvAaeT2TrZcEUA6fYPSZ

* docs(schedule): note the day view mode in the Schedule wiki

* fix(schedule): make the day-view header compact and locale-aware

Address review feedback on the single-day view:

- Build the day title with a locale-aware Intl skeleton instead of a
  hardcoded 'EEE, MMM d, yyyy' pattern, which pinned en-US field ordering.
- Shrink the title responsively so it never gets ellipsis-clipped: show
  the full weekday + date + year on roomy widths, and fall back to a
  compact month + day below TABLET, where the toggle group and nav
  controls leave too little room. The weekday still shows in the
  day-column header.
- Rename the .day-view-btn test hook to .e2e-day-view-btn per convention.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(schedule): rename week-month-* classes to time-view-*

The toggle group now has three buttons (Day, Week, Month), so the
week-month-selector / week-month-btn names no longer describe it. Rename
to time-view-selector / time-view-btn. Pure CSS class rename, no behavior
change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(schedule): complete time-view-* rename in zen theme

The class rename left zen.css targeting .week-month-*, so its overrides
matched nothing. The :not(.active) rule is the load-bearing one: its
!important suppresses the toggle's hover highlight, which zen exists to
strip.

Co-authored-by: Jon Kilroy <jkusa@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
Co-authored-by: Jon Kilroy <jkusa@users.noreply.github.com>
This commit is contained in:
Jon Kilroy 2026-07-17 09:00:08 -05:00 committed by GitHub
parent 5097155e25
commit d5afe62016
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 309 additions and 24 deletions

View file

@ -0,0 +1,97 @@
import { expect, test } from '../../fixtures/test.fixture';
/** Format a Date as the app's day key (YYYY-MM-DD in local time). */
const dbDate = (d: Date): string => {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${y}-${m}-${day}`;
};
// schedule-week draws two [data-day] cells per day (main + end-of-day) and
// schedule-month also uses [data-day], so scope day counting to the main
// week columns only.
const DAY_COL = 'schedule-week .col:not(.end-of-day)[data-day]';
const DESKTOP_VIEWPORT = { width: 1280, height: 720 };
test.describe('Schedule day view', () => {
test.use({ viewport: DESKTOP_VIEWPORT });
test('renders one day, navigates a day at a time, and switches views', async ({
page,
workViewPage,
}) => {
await workViewPage.waitForTaskList();
await page.getByRole('menuitem', { name: 'Schedule' }).click();
const dayCols = page.locator(DAY_COL);
const dayBtn = page.getByRole('button', { name: 'View Day' });
const weekBtn = page.getByRole('button', { name: 'View Week' });
const monthBtn = page.getByRole('button', { name: 'View Month' });
const nextBtn = page.getByRole('button', { name: 'Next Day' });
const prevBtn = page.getByRole('button', { name: 'Previous Day' });
const todayBtn = page.locator('schedule .today-btn');
// Scope to the header container: task events also render a `.title`, so a
// bare `schedule .title` would be ambiguous once the schedule has events.
const title = page.locator('schedule .schedule-nav-controls .title');
const scheduleWeek = page.locator('schedule-week');
const scheduleMonth = page.locator('schedule-month');
const today = dbDate(new Date());
// Calendar-based next day (DST-safe) so it matches the app's setDate(+1) logic.
const tomorrowDate = new Date();
tomorrowDate.setDate(tomorrowDate.getDate() + 1);
const tomorrow = dbDate(tomorrowDate);
// --- Day view renders exactly one day (today), single-date header ---
await dayBtn.click();
await expect(dayCols).toHaveCount(1);
await expect(dayCols.first()).toHaveAttribute('data-day', today);
await expect(dayBtn).toHaveAttribute('aria-pressed', 'true');
// Header is a single en-GB date ("Fri, 17 Jul 2026"; locale pinned in
// config), not a week range.
await expect(title).toHaveText(/^\w+, \d{1,2} \w+ \d{4}$/);
// Viewing today: cannot go earlier, "today" reset is disabled.
await expect(prevBtn).toBeDisabled();
await expect(todayBtn).toBeDisabled();
// --- Navigation moves exactly one day at a time ---
await nextBtn.click();
await expect(dayCols).toHaveCount(1);
await expect(dayCols.first()).toHaveAttribute('data-day', tomorrow);
// Off today, both "back to today" affordances become enabled.
await expect(prevBtn).toBeEnabled();
await expect(todayBtn).toBeEnabled();
// Prev steps back to today (snap-to-today) and re-locks.
await prevBtn.click();
await expect(dayCols.first()).toHaveAttribute('data-day', today);
await expect(prevBtn).toBeDisabled();
// The Today button also resets to today after navigating away (distinct
// code path from Prev: goToToday() vs goToPreviousPeriod()).
await nextBtn.click();
await expect(dayCols.first()).toHaveAttribute('data-day', tomorrow);
await todayBtn.click();
await expect(dayCols.first()).toHaveAttribute('data-day', today);
await expect(todayBtn).toBeDisabled();
// --- Switching views renders the expected grid each time ---
await weekBtn.click(); // day -> week: week grid, more than one day
await expect(weekBtn).toHaveAttribute('aria-pressed', 'true');
await expect(scheduleWeek).toBeVisible();
// week shows multiple day columns (retry-safe; not coupled to the exact count)
await expect.poll(() => dayCols.count()).toBeGreaterThan(1);
await monthBtn.click(); // week -> month: month grid replaces the week grid
await expect(monthBtn).toHaveAttribute('aria-pressed', 'true');
await expect(scheduleMonth).toBeVisible();
await expect(scheduleWeek).toHaveCount(0);
await dayBtn.click(); // month -> day: back to a single day column
await expect(dayBtn).toHaveAttribute('aria-pressed', 'true');
await expect(scheduleMonth).toHaveCount(0);
await expect(dayCols).toHaveCount(1);
});
});