diff --git a/docs/acceptance/shortcuts.md b/docs/acceptance/shortcuts.md index d13693e97..34fbcee0e 100644 --- a/docs/acceptance/shortcuts.md +++ b/docs/acceptance/shortcuts.md @@ -337,7 +337,7 @@ On Day view, holding Mod reveals numbered chips left to right: `1` on the view d ### UX -The sidebar month picker is a keyboard cursor, not a click target. `I` (or hold Mod and press the picker's digit) lands on the cursor week's first day. In Week view every arrow key moves the cursor one week row; Day view moves one day. Enter opens the cursor week (or day). `Mod+Shift+,` / `Mod+Shift+.` and the header chevrons change the month and keep a focusable day in the new month, so arrows keep working. Clicking a day does nothing; hovering or focusing the picker shows a caption with the keys, and repeated clicks surface the keyboard hint. +The sidebar month picker is a keyboard cursor, not a click target. `I` (or hold Mod and press the picker's digit) lands on the cursor week's first day. In Week view every arrow key moves the cursor one week row; Day view moves one day. Enter opens the cursor week (or day). `Mod+Shift+,` / `Mod+Shift+.` and the header chevrons change the month and keep a focusable day in the new month, so arrows keep working. Clicking a day does nothing; hovering the picker shows `I focuses the picker`, focusing it reveals the arrow keycaps, and using an arrow adds `Enter opens it`. Repeated clicks surface the keyboard hint. ### Steps @@ -352,8 +352,8 @@ The sidebar month picker is a keyboard cursor, not a click target. `I` (or hold - After `I`, the first day of the highlighted week row has focus and the whole row shows the focus ring. - Each arrow moves the ring one row; the calendar grid does not move yet. - After `Mod+Shift+.`, the next month is shown and focus is still on a day in that month. -- Enter anchors the week view on the focused row and the accent capsule matches the visible window. -- Clicks do not navigate. The caption under the grid reads `I focuses the picker · Arrows move by week · Enter opens it`; the keyboard hint says to press `I`, then use the arrow keys and Enter. +- Enter anchors the week view on the focused row and the muted week capsule matches the visible window. Today is an ink circle, not an accent fill. +- Clicks do not navigate. Hovering the picker shows `I focuses the picker`. Focusing it replaces that with up/down arrow keycaps and `move by week`. After an arrow, `Enter opens it` appears with `Enter` as a keycap. The keyboard hint says to press `I`, then use the arrow keys and Enter. --- diff --git a/packages/backend/src/booking/services/public-booking.service.db.test.ts b/packages/backend/src/booking/services/public-booking.service.db.test.ts index af5e251cb..01b3385ec 100644 --- a/packages/backend/src/booking/services/public-booking.service.db.test.ts +++ b/packages/backend/src/booking/services/public-booking.service.db.test.ts @@ -40,6 +40,31 @@ import { } from "bun:test"; import { createHash, randomBytes, timingSafeEqual } from "node:crypto"; +const utcDatePlusDays = (isoDate: string, days: number): string => { + const [year, month, day] = isoDate.split("-").map(Number); + return new Date(Date.UTC(year!, month! - 1, day! + days)) + .toISOString() + .slice(0, 10); +}; + +/** Upcoming Monday in UTC, skipping today so 09:00-17:00 slots stay bookable. */ +const nextUtcMonday = (): string => { + const now = new Date(); + const todayUtc = Date.UTC( + now.getUTCFullYear(), + now.getUTCMonth(), + now.getUTCDate(), + ); + const weekday = new Date(todayUtc).getUTCDay(); + const daysUntilMonday = weekday === 1 ? 7 : (8 - weekday) % 7; + return new Date(todayUtc + daysUntilMonday * 86_400_000) + .toISOString() + .slice(0, 10); +}; + +const BOOKING_MONDAY = nextUtcMonday(); +const BOOKING_TUESDAY = utcDatePlusDays(BOOKING_MONDAY, 1); + const calendarId = () => new ObjectId().toString(); const writableCalendar = (id = calendarId()) => ({ @@ -133,7 +158,7 @@ const createNamedUser = async (name: string) => { const busyResponse = (bookable = true) => ({ intervals: [], - computedAt: "2026-09-07T12:00:00.000Z", + computedAt: `${BOOKING_MONDAY}T12:00:00.000Z`, connections: [], complete: true, issues: [], @@ -267,7 +292,7 @@ describe("PublicBookingService", () => { it("confirms a reservation and calls createBookingEvent once", async () => { const { slug } = await enableBookingPage(); - const slotStart = "2026-09-07T10:00:00.000Z"; + const slotStart = `${BOOKING_MONDAY}T10:00:00.000Z`; const response = await service.createReservation(slug, { slotStart, @@ -290,7 +315,7 @@ describe("PublicBookingService", () => { it("confirms at the pinned duration and calls createBookingEvent once", async () => { const { slug } = await enableBookingPage(); - const slotStart = "2026-09-07T10:00:00.000Z"; + const slotStart = `${BOOKING_MONDAY}T10:00:00.000Z`; const response = await service.createReservation(slug, { slotStart, @@ -303,10 +328,10 @@ describe("PublicBookingService", () => { expect(createBookingEvent).toHaveBeenCalledTimes(1); expect(createBookingEvent.mock.calls[0]?.[1]).toMatchObject({ start: slotStart, - end: "2026-09-07T10:30:00.000Z", + end: `${BOOKING_MONDAY}T10:30:00.000Z`, }); expect(response.slotStart).toBe(slotStart); - expect(response.slotEnd).toBe("2026-09-07T10:30:00.000Z"); + expect(response.slotEnd).toBe(`${BOOKING_MONDAY}T10:30:00.000Z`); }); it("rejects confirm when pinned duration does not match the page", async () => { @@ -325,7 +350,7 @@ describe("PublicBookingService", () => { await expect( service.createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", guestTimeZone: "Europe/London", @@ -343,7 +368,7 @@ describe("PublicBookingService", () => { await expect( service.createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", guestTimeZone: "Europe/London", @@ -360,7 +385,7 @@ describe("PublicBookingService", () => { await expect( service.createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", guestTimeZone: "Europe/London", @@ -382,7 +407,7 @@ describe("PublicBookingService", () => { const error = await service .createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", guestTimeZone: "Europe/London", @@ -410,8 +435,8 @@ describe("PublicBookingService", () => { ); const response = await service.getSlots(slug, { - start: "2026-09-07T00:00:00.000Z", - end: "2026-09-08T00:00:00.000Z", + start: `${BOOKING_MONDAY}T00:00:00.000Z`, + end: `${BOOKING_TUESDAY}T00:00:00.000Z`, timeZone: "UTC", }); @@ -447,15 +472,15 @@ describe("PublicBookingService", () => { ...busyResponse(true), intervals: [ { - start: "2026-09-07T10:00:00.000Z", - end: "2026-09-07T11:00:00.000Z", + start: `${BOOKING_MONDAY}T10:00:00.000Z`, + end: `${BOOKING_MONDAY}T11:00:00.000Z`, }, ], })); await expect( service.createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", guestTimeZone: "Europe/London", @@ -472,15 +497,15 @@ describe("PublicBookingService", () => { ...busyResponse(false), intervals: [ { - start: "2026-09-07T10:00:00.000Z", - end: "2026-09-07T11:00:00.000Z", + start: `${BOOKING_MONDAY}T10:00:00.000Z`, + end: `${BOOKING_MONDAY}T11:00:00.000Z`, }, ], })); const response = await service.getSlots(slug, { - start: "2026-09-07T00:00:00.000Z", - end: "2026-09-08T00:00:00.000Z", + start: `${BOOKING_MONDAY}T00:00:00.000Z`, + end: `${BOOKING_TUESDAY}T00:00:00.000Z`, timeZone: "UTC", }); @@ -494,8 +519,8 @@ describe("PublicBookingService", () => { ...busyResponse(true), intervals: [ { - start: "2026-09-07T10:00:00.000Z", - end: "2026-09-07T11:00:00.000Z", + start: `${BOOKING_MONDAY}T10:00:00.000Z`, + end: `${BOOKING_MONDAY}T11:00:00.000Z`, hostIsOrganizer: false, hostResponseStatus: "needsAction", }, @@ -503,15 +528,16 @@ describe("PublicBookingService", () => { })); const response = await service.getSlots(slug, { - start: "2026-09-07T00:00:00.000Z", - end: "2026-09-08T00:00:00.000Z", + start: `${BOOKING_MONDAY}T00:00:00.000Z`, + end: `${BOOKING_TUESDAY}T00:00:00.000Z`, timeZone: "UTC", }); expect( response.slots.some( (slot) => - Date.parse(slot.slotStart) === Date.parse("2026-09-07T10:00:00.000Z"), + Date.parse(slot.slotStart) === + Date.parse(`${BOOKING_MONDAY}T10:00:00.000Z`), ), ).toBe(true); }); @@ -522,8 +548,8 @@ describe("PublicBookingService", () => { ...busyResponse(true), intervals: [ { - start: "2026-09-07T10:00:00.000Z", - end: "2026-09-07T11:00:00.000Z", + start: `${BOOKING_MONDAY}T10:00:00.000Z`, + end: `${BOOKING_MONDAY}T11:00:00.000Z`, hostIsOrganizer: true, hostResponseStatus: null, }, @@ -531,15 +557,16 @@ describe("PublicBookingService", () => { })); const response = await service.getSlots(slug, { - start: "2026-09-07T00:00:00.000Z", - end: "2026-09-08T00:00:00.000Z", + start: `${BOOKING_MONDAY}T00:00:00.000Z`, + end: `${BOOKING_TUESDAY}T00:00:00.000Z`, timeZone: "UTC", }); expect( response.slots.some( (slot) => - Date.parse(slot.slotStart) === Date.parse("2026-09-07T10:00:00.000Z"), + Date.parse(slot.slotStart) === + Date.parse(`${BOOKING_MONDAY}T10:00:00.000Z`), ), ).toBe(false); }); @@ -574,8 +601,8 @@ describe("PublicBookingService", () => { ...busyResponse(true), intervals: [ { - start: "2026-09-07T10:00:00.000Z", - end: "2026-09-07T11:00:00.000Z", + start: `${BOOKING_MONDAY}T10:00:00.000Z`, + end: `${BOOKING_MONDAY}T11:00:00.000Z`, hostIsOrganizer: true, hostResponseStatus: null, }, @@ -584,14 +611,15 @@ describe("PublicBookingService", () => { }); const slots = await service.getSlots(slug, { - start: "2026-09-07T00:00:00.000Z", - end: "2026-09-08T00:00:00.000Z", + start: `${BOOKING_MONDAY}T00:00:00.000Z`, + end: `${BOOKING_TUESDAY}T00:00:00.000Z`, timeZone: "UTC", }); expect( slots.slots.some( (slot) => - Date.parse(slot.slotStart) === Date.parse("2026-09-07T10:00:00.000Z"), + Date.parse(slot.slotStart) === + Date.parse(`${BOOKING_MONDAY}T10:00:00.000Z`), ), ).toBe(false); @@ -604,7 +632,7 @@ describe("PublicBookingService", () => { await expect( service.createReservation(slug, { - slotStart: "2026-09-07T12:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T12:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", guestTimeZone: "Europe/London", @@ -655,7 +683,7 @@ describe("PublicBookingService", () => { expect(publicPage.conference).toBe("none"); const created = await service.createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", guestTimeZone: "Europe/London", @@ -705,7 +733,7 @@ describe("PublicBookingService", () => { ); const created = await bookingService.createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", notes: "Zoom: https://example.com/meet", @@ -765,7 +793,7 @@ describe("PublicBookingService", () => { ); const created = await bookingService.createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", guestTimeZone: "Europe/London", @@ -819,7 +847,7 @@ describe("PublicBookingService", () => { ); await bookingService.createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", guestTimeZone: "Europe/London", @@ -873,8 +901,8 @@ describe("PublicBookingService", () => { it("accepts a month-length slot window within the existing 60-day cap", async () => { const { slug } = await enableBookingPage(); const response = await service.getSlots(slug, { - start: "2026-08-01T00:00:00.000Z", - end: "2026-09-01T00:00:00.000Z", + start: `${BOOKING_MONDAY}T00:00:00.000Z`, + end: `${utcDatePlusDays(BOOKING_MONDAY, 31)}T00:00:00.000Z`, timeZone: "UTC", }); @@ -887,8 +915,8 @@ describe("PublicBookingService", () => { await expect( service.getSlots(slug, { - start: "2026-09-08T00:00:00.000Z", - end: "2026-09-07T00:00:00.000Z", + start: `${BOOKING_TUESDAY}T00:00:00.000Z`, + end: `${BOOKING_MONDAY}T00:00:00.000Z`, timeZone: "UTC", }), ).rejects.toMatchObject({ bookingCode: "INVALID_INPUT" }); @@ -896,7 +924,7 @@ describe("PublicBookingService", () => { it("cancels idempotently", async () => { const { slug } = await enableBookingPage(); - const slotStart = "2026-09-07T10:00:00.000Z"; + const slotStart = `${BOOKING_MONDAY}T10:00:00.000Z`; const created = await service.createReservation(slug, { slotStart, guestName: "Ada Lovelace", @@ -919,7 +947,7 @@ describe("PublicBookingService", () => { it("marks cancelled before delete so a failed provider delete does not keep the slot", async () => { const { slug } = await enableBookingPage(); - const slotStart = "2026-09-07T10:00:00.000Z"; + const slotStart = `${BOOKING_MONDAY}T10:00:00.000Z`; const created = await service.createReservation(slug, { slotStart, guestName: "Ada Lovelace", @@ -995,7 +1023,7 @@ describe("PublicBookingService", () => { it("returns minimal public reservation details", async () => { const { slug } = await enableBookingPage(); const created = await service.createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", notes: "secret notes", @@ -1027,7 +1055,7 @@ describe("PublicBookingService", () => { it("returns the booked slot duration after the host changes page duration", async () => { const { slug, userId, calendarId } = await enableBookingPage(); const created = await service.createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", notes: "secret notes", @@ -1056,7 +1084,7 @@ describe("PublicBookingService", () => { it("returns cancelled status without leaking guest contact", async () => { const { slug } = await enableBookingPage(); const created = await service.createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", guestTimeZone: "Europe/London", @@ -1082,7 +1110,7 @@ describe("PublicBookingService", () => { it("still returns slots when the page already has a confirmed reservation", async () => { const { slug } = await enableBookingPage(); - const booked = "2026-09-07T10:00:00.000Z"; + const booked = `${BOOKING_MONDAY}T10:00:00.000Z`; await service.createReservation(slug, { slotStart: booked, guestName: "Ada Lovelace", @@ -1092,8 +1120,8 @@ describe("PublicBookingService", () => { }); const response = await service.getSlots(slug, { - start: "2026-09-07T00:00:00.000Z", - end: "2026-09-08T00:00:00.000Z", + start: `${BOOKING_MONDAY}T00:00:00.000Z`, + end: `${BOOKING_TUESDAY}T00:00:00.000Z`, timeZone: "UTC", }); @@ -1105,8 +1133,8 @@ describe("PublicBookingService", () => { it("returns the same slot set for two different guest timeZone values", async () => { const { slug } = await enableBookingPage(); const window = { - start: "2026-09-07T00:00:00.000Z", - end: "2026-09-08T00:00:00.000Z", + start: `${BOOKING_MONDAY}T00:00:00.000Z`, + end: `${BOOKING_TUESDAY}T00:00:00.000Z`, }; const utc = await service.getSlots(slug, { @@ -1137,8 +1165,8 @@ describe("PublicBookingService", () => { try { const response = await service.getSlots(slug, { - start: "2026-09-07T00:00:00.000Z", - end: "2026-09-08T00:00:00.000Z", + start: `${BOOKING_MONDAY}T00:00:00.000Z`, + end: `${BOOKING_TUESDAY}T00:00:00.000Z`, timeZone: "UTC", }); @@ -1148,7 +1176,7 @@ describe("PublicBookingService", () => { ); expect(response.bookable).toBe(true); expect(response.slots.map((slot) => slot.slotStart)).toContain( - "2026-09-07T10:00:00Z", + `${BOOKING_MONDAY}T10:00:00Z`, ); } finally { listSpy.mockRestore(); @@ -1161,21 +1189,21 @@ describe("PublicBookingService", () => { }); await seedConfirmedReservation( pageId, - "2026-09-07T09:00:00.000Z", - "2026-09-07T09:30:00.000Z", + `${BOOKING_MONDAY}T09:00:00.000Z`, + `${BOOKING_MONDAY}T09:30:00.000Z`, ); const response = await service.getSlots(slug, { - start: "2026-09-07T09:30:00.000Z", - end: "2026-09-07T11:00:00.000Z", + start: `${BOOKING_MONDAY}T09:30:00.000Z`, + end: `${BOOKING_MONDAY}T11:00:00.000Z`, timeZone: "UTC", }); expect(response.slots.map((slot) => slot.slotStart)).not.toContain( - "2026-09-07T09:30:00Z", + `${BOOKING_MONDAY}T09:30:00Z`, ); expect(response.slots.map((slot) => slot.slotStart)).toContain( - "2026-09-07T09:45:00Z", + `${BOOKING_MONDAY}T09:45:00Z`, ); }); @@ -1186,19 +1214,19 @@ describe("PublicBookingService", () => { }); await seedConfirmedReservation( pageId, - "2026-09-08T00:10:00.000Z", - "2026-09-08T00:40:00.000Z", + `${BOOKING_TUESDAY}T00:10:00.000Z`, + `${BOOKING_TUESDAY}T00:40:00.000Z`, ); const response = await service.getSlots(slug, { - start: "2026-09-07T21:00:00.000Z", - end: "2026-09-07T23:30:00.000Z", + start: `${BOOKING_MONDAY}T21:00:00.000Z`, + end: `${BOOKING_MONDAY}T23:30:00.000Z`, timeZone: "UTC", }); const starts = response.slots.map((slot) => slot.slotStart); - expect(starts).toContain("2026-09-07T21:00:00Z"); - expect(starts).not.toContain("2026-09-07T23:15:00Z"); + expect(starts).toContain(`${BOOKING_MONDAY}T21:00:00Z`); + expect(starts).not.toContain(`${BOOKING_MONDAY}T23:15:00Z`); }); it("still applies max bookings per day when the only reservation is earlier the same local day", async () => { @@ -1207,13 +1235,13 @@ describe("PublicBookingService", () => { }); await seedConfirmedReservation( pageId, - "2026-09-07T10:00:00.000Z", - "2026-09-07T10:30:00.000Z", + `${BOOKING_MONDAY}T10:00:00.000Z`, + `${BOOKING_MONDAY}T10:30:00.000Z`, ); const response = await service.getSlots(slug, { - start: "2026-09-07T14:00:00.000Z", - end: "2026-09-07T17:00:00.000Z", + start: `${BOOKING_MONDAY}T14:00:00.000Z`, + end: `${BOOKING_MONDAY}T17:00:00.000Z`, timeZone: "UTC", }); @@ -1223,7 +1251,7 @@ describe("PublicBookingService", () => { it("accepts a second non-overlapping booking on the same page", async () => { const { slug } = await enableBookingPage(); await service.createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", guestTimeZone: "Europe/London", @@ -1231,7 +1259,7 @@ describe("PublicBookingService", () => { }); const second = await service.createReservation(slug, { - slotStart: "2026-09-07T11:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T11:00:00.000Z`, guestName: "Grace Hopper", guestEmail: "grace@example.com", guestTimeZone: "America/New_York", @@ -1250,15 +1278,15 @@ describe("PublicBookingService", () => { createBookingEvent.mockImplementation(async () => { await seedConfirmedReservation( pageId, - "2026-09-07T10:15:00.000Z", - "2026-09-07T10:45:00.000Z", + `${BOOKING_MONDAY}T10:15:00.000Z`, + `${BOOKING_MONDAY}T10:45:00.000Z`, ); return "our-evt"; }); await expect( service.createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", guestTimeZone: "Europe/London", @@ -1273,8 +1301,8 @@ describe("PublicBookingService", () => { const survivors = await bookingReservationRepository.listConfirmedOverlapping( pageId, - new Date("2026-09-07T09:00:00.000Z"), - new Date("2026-09-07T12:00:00.000Z"), + new Date(`${BOOKING_MONDAY}T09:00:00.000Z`), + new Date(`${BOOKING_MONDAY}T12:00:00.000Z`), ); expect(survivors).toHaveLength(1); }); @@ -1284,8 +1312,8 @@ describe("PublicBookingService", () => { createBookingEvent.mockImplementation(async () => { await seedConfirmedReservation( pageId, - "2026-09-07T10:15:00.000Z", - "2026-09-07T10:45:00.000Z", + `${BOOKING_MONDAY}T10:15:00.000Z`, + `${BOOKING_MONDAY}T10:45:00.000Z`, ); return "our-evt"; }); @@ -1302,7 +1330,7 @@ describe("PublicBookingService", () => { try { await expect( service.createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", guestTimeZone: "Europe/London", @@ -1319,13 +1347,13 @@ describe("PublicBookingService", () => { principalId: userId.toString(), calendarId, eventId: "our-evt", - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, }); const survivors = await bookingReservationRepository.listConfirmedOverlapping( pageId, - new Date("2026-09-07T09:00:00.000Z"), - new Date("2026-09-07T12:00:00.000Z"), + new Date(`${BOOKING_MONDAY}T09:00:00.000Z`), + new Date(`${BOOKING_MONDAY}T12:00:00.000Z`), ); expect(survivors).toHaveLength(1); } finally { @@ -1338,15 +1366,15 @@ describe("PublicBookingService", () => { createBookingEvent.mockImplementation(async () => { await seedConfirmedReservation( pageId, - "2026-09-07T10:00:00.000Z", - "2026-09-07T10:30:00.000Z", + `${BOOKING_MONDAY}T10:00:00.000Z`, + `${BOOKING_MONDAY}T10:30:00.000Z`, ); return "our-evt"; }); await expect( service.createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", guestTimeZone: "Europe/London", @@ -1361,7 +1389,7 @@ describe("PublicBookingService", () => { const { slug } = await enableBookingPage(); await service.createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", notes: "bring coffee", @@ -1383,7 +1411,7 @@ describe("PublicBookingService", () => { }); const created = await service.createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", guestTimeZone: "Europe/London", @@ -1417,7 +1445,7 @@ describe("PublicBookingService", () => { ); await bookingService.createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", guestTimeZone: "Europe/London", @@ -1433,7 +1461,7 @@ describe("PublicBookingService", () => { it("patches guest name and notes and submits an event update", async () => { const { slug } = await enableBookingPage(); const created = await service.createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", notes: "bring coffee", @@ -1465,7 +1493,7 @@ describe("PublicBookingService", () => { it("rejects a wrong patch token without changing the row", async () => { const { slug } = await enableBookingPage(); const created = await service.createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", notes: "bring coffee", @@ -1490,7 +1518,7 @@ describe("PublicBookingService", () => { it("rejects patching a cancelled reservation without a command", async () => { const { slug } = await enableBookingPage(); const created = await service.createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", notes: "bring coffee", @@ -1548,7 +1576,7 @@ describe("PublicBookingService", () => { const { slug } = await enableBookingPage(); await expect( service.createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "not-an-email", guestTimeZone: "Europe/London", @@ -1560,7 +1588,7 @@ describe("PublicBookingService", () => { it("reschedules a confirmed reservation to a new slot", async () => { const { slug } = await enableBookingPage(); const created = await service.createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", guestTimeZone: "Europe/London", @@ -1573,32 +1601,34 @@ describe("PublicBookingService", () => { const response = await service.rescheduleReservation(reservationId, { token, - slotStart: "2026-09-07T11:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T11:00:00.000Z`, guestTimeZone: "America/Denver", durationMinutes: 30, }); expect(updateBookingEvent).toHaveBeenCalledTimes(1); expect(updateBookingEvent.mock.calls[0]?.[1]).toMatchObject({ - start: "2026-09-07T11:00:00.000Z", - end: "2026-09-07T11:30:00.000Z", + start: `${BOOKING_MONDAY}T11:00:00.000Z`, + end: `${BOOKING_MONDAY}T11:30:00.000Z`, }); expect(createBookingEvent).not.toHaveBeenCalled(); - expect(response.slotStart).toBe("2026-09-07T11:00:00.000Z"); - expect(response.slotEnd).toBe("2026-09-07T11:30:00.000Z"); + expect(response.slotStart).toBe(`${BOOKING_MONDAY}T11:00:00.000Z`); + expect(response.slotEnd).toBe(`${BOOKING_MONDAY}T11:30:00.000Z`); expect(response.guestTimeZone).toBe("America/Denver"); expect(response.status).toBe("confirmed"); expect(response).not.toHaveProperty("guestEmail"); expect(response).not.toHaveProperty("cancelUrl"); const stored = await bookingReservationRepository.findById(reservationId); - expect(stored?.slotStart.toISOString()).toBe("2026-09-07T11:00:00.000Z"); + expect(stored?.slotStart.toISOString()).toBe( + `${BOOKING_MONDAY}T11:00:00.000Z`, + ); expect(stored?.calendarEventId).toBeTruthy(); }); it("rejects reschedule when pinned duration does not match the page", async () => { const { slug, userId, calendarId } = await enableBookingPage(); const created = await service.createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", guestTimeZone: "Europe/London", @@ -1623,7 +1653,7 @@ describe("PublicBookingService", () => { await expect( service.rescheduleReservation(reservationId, { token, - slotStart: "2026-09-07T11:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T11:00:00.000Z`, guestTimeZone: "Europe/London", durationMinutes: 30, }), @@ -1638,7 +1668,7 @@ describe("PublicBookingService", () => { it("treats a second reschedule to the same slot as idempotent", async () => { const { slug } = await enableBookingPage(); const created = await service.createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", guestTimeZone: "Europe/London", @@ -1648,7 +1678,7 @@ describe("PublicBookingService", () => { const reservationId = new ObjectId(created.reservationId); await service.rescheduleReservation(reservationId, { token, - slotStart: "2026-09-07T11:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T11:00:00.000Z`, guestTimeZone: "America/Denver", durationMinutes: 30, }); @@ -1656,20 +1686,20 @@ describe("PublicBookingService", () => { const again = await service.rescheduleReservation(reservationId, { token, - slotStart: "2026-09-07T11:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T11:00:00.000Z`, guestTimeZone: "UTC", durationMinutes: 30, }); expect(updateBookingEvent).not.toHaveBeenCalled(); - expect(again.slotStart).toBe("2026-09-07T11:00:00.000Z"); + expect(again.slotStart).toBe(`${BOOKING_MONDAY}T11:00:00.000Z`); expect(again.guestTimeZone).toBe("America/Denver"); }); it("rejects reschedule onto another confirmed reservation", async () => { const { slug, pageId } = await enableBookingPage(); const created = await service.createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", guestTimeZone: "Europe/London", @@ -1677,8 +1707,8 @@ describe("PublicBookingService", () => { }); await seedConfirmedReservation( pageId, - "2026-09-07T11:00:00.000Z", - "2026-09-07T11:30:00.000Z", + `${BOOKING_MONDAY}T11:00:00.000Z`, + `${BOOKING_MONDAY}T11:30:00.000Z`, ); const token = new URL(created.cancelUrl).searchParams.get("token"); const reservationId = new ObjectId(created.reservationId); @@ -1687,7 +1717,7 @@ describe("PublicBookingService", () => { await expect( service.rescheduleReservation(reservationId, { token, - slotStart: "2026-09-07T11:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T11:00:00.000Z`, guestTimeZone: "Europe/London", durationMinutes: 30, }), @@ -1698,7 +1728,7 @@ describe("PublicBookingService", () => { it("rejects reschedule of a cancelled reservation", async () => { const { slug } = await enableBookingPage(); const created = await service.createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", guestTimeZone: "Europe/London", @@ -1712,7 +1742,7 @@ describe("PublicBookingService", () => { await expect( service.rescheduleReservation(reservationId, { token, - slotStart: "2026-09-07T11:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T11:00:00.000Z`, guestTimeZone: "Europe/London", durationMinutes: 30, }), @@ -1723,7 +1753,7 @@ describe("PublicBookingService", () => { it("rejects a bad or missing reschedule token as not-found", async () => { const { slug } = await enableBookingPage(); const created = await service.createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", guestTimeZone: "Europe/London", @@ -1735,14 +1765,14 @@ describe("PublicBookingService", () => { await expect( service.rescheduleReservation(reservationId, { token: "not-the-token", - slotStart: "2026-09-07T11:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T11:00:00.000Z`, guestTimeZone: "Europe/London", durationMinutes: 30, }), ).rejects.toMatchObject({ bookingCode: "RESERVATION_NOT_FOUND" }); await expect( service.rescheduleReservation(reservationId, { - slotStart: "2026-09-07T11:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T11:00:00.000Z`, guestTimeZone: "Europe/London", durationMinutes: 30, }), @@ -1750,7 +1780,7 @@ describe("PublicBookingService", () => { await expect( service.rescheduleReservation(new ObjectId(), { token: "a".repeat(32), - slotStart: "2026-09-07T11:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T11:00:00.000Z`, guestTimeZone: "Europe/London", durationMinutes: 30, }), @@ -1761,7 +1791,7 @@ describe("PublicBookingService", () => { it("includes the current start on tokenized slots and hides it on public slots", async () => { const { slug } = await enableBookingPage(); const created = await service.createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", guestTimeZone: "Europe/London", @@ -1770,8 +1800,8 @@ describe("PublicBookingService", () => { const token = new URL(created.cancelUrl).searchParams.get("token"); const reservationId = new ObjectId(created.reservationId); const window = { - start: "2026-09-07T00:00:00.000Z", - end: "2026-09-08T00:00:00.000Z", + start: `${BOOKING_MONDAY}T00:00:00.000Z`, + end: `${BOOKING_TUESDAY}T00:00:00.000Z`, timeZone: "UTC", }; @@ -1781,7 +1811,7 @@ describe("PublicBookingService", () => { token, }); - const booked = Date.parse("2026-09-07T10:00:00.000Z"); + const booked = Date.parse(`${BOOKING_MONDAY}T10:00:00.000Z`); expect( publicSlots.slots.map((slot) => Date.parse(slot.slotStart)), ).not.toContain(booked); @@ -1797,7 +1827,7 @@ describe("PublicBookingService", () => { it("keeps overlapping host busy after self-exclusion", async () => { const { slug } = await enableBookingPage(); const created = await service.createReservation(slug, { - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", guestTimeZone: "Europe/London", @@ -1808,8 +1838,8 @@ describe("PublicBookingService", () => { ...busyResponse(true), intervals: [ { - start: "2026-09-07T10:00:00.000Z", - end: "2026-09-07T11:00:00.000Z", + start: `${BOOKING_MONDAY}T10:00:00.000Z`, + end: `${BOOKING_MONDAY}T11:00:00.000Z`, hostIsOrganizer: true, }, ], @@ -1819,14 +1849,14 @@ describe("PublicBookingService", () => { new ObjectId(created.reservationId), { token, - start: "2026-09-07T00:00:00.000Z", - end: "2026-09-08T00:00:00.000Z", + start: `${BOOKING_MONDAY}T00:00:00.000Z`, + end: `${BOOKING_TUESDAY}T00:00:00.000Z`, timeZone: "UTC", }, ); - const occupied = Date.parse("2026-09-07T10:00:00.000Z"); - const halfHour = Date.parse("2026-09-07T10:30:00.000Z"); + const occupied = Date.parse(`${BOOKING_MONDAY}T10:00:00.000Z`); + const halfHour = Date.parse(`${BOOKING_MONDAY}T10:30:00.000Z`); const starts = tokenized.slots.map((slot) => Date.parse(slot.slotStart)); expect(starts).not.toContain(occupied); expect(starts).not.toContain(halfHour); @@ -1957,8 +1987,8 @@ describe("Public booking routes", () => { await bookingReservationRepository.insert({ _id: reservationId, pageId: new ObjectId(page.id), - slotStart: new Date("2026-09-07T10:00:00.000Z"), - slotEnd: new Date("2026-09-07T10:30:00.000Z"), + slotStart: new Date(`${BOOKING_MONDAY}T10:00:00.000Z`), + slotEnd: new Date(`${BOOKING_MONDAY}T10:30:00.000Z`), guestName: "Ada Lovelace", guestEmail: "ada@example.com", notes: "secret notes", @@ -1974,7 +2004,7 @@ describe("Public booking routes", () => { .expect(Status.OK); expect(response.body).toEqual({ - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestTimeZone: "Europe/London", durationMinutes: 30, hostDisplayName: "Permalink Host", @@ -2010,7 +2040,7 @@ describe("Public booking routes", () => { .getServer() .post(`/api/booking/pages/${slug}/reservations`) .send({ - slotStart: "2026-09-07T10:00:00.000Z", + slotStart: `${BOOKING_MONDAY}T10:00:00.000Z`, guestName: "Ada Lovelace", guestEmail: "ada@example.com", guestTimeZone: "Europe/London", diff --git a/packages/web/src/common/styles/theme-css.test.ts b/packages/web/src/common/styles/theme-css.test.ts index 8369ea9de..d9511aff0 100644 --- a/packages/web/src/common/styles/theme-css.test.ts +++ b/packages/web/src/common/styles/theme-css.test.ts @@ -230,4 +230,16 @@ describe("Tailwind theme CSS", () => { /data-view="sidebar"\] \.react-datepicker__day[\s\S]{0,120}color:\s*inherit/, ); }); + + it("paints month-picker selection with ink, not accent", () => { + expect(indexCss).toMatch( + /\.c-month-picker \.c-date-picker[\s\S]*--date-picker-selected-bg:\s*var\(--text\)/, + ); + expect(indexCss).toMatch( + /\.c-month-picker \.c-date-picker[\s\S]*--date-picker-selected-text:\s*var\(--background\)/, + ); + expect(indexCss).not.toMatch( + /\.c-month-picker \.c-date-picker[\s\S]*day--today:not\([^)]+\) \{\s*color:\s*var\(--accent\)/, + ); + }); }); diff --git a/packages/web/src/components/Sidebar/MonthPicker/MonthPicker.test.tsx b/packages/web/src/components/Sidebar/MonthPicker/MonthPicker.test.tsx index a7ae5b0a7..42c08bd18 100644 --- a/packages/web/src/components/Sidebar/MonthPicker/MonthPicker.test.tsx +++ b/packages/web/src/components/Sidebar/MonthPicker/MonthPicker.test.tsx @@ -255,15 +255,36 @@ describe("MonthPicker", () => { expect(tabStop).toHaveFocus(); }); - it("teaches the focus key and the unit in its caption", () => { - const { rerender } = renderPicker( - , - ); + it("teaches the focus key first, then arrow icons, then Enter", async () => { + const user = userEvent.setup({ skipHover: true }); + renderPicker(); const picker = screen.getByRole("group", { name: "Date navigation" }); - expect(picker.textContent).toContain("I"); - expect(picker.textContent).toContain("Arrows move by week"); + expect(picker).toHaveTextContent("I focuses the picker"); + expect(picker).not.toHaveTextContent("Arrows"); + expect(picker).not.toHaveTextContent("move by week"); + expect(picker).not.toHaveTextContent("opens it"); - rerender( + act(() => getTabStopDay()?.focus()); + expect(picker).toHaveTextContent("move by week"); + expect(picker).not.toHaveTextContent("focuses the picker"); + expect(picker).not.toHaveTextContent("Arrows"); + expect(picker.querySelector("[data-testid='arrowup-icon']")).not.toBeNull(); + expect( + picker.querySelector("[data-testid='arrowdown-icon']"), + ).not.toBeNull(); + expect(picker).not.toHaveTextContent("opens it"); + + await user.keyboard("{ArrowDown}"); + expect(picker).toHaveTextContent("Enter"); + expect(picker).toHaveTextContent("opens it"); + const enterKeycap = [ + ...picker.querySelectorAll("[aria-hidden='true']"), + ].find((node) => node.textContent === "Enter"); + expect(enterKeycap?.className).toContain("c-keycap"); + }); + + it("names the day unit once the picker is focused", () => { + renderPicker( { viewStart={dayjs("2026-05-13")} />, ); - expect(picker.textContent).toContain("Arrows move by day"); + const picker = screen.getByRole("group", { name: "Date navigation" }); + expect(picker).toHaveTextContent("I focuses the picker"); + + act(() => getTabStopDay()?.focus()); + expect(picker).toHaveTextContent("move by day"); + expect(picker).not.toHaveTextContent("opens it"); }); it("highlights the visible Sun-Sat window, not adjacent days", () => { diff --git a/packages/web/src/components/Sidebar/MonthPicker/MonthPicker.tsx b/packages/web/src/components/Sidebar/MonthPicker/MonthPicker.tsx index bb0a60dc9..5f6c0d3f3 100644 --- a/packages/web/src/components/Sidebar/MonthPicker/MonthPicker.tsx +++ b/packages/web/src/components/Sidebar/MonthPicker/MonthPicker.tsx @@ -10,9 +10,9 @@ import dayjs, { type Dayjs } from "@core/util/date/dayjs"; import { TrialBadge } from "@web/billing/TrialBadge"; import { ID_DATEPICKER_SIDEBAR } from "@web/common/constants/web.constants"; import { DatePicker } from "@web/components/DatePicker/DatePicker"; -import { ShortcutKeys } from "@web/components/Shortcuts/ShortcutKeys"; import { POINTER_ACTIONS } from "@web/shortcuts/keyboard-only/pointer-action"; import { pageJumpAttrs } from "@web/shortcuts/page-jump/page-jump.targets"; +import { MonthPickerHint } from "./MonthPickerHint"; import { type MonthPickerUnit, normalizePickerCursor, @@ -166,7 +166,7 @@ export const MonthPicker: FC = ({ onMouseDownCapture={swallowDayPointer} {...pageJumpAttrs("month-picker")} > -
+ = ({ view="sidebar" withTodayButton={true} /> - - focuses the picker · Arrows move by {unit} · - Enter opens it - -
+ ); }; diff --git a/packages/web/src/components/Sidebar/MonthPicker/MonthPickerHint.tsx b/packages/web/src/components/Sidebar/MonthPicker/MonthPickerHint.tsx new file mode 100644 index 000000000..12661b41e --- /dev/null +++ b/packages/web/src/components/Sidebar/MonthPicker/MonthPickerHint.tsx @@ -0,0 +1,83 @@ +import { type FC, type ReactNode, useState } from "react"; +import { ShortcutKeys } from "@web/components/Shortcuts/ShortcutKeys"; +import { type MonthPickerUnit } from "./monthPickerCursor"; + +interface Props { + children: ReactNode; + unit: MonthPickerUnit; +} + +const hintClassName = + "c-context-tooltip top-full bottom-auto left-1/2 mt-1.5 mb-0 flex -translate-x-1/2 items-center gap-1.5"; + +function statusText( + isFocused: boolean, + hasMovedWithArrows: boolean, + unit: MonthPickerUnit, +): string { + if (!isFocused) return "I focuses the picker"; + if (hasMovedWithArrows) { + return `Up and down arrows move by ${unit}. Enter opens it.`; + } + return `Up and down arrows move by ${unit}`; +} + +/** + * Caption under the month grid. Hover (or any unfocused glance) only + * advertises `I`. Focusing the picker reveals the arrow keycaps; using an + * arrow then adds Enter, styled as the same kind of keycap as `I`. + * + * Hint state lives here so updating it does not re-render react-datepicker + * (a parent setState resets its keyboard cursor). + */ +export const MonthPickerHint: FC = ({ children, unit }) => { + const [isFocused, setIsFocused] = useState(false); + const [hasMovedWithArrows, setHasMovedWithArrows] = useState(false); + + return ( +
{ + const next = event.relatedTarget; + if (next instanceof Node && event.currentTarget.contains(next)) return; + setIsFocused(false); + }} + onFocusCapture={() => setIsFocused(true)} + onKeyDownCapture={(event) => { + if ( + event.key === "ArrowUp" || + event.key === "ArrowDown" || + event.key === "ArrowLeft" || + event.key === "ArrowRight" + ) { + setHasMovedWithArrows(true); + } + }} + > + {children} + + + {statusText(isFocused, hasMovedWithArrows, unit)} + + {isFocused ? ( + <> + + move by {unit} + {hasMovedWithArrows ? ( + <> + · + + opens it + + ) : null} + + ) : ( + <> + + focuses the picker + + )} + +
+ ); +}; diff --git a/packages/web/src/index.css b/packages/web/src/index.css index 8eb180441..6661e2978 100644 --- a/packages/web/src/index.css +++ b/packages/web/src/index.css @@ -1217,6 +1217,12 @@ * @utility form needed are no longer required. */ .c-month-picker .c-date-picker { + /* Ink fill, not accent: the cyan week/day fill steals attention from the + grid. --text on --background is high-contrast in both themes and matches + the charcoal circle on Light Beach. */ + --date-picker-selected-bg: var(--text); + --date-picker-selected-text: var(--background); + & .react-datepicker__month-container, & .react-datepicker__month { overflow: visible; @@ -1228,7 +1234,20 @@ color: var(--text); } & .react-datepicker__day--today:not(.react-datepicker__day--selected) { - color: var(--accent); + position: relative; + isolation: isolate; + color: var(--text); + } + & + .react-datepicker__day--today:not( + .react-datepicker__day--selected + )::before { + position: absolute; + inset: -2px; + z-index: -1; + border-radius: 50%; + background: var(--date-picker-hover-bg); + content: ""; } & .react-datepicker__day--outside-month { color: var(--text-muted); @@ -1273,8 +1292,9 @@ /* * Week mode: the cursor is a whole week row. react-datepicker's showWeekPicker * marks every day in the week as selected, so the per-day circle above is - * replaced by one accent capsule on the row, and the focus ring wraps the - * row instead of its first day (the only tab stop). + * replaced by one muted capsule on the row, and the focus ring wraps the + * row instead of its first day (the only tab stop). Today keeps an ink + * circle so the current day still reads apart from the week bar. */ .c-month-picker[data-picker-unit="week"] .c-date-picker { & .react-datepicker__day--selected::before { @@ -1282,7 +1302,7 @@ } & .react-datepicker__day--selected, & .react-datepicker__day--selected:hover { - color: var(--date-picker-selected-text); + color: var(--text); } & .react-datepicker__week--selected { position: relative; @@ -1291,13 +1311,27 @@ position: absolute; inset: 0 -3px; border-radius: var(--radius-default); - background: var(--date-picker-selected-bg); + background: var(--date-picker-hover-bg); content: ""; } & .react-datepicker__week--selected .react-datepicker__day { position: relative; z-index: 10; } + & .react-datepicker__week--selected .react-datepicker__day--today::before { + position: absolute; + inset: -2px; + z-index: -1; + border-radius: 50%; + background: var(--text); + content: ""; + } + & .react-datepicker__week--selected .react-datepicker__day--today, + & .react-datepicker__week--selected .react-datepicker__day--today:hover { + position: relative; + isolation: isolate; + color: var(--background); + } & .react-datepicker__day:focus-visible { outline: none; }