diff --git a/apps/calendar-macos/CompassKit/Sources/CompassKit/Resources/Fixtures/block-party.tasks.json b/apps/calendar-macos/CompassKit/Sources/CompassKit/Resources/Fixtures/block-party.tasks.json
index 3c3c59c970..2608266f4c 100644
--- a/apps/calendar-macos/CompassKit/Sources/CompassKit/Resources/Fixtures/block-party.tasks.json
+++ b/apps/calendar-macos/CompassKit/Sources/CompassKit/Resources/Fixtures/block-party.tasks.json
@@ -154,10 +154,10 @@
"id": "jump-to-kickoff",
"type": "eventJump",
"title": "Fly across the board",
- "instruction": "Press F to reveal jump letters, then type the one on Team kickoff.",
- "hint": "Press F and letters appear on every block. Type the letter sitting on Team kickoff.",
+ "instruction": "Press f to reveal jump labels, then type the one on Team kickoff.",
+ "hint": "Press f and labels appear on every block. Type the lowercase label on Team kickoff.",
"keycaps": [
- "F"
+ "f"
],
"targetEventId": "game-kickoff"
},
diff --git a/apps/calendar-macos/CompassKit/Sources/CompassKit/Resources/shortcuts.json b/apps/calendar-macos/CompassKit/Sources/CompassKit/Resources/shortcuts.json
index 87aa2beb62..ed6e935fc0 100644
--- a/apps/calendar-macos/CompassKit/Sources/CompassKit/Resources/shortcuts.json
+++ b/apps/calendar-macos/CompassKit/Sources/CompassKit/Resources/shortcuts.json
@@ -1167,7 +1167,7 @@
"eventJump": {
"bareLetter": "f",
"keycaps": [
- "F"
+ "f"
]
},
"moveEvent": {
diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/WeekDayColumnHeaderControl.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/WeekDayColumnHeaderControl.swift
index e42d524d39..8eb7a4fa0d 100644
--- a/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/WeekDayColumnHeaderControl.swift
+++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/WeekDayColumnHeaderControl.swift
@@ -62,7 +62,7 @@ final class WeekDayColumnHeaderControl: NSView {
if showDayJumpPrefixes {
let weekday = EffectiveTimeZone.calendar.component(.weekday, from: date)
- let prefix = DayJumpPrefixes.prefix(forWeekday: weekday - 1).uppercased()
+ let prefix = DayJumpPrefixes.prefix(forWeekday: weekday - 1)
chipLabel.stringValue = dayJumpUseShiftPrefix ? "⇧\(prefix)" : prefix
chipLabel.isHidden = false
chipLabel.textColor = palette.text
diff --git a/apps/calendar-macos/CompassUI/Tests/CompassUIUnitTests/TimeGridDayJumpPrefixTests.swift b/apps/calendar-macos/CompassUI/Tests/CompassUIUnitTests/TimeGridDayJumpPrefixTests.swift
index dd34702ee0..f432dfe713 100644
--- a/apps/calendar-macos/CompassUI/Tests/CompassUIUnitTests/TimeGridDayJumpPrefixTests.swift
+++ b/apps/calendar-macos/CompassUI/Tests/CompassUIUnitTests/TimeGridDayJumpPrefixTests.swift
@@ -3,8 +3,8 @@ import CompassKit
import XCTest
@testable import CompassUI
-/// Web `DayLabels`: while Mod is held the weekday name becomes a ⇧SU / ⇧M
-/// keycap, and event-jump mode shows the bare letter.
+/// Web `DayLabels`: while Mod is held the weekday name becomes a ⇧su / ⇧m
+/// keycap, and event-jump mode shows the bare lowercase prefix.
@MainActor
final class TimeGridDayJumpPrefixTests: XCTestCase {
private let dayKeys = ["2026-05-17", "2026-05-18"]
@@ -16,14 +16,14 @@ final class TimeGridDayJumpPrefixTests: XCTestCase {
func testWeekHeadersShowShiftPrefixedChipsWhileModIsHeld() {
let view = render(showDayJumpPrefixes: true)
- XCTAssertTrue(headerTexts(in: view, dayKey: "2026-05-17").contains("⇧SU"))
- XCTAssertTrue(headerTexts(in: view, dayKey: "2026-05-18").contains("⇧M"))
+ XCTAssertTrue(headerTexts(in: view, dayKey: "2026-05-17").contains("⇧su"))
+ XCTAssertTrue(headerTexts(in: view, dayKey: "2026-05-18").contains("⇧m"))
XCTAssertFalse(headerTexts(in: view, dayKey: "2026-05-17").contains("Sun"))
}
func testEventJumpModeShowsBarePrefixes() {
let view = render(showDayJumpPrefixes: true, dayJumpUseShiftPrefix: false)
- XCTAssertTrue(headerTexts(in: view, dayKey: "2026-05-17").contains("SU"))
+ XCTAssertTrue(headerTexts(in: view, dayKey: "2026-05-17").contains("su"))
}
private func render(showDayJumpPrefixes: Bool, dayJumpUseShiftPrefix: Bool = true) -> TimeGridView {
diff --git a/apps/calendar-web/src/components/ShortcutShowcase/PracticeCalendar.tsx b/apps/calendar-web/src/components/ShortcutShowcase/PracticeCalendar.tsx
index 828b4a7ed7..de158a55f2 100644
--- a/apps/calendar-web/src/components/ShortcutShowcase/PracticeCalendar.tsx
+++ b/apps/calendar-web/src/components/ShortcutShowcase/PracticeCalendar.tsx
@@ -99,7 +99,7 @@ const PracticeBlock: FC<{
}`}
data-practice-jump-letter={jumpLetter}
>
- {jumpLetter.toUpperCase()}
+ {jumpLetter}
)}
diff --git a/apps/calendar-web/src/components/ShortcutShowcase/ShortcutShowcase.test.tsx b/apps/calendar-web/src/components/ShortcutShowcase/ShortcutShowcase.test.tsx
index 2639a7b8cb..ad6eb60fa3 100644
--- a/apps/calendar-web/src/components/ShortcutShowcase/ShortcutShowcase.test.tsx
+++ b/apps/calendar-web/src/components/ShortcutShowcase/ShortcutShowcase.test.tsx
@@ -195,13 +195,13 @@ describe("ShortcutShowcase", () => {
pressKey("Enter");
// Esc-skip to the jump task. With every board task skipped, letters run
- // over the three seed events alone, so kickoff's chip is A here.
+ // over the three seed events alone, so kickoff's chip is a here.
for (let i = 0; i < 7; i += 1) pressKey("Escape");
expect(screen.getByText("Fly across the board")).toBeTruthy();
- expect(screen.getByText("A")).toBeTruthy();
+ expect(screen.getByText("a")).toBeTruthy();
pressKey("f");
// The chips on the blocks join the one already on the card.
- expect(screen.getAllByText("A").length).toBe(2);
+ expect(screen.getAllByText("a").length).toBe(2);
// Skip past the jump and page-jump tasks to reach the palette task:
// its Mod+K hint becomes Esc once the palette opens.
diff --git a/apps/calendar-web/src/components/ShortcutShowcase/game.state.test.ts b/apps/calendar-web/src/components/ShortcutShowcase/game.state.test.ts
index 52ba9e2860..036fa5b2c1 100644
--- a/apps/calendar-web/src/components/ShortcutShowcase/game.state.test.ts
+++ b/apps/calendar-web/src/components/ShortcutShowcase/game.state.test.ts
@@ -408,7 +408,7 @@ describe("keycap progress", () => {
>;
expect(task.id).toBe("jump-to-kickoff");
// The full sequence is on the card up front: H, then kickoff's letter.
- expect(getDisplayKeycaps(task, state)).toEqual(["F", "S"]);
+ expect(getDisplayKeycaps(task, state)).toEqual(["f", "s"]);
expect(getNextKeycapIndex(task, state)).toBe(0);
state = handleGameKey(state, key.jump, T0);
diff --git a/apps/calendar-web/src/components/ShortcutShowcase/game.state.ts b/apps/calendar-web/src/components/ShortcutShowcase/game.state.ts
index 07c75f0dbc..f5f6db8203 100644
--- a/apps/calendar-web/src/components/ShortcutShowcase/game.state.ts
+++ b/apps/calendar-web/src/components/ShortcutShowcase/game.state.ts
@@ -536,7 +536,7 @@ export const getDisplayKeycaps = (
): readonly string[] => {
if (task.type === "eventJump") {
const letter = getJumpLetters(state.practice)[task.targetEventId];
- return letter ? [...task.keycaps, letter.toUpperCase()] : task.keycaps;
+ return letter ? [...task.keycaps, letter] : task.keycaps;
}
if (
(task.type === "palette" && state.simOverlay === "palette") ||
diff --git a/apps/calendar-web/src/components/ShortcutShowcase/game.tasks.ts b/apps/calendar-web/src/components/ShortcutShowcase/game.tasks.ts
index 8fdef0ade6..4261483443 100644
--- a/apps/calendar-web/src/components/ShortcutShowcase/game.tasks.ts
+++ b/apps/calendar-web/src/components/ShortcutShowcase/game.tasks.ts
@@ -210,8 +210,8 @@ export const RUN_TASKS: readonly GameTask[] = [
type: "eventJump",
title: "Fly across the board",
instruction:
- "Press F to reveal jump letters, then type the one on Team kickoff.",
- hint: "Press F and letters appear on every block. Type the letter sitting on Team kickoff.",
+ "Press f to reveal jump labels, then type the one on Team kickoff.",
+ hint: "Press f and labels appear on every block. Type the lowercase label on Team kickoff.",
keycaps: KEYMAP.eventJump.keycaps,
targetEventId: "game-kickoff",
},
diff --git a/apps/calendar-web/src/shortcuts/shift-hint/ShiftHintOverlay.test.tsx b/apps/calendar-web/src/shortcuts/shift-hint/ShiftHintOverlay.test.tsx
index b63f75ba85..20f2fb5597 100644
--- a/apps/calendar-web/src/shortcuts/shift-hint/ShiftHintOverlay.test.tsx
+++ b/apps/calendar-web/src/shortcuts/shift-hint/ShiftHintOverlay.test.tsx
@@ -76,7 +76,7 @@ describe("ShiftHintOverlay", () => {
render();
- expect(overlayRoot()?.textContent).toBe("M1");
+ expect(overlayRoot()?.textContent).toBe("m1");
expect(overlayChip()?.style.top).toBe("224px");
expect(overlayChip()?.style.left).toBe("214px");
});
@@ -108,7 +108,7 @@ describe("ShiftHintOverlay", () => {
render();
- expect(overlayChip()?.textContent).toBe("SU1");
+ expect(overlayChip()?.textContent).toBe("su1");
expect(overlayChip()?.style.top).toBe("204px");
expect(overlayChip()?.style.left).toBe("206px");
});
diff --git a/apps/calendar-web/src/shortcuts/shift-hint/ShiftHintOverlay.tsx b/apps/calendar-web/src/shortcuts/shift-hint/ShiftHintOverlay.tsx
index 913713407b..14c734b59b 100644
--- a/apps/calendar-web/src/shortcuts/shift-hint/ShiftHintOverlay.tsx
+++ b/apps/calendar-web/src/shortcuts/shift-hint/ShiftHintOverlay.tsx
@@ -31,8 +31,8 @@ export function ShiftHintOverlay({ hints }: { hints: ActiveShiftHint[] }) {
return null;
}
- const label = hint.hint.toUpperCase();
- // Longer labels (SU1, F10) need more room than the old AA chips.
+ const label = hint.hint;
+ // Longer labels (su1, f10) need more room than the old AA chips.
const chipWidth = Math.max(22, 10 + label.length * 8);
return (
diff --git a/apps/calendar-web/src/shortcuts/shift-hint/assign-shift-hint-keys.ts b/apps/calendar-web/src/shortcuts/shift-hint/assign-shift-hint-keys.ts
index 96156a61d0..13790bf79a 100644
--- a/apps/calendar-web/src/shortcuts/shift-hint/assign-shift-hint-keys.ts
+++ b/apps/calendar-web/src/shortcuts/shift-hint/assign-shift-hint-keys.ts
@@ -1,7 +1,7 @@
/**
* Day-prefix event jump labels.
*
- * SU/M/T/W/R/F/SA + per-day chronological index (W4, SU1, F10), in both week
+ * su/m/t/w/r/f/sa + per-day chronological index (w4, su1, f10), in both week
* and day view. Day view groups on the single date it shows, so its labels are
* that weekday's prefix (W1, W2, …). One scheme for both views is what keeps a
* bare digit free everywhere for quick-time create (shortcuts/quick-time).
diff --git a/apps/calendar-web/src/shortcuts/shift-hint/useShiftHoldEventHints.quick-time.test.tsx b/apps/calendar-web/src/shortcuts/shift-hint/useShiftHoldEventHints.quick-time.test.tsx
index 87ac942b37..72a1972faa 100644
--- a/apps/calendar-web/src/shortcuts/shift-hint/useShiftHoldEventHints.quick-time.test.tsx
+++ b/apps/calendar-web/src/shortcuts/shift-hint/useShiftHoldEventHints.quick-time.test.tsx
@@ -433,7 +433,7 @@ describe("typed-time deferred commit", () => {
);
});
- it("still toggles jump off with f after a half-typed column time", () => {
+ it("exits jump with Esc after a half-typed column time", () => {
const { createAt, type } = mountColumn();
act(() => {
@@ -443,7 +443,7 @@ describe("typed-time deferred commit", () => {
}),
);
type(["9"]);
- dispatch(keydown("f"));
+ dispatch(keydown("Escape"));
});
expect(createAt).not.toHaveBeenCalled();
@@ -519,6 +519,40 @@ describe("typed-time deferred commit", () => {
fridayElements.splice(0, fridayElements.length);
});
+ it("opens the fourth Friday event on bare f4 after flash jump, with no draft", async () => {
+ const { createAt, openEvent } = mountFriday();
+ const fourthId = fridayIds[3]!;
+ const fourthEl = fridayElements[3]!;
+
+ act(() => {
+ dispatch(keydown("f"));
+ });
+ expect(useEventJumpStore.getState().isActive).toBe(true);
+ dispatch(keydown("f"));
+ dispatch(digit("4"));
+
+ await act(
+ () =>
+ new Promise((resolve) => {
+ setTimeout(resolve, DIGIT_AMBIGUOUS_COMMIT_MS * 2 + 50);
+ }),
+ );
+
+ expect(document.activeElement).toBe(fourthEl);
+ expect(openEvent).not.toHaveBeenCalled();
+ expect(createAt).not.toHaveBeenCalled();
+
+ const enter = new KeyboardEvent("keydown", {
+ bubbles: true,
+ cancelable: true,
+ key: "Enter",
+ });
+ act(() => {
+ fourthEl.dispatchEvent(enter);
+ });
+ expect(openEvent).toHaveBeenCalledWith(fourthId);
+ });
+
it("opens the fourth Friday event on f then 4 then Enter, with no draft", () => {
const { createAt, openEvent, pressFriday } = mountFriday();
const fourthId = fridayIds[3]!;
diff --git a/apps/calendar-web/src/shortcuts/shift-hint/useShiftHoldEventHints.test.tsx b/apps/calendar-web/src/shortcuts/shift-hint/useShiftHoldEventHints.test.tsx
index a952b05e0f..c0e5fc2172 100644
--- a/apps/calendar-web/src/shortcuts/shift-hint/useShiftHoldEventHints.test.tsx
+++ b/apps/calendar-web/src/shortcuts/shift-hint/useShiftHoldEventHints.test.tsx
@@ -495,7 +495,7 @@ describe("useShiftHoldEventHints", () => {
expect(useEventJumpStore.getState()).toMatchObject({
isActive: true,
- pointerHintKey: "W2",
+ pointerHintKey: "w2",
pointerHintEventId: EVENT_B,
});
expect(focus).toHaveBeenCalledWith(
@@ -571,7 +571,7 @@ describe("useShiftHoldEventHints", () => {
act(() => {
requestPointerEventJump(clicked);
});
- expect(useEventJumpStore.getState().pointerHintKey).toBe("W2");
+ expect(useEventJumpStore.getState().pointerHintKey).toBe("w2");
focus.mockClear();
act(() => {
@@ -683,7 +683,7 @@ describe("useShiftHoldEventHints", () => {
act(() => {
requestPointerEventJump(EVENT_B);
});
- expect(useEventJumpStore.getState().pointerHintKey).toBe("W2");
+ expect(useEventJumpStore.getState().pointerHintKey).toBe("w2");
act(() => {
rerender({
@@ -694,7 +694,7 @@ describe("useShiftHoldEventHints", () => {
});
});
- expect(useEventJumpStore.getState().pointerHintKey).toBe("W3");
+ expect(useEventJumpStore.getState().pointerHintKey).toBe("w3");
});
it("publishes every visible column as a day key can select empty ones", () => {
@@ -783,20 +783,25 @@ describe("useShiftHoldEventHints", () => {
expect(result.current.hints).toEqual([]);
});
- it("toggles off with a second f", () => {
- const { result } = mountHints();
+ it("selects Friday with bare f while jump mode is on", () => {
+ const friday = WEEK_DAYS[5]!;
+ const fridayId = EventIdSchema.parse("ffffffffffffffffffffffff");
+ const { focus, elements } = mountHints([
+ timedFixture(fridayId, friday.hour(10).format()),
+ ]);
act(() => {
pressEventJump();
+ dispatch("keydown", "f");
});
- expect(useEventJumpStore.getState().isActive).toBe(true);
- expect(result.current.hints).toHaveLength(3);
- act(() => {
- pressEventJump();
- });
- expect(useEventJumpStore.getState().isActive).toBe(false);
- expect(result.current.hints).toEqual([]);
+ expect(useEventJumpStore.getState().isActive).toBe(true);
+ expect(focus).toHaveBeenCalledWith(
+ expect.objectContaining({ eventId: fridayId, element: elements[0] }),
+ );
+ expect(useEventJumpStore.getState().activeDayKeys).toEqual([
+ friday.format("YYYY-MM-DD"),
+ ]);
});
it("focuses Saturday from idle with Shift+S then a", () => {
diff --git a/apps/calendar-web/src/shortcuts/shift-hint/useShiftHoldEventHints.ts b/apps/calendar-web/src/shortcuts/shift-hint/useShiftHoldEventHints.ts
index eae17c82f7..7a1e09153f 100644
--- a/apps/calendar-web/src/shortcuts/shift-hint/useShiftHoldEventHints.ts
+++ b/apps/calendar-web/src/shortcuts/shift-hint/useShiftHoldEventHints.ts
@@ -174,7 +174,8 @@ const buildDayJumpAssignments = (
* its first one. Shift is what makes the day columns always available: bare
* `t`, `p`, and `m` keep their own commands. Once jump mode is on, the labels
* are typed bare (`w`, `w1`…) and win over global shortcuts. Esc exits, a
- * second `f` toggles off. `s` is only the Sunday/Saturday prefix. `e` is not
+ * Esc exits jump mode (`f` is Friday's column letter while jump is on). `s` is
+ * only the Sunday/Saturday prefix. `e` is not
* a day prefix: jump yields to an armed (or about-to-arm) edit sequence so
* `e` then `t` edits the title instead of selecting Tuesday. `c` is left
* unclaimed too, so the create shortcuts can consume the selected column.
@@ -604,15 +605,6 @@ export function useShiftHoldEventHints({
return;
}
- // The flash letter is also Friday's day prefix; bare F toggles off instead
- // of jumping to Friday (Shift+F still enters Friday from idle).
- if (isBareLetterKey(event, KEYMAP.eventJump.bareLetter)) {
- event.preventDefault();
- event.stopPropagation();
- deactivate();
- return;
- }
-
// Empty buffer: digits are times. Once a day letter is typed ("w"),
// 1-2 digits still index that day's events. A four-digit HHMM still
// creates on the selected column so focusing Tuesday then 1230 lands
@@ -749,7 +741,7 @@ export function useShiftHoldEventHints({
eventJumpActions.setActiveDayKeys([assignment.dayKey]);
eventJumpActions.setPointerHint({
eventId: assignment.eventId,
- key: assignment.hint.toUpperCase(),
+ key: assignment.hint,
});
setHints(toActiveHints(assignments, visibleByIdRef.current));
// Focus now so the advertised Enter path works, including when this
@@ -830,7 +822,7 @@ export function useShiftHoldEventHints({
if (!assignment) {
eventJumpActions.setPointerHint(null);
} else {
- const nextKey = assignment.hint.toUpperCase();
+ const nextKey = assignment.hint;
if (nextKey !== useEventJumpStore.getState().pointerHintKey) {
eventJumpActions.setPointerHint({
eventId: assignment.eventId,
diff --git a/apps/calendar-web/src/shortcuts/tips/shortcut-tips.data.test.ts b/apps/calendar-web/src/shortcuts/tips/shortcut-tips.data.test.ts
index 32fe4bb27b..fe46255143 100644
--- a/apps/calendar-web/src/shortcuts/tips/shortcut-tips.data.test.ts
+++ b/apps/calendar-web/src/shortcuts/tips/shortcut-tips.data.test.ts
@@ -26,7 +26,7 @@ describe("getHintPlainText", () => {
expect(plainTextById["create-event"]).toBe("C creates an event");
expect(plainTextById["page-jump"]).toBe(`Hold ${mod} to see jump targets`);
expect(plainTextById["event-jump"]).toBe(
- "F shows event and open-time shortcuts",
+ "f shows event and open-time shortcuts",
);
expect(plainTextById["week-day-focus"]).toBe("Shift+M jumps to Monday");
expect(plainTextById.nudge).toBe("Shift and an arrow moves the event");
@@ -46,10 +46,10 @@ describe("getHintPlainText", () => {
"Shift+R jumps to Thursday",
);
expect(getHintPlainText(weekDayFocusHint("su"))).toBe(
- "Shift+S then U jumps to Sunday",
+ "Shift+S then u jumps to Sunday",
);
expect(getHintPlainText(weekDayFocusHint("sa"))).toBe(
- "Shift+S then A jumps to Saturday",
+ "Shift+S then a jumps to Saturday",
);
});
diff --git a/apps/calendar-web/src/shortcuts/tips/shortcut-tips.data.ts b/apps/calendar-web/src/shortcuts/tips/shortcut-tips.data.ts
index 5696b3a251..873d1e8732 100644
--- a/apps/calendar-web/src/shortcuts/tips/shortcut-tips.data.ts
+++ b/apps/calendar-web/src/shortcuts/tips/shortcut-tips.data.ts
@@ -108,12 +108,7 @@ function weekDayFocusParts(prefix: DayJumpPrefix): readonly ShortcutTipPart[] {
const [first, second] = prefix;
const shifted = { keys: ["Shift", first.toUpperCase()] } as const;
if (!second) return [shifted, ` jumps to ${dayName}`];
- return [
- shifted,
- " then ",
- { key: second.toUpperCase() },
- ` jumps to ${dayName}`,
- ];
+ return [shifted, " then ", { key: second }, ` jumps to ${dayName}`];
}
/** The weekday tip names whichever column the sidebar picked, so it teaches the
diff --git a/apps/calendar-web/src/views/Week/components/Header/DayLabels.test.tsx b/apps/calendar-web/src/views/Week/components/Header/DayLabels.test.tsx
index 3188cdc988..65132d4d24 100644
--- a/apps/calendar-web/src/views/Week/components/Header/DayLabels.test.tsx
+++ b/apps/calendar-web/src/views/Week/components/Header/DayLabels.test.tsx
@@ -80,21 +80,21 @@ describe("DayLabels", () => {
expect(screen.getByText("Tue")).toBeTruthy();
expect(screen.getByText("Wed")).toBeTruthy();
expect(screen.getByText("Thu")).toBeTruthy();
- expect(screen.queryByText("M")).toBeNull();
- expect(screen.queryByText("R")).toBeNull();
+ expect(screen.queryByText("m")).toBeNull();
+ expect(screen.queryByText("r")).toBeNull();
});
it("swaps weekday abbreviations for day-jump prefixes while event jump is on", () => {
eventJumpActions.setActive(true);
renderLabels();
- expect(screen.getByText("M")).toBeTruthy();
- expect(screen.getByText("T")).toBeTruthy();
- expect(screen.getByText("W")).toBeTruthy();
- expect(screen.getByText("R")).toBeTruthy();
+ expect(screen.getByText("m")).toBeTruthy();
+ expect(screen.getByText("t")).toBeTruthy();
+ expect(screen.getByText("w")).toBeTruthy();
+ expect(screen.getByText("r")).toBeTruthy();
expect(screen.queryByText("Mon")).toBeNull();
- expect(screen.getByRole("status").textContent).toContain("Monday M");
- expect(screen.getByRole("status").textContent).toContain("Thursday R");
+ expect(screen.getByRole("status").textContent).toContain("Monday m");
+ expect(screen.getByRole("status").textContent).toContain("Thursday r");
expect(screen.getByRole("status").textContent).toContain(
"Type the day key to focus that column.",
);
@@ -105,8 +105,8 @@ describe("DayLabels", () => {
renderLabels();
// Jump mode is off here, so the column chip advertises the Shift entry.
- expect(screen.getByText("⇧M")).toBeTruthy();
- expect(screen.getByText("⇧R")).toBeTruthy();
+ expect(screen.getByText("⇧m")).toBeTruthy();
+ expect(screen.getByText("⇧r")).toBeTruthy();
expect(screen.queryByText("Mon")).toBeNull();
expect(screen.getByRole("status").textContent).toContain(
"Hold Shift and press the day key to focus that column.",
diff --git a/apps/calendar-web/src/views/Week/components/Header/DayLabels.tsx b/apps/calendar-web/src/views/Week/components/Header/DayLabels.tsx
index 49956706dd..23064b2b9e 100644
--- a/apps/calendar-web/src/views/Week/components/Header/DayLabels.tsx
+++ b/apps/calendar-web/src/views/Week/components/Header/DayLabels.tsx
@@ -28,7 +28,7 @@ import { GridTimezoneLabel } from "@web/timezone/GridTimezoneLabel";
const dayJumpPrefix = (day: Dayjs) => {
const weekday = day.day() as DayJumpWeekday;
- return DAY_JUMP_PREFIX_BY_WEEKDAY[weekday].toUpperCase();
+ return DAY_JUMP_PREFIX_BY_WEEKDAY[weekday];
};
interface Props {
diff --git a/docs/acceptance/shortcuts.md b/docs/acceptance/shortcuts.md
index 84523f5bce..bbaa8793de 100644
--- a/docs/acceptance/shortcuts.md
+++ b/docs/acceptance/shortcuts.md
@@ -307,23 +307,23 @@ After deleting or moving an event, pressing Cmd+Z (Mac) or Ctrl+Z (Windows/Linux
### UX
-Pressing `F` shows event-jump chips. Week view chips use day prefixes (`SU`/`M`/`T`/`W`/`R`/`F`/`SA`) plus a per-day index (`W4`, `SU1`). Day view uses numeric chips (`1`, `2`, …). Pressing a day letter highlights that column and focuses its first event, if it has one; an empty column is still selected, so `C`, `Shift+C`, Shift+Arrow, and typed digits (`1400`) create on it. A following digit focuses that index. From idle, a column is entered with Shift and its day letter (`Shift+W`, `Shift+S` then `U`/`A` for the weekend), which always works: bare `T` stays “go to today”, bare `M` stays “open event menu”, and bare `P` focuses the latest notice. Day view keeps bare digits, since `Shift+1` is `!`. `Esc` exits (a second `F` also toggles off). Holding Mod reveals the same day prefixes on week column headers, shown as `⇧W` while jump mode is off. Bare Shift and Shift+Tab do not show jump chips.
+Pressing `f` shows event-jump chips. Week view chips use day prefixes (`su`/`m`/`t`/`w`/`r`/`f`/`sa`) plus a per-day index (`w4`, `su1`). Day view uses numeric chips (`1`, `2`, …). Type the lowercase label on a chip to focus that event (`f4`, `w2`). Uppercase letters work the same. Pressing a day letter highlights that column and focuses its first event, if it has one; an empty column is still selected, so `c`, `Shift+c`, Shift+Arrow, and typed digits (`1400`) create on it. A following digit focuses that index. From idle, a column is entered with Shift and its day letter (`Shift+w`, `Shift+s` then `u`/`a` for the weekend), which always works: bare `t` stays “go to today”, bare `m` stays “open event menu”, and bare `p` focuses the latest notice. Day view keeps bare digits, since `Shift+1` is `!`. `Esc` exits jump mode. Holding Mod reveals the same day prefixes on week column headers, shown as `⇧w` while jump mode is off. Bare Shift and Shift+Tab do not show jump chips.
### Steps
1. Navigate to `/week` with timed events on at least two different days.
-2. Press `F` once; chips should appear.
-3. Press the day letter on a chip (for example `W` for Wednesday), then optionally a digit (`2`) or use arrow keys.
+2. Press `f` once; chips should appear.
+3. Press the day letter on a chip (for example `w` for Wednesday), then optionally a digit (`2`) or use arrow keys.
4. Press `Esc` to exit.
-5. Without pressing `F`, press `Shift+W` (or `Shift+S` then `A`) and confirm the matching event is focused; a following digit still refines to `W2`.
+5. Without pressing `f`, press `Shift+w` (or `Shift+s` then `a`) and confirm the matching event is focused; a following digit still refines to `w2`.
6. Press Shift alone or Shift+Tab and confirm jump mode does not activate.
### Expected Results
-- Chips appear on events currently visible in the grid when `F` is pressed and stay until Esc. Scrolled-off events keep their jump keys but hide their chips.
-- A day letter highlights that column and focuses the first event; digits refine to `Wn`. On a column with no events the highlight still appears, nothing focuses, and `C` or a typed `HHMM` creates there.
-- Shift + the day letter enters a column from idle, including while an event is focused; `F` flashes every chip (a second bare `F` exits; bare `F` is not Friday's column letter while jump is on). Weekend columns use `Shift+S` then `A` / `U`.
-- Bare `T` still goes to today while jump is off. With an event focused, bare `M` still opens the event menu. With a visible notice, bare `P` still focuses that notice.
+- Chips appear on events currently visible in the grid when `f` is pressed and stay until Esc. Labels are lowercase and match the keys you type. Scrolled-off events keep their jump keys but hide their chips.
+- A day letter highlights that column and focuses the first event; digits refine to `wn`. On a column with no events the highlight still appears, nothing focuses, and `c` or a typed `HHMM` creates there.
+- Shift + the day letter enters a column from idle, including while an event is focused; `f` flashes every chip. While jump is on, `f` is also Friday's column letter (`f` then `4` focuses `f4`). Weekend columns use `Shift+s` then `a` / `u`.
+- Bare `t` still goes to today while jump is off. With an event focused, bare `m` still opens the event menu. With a visible notice, bare `p` still focuses that notice.
- Arrow keys keep jump mode on so letter-then-arrows works.
- Shift alone / Shift+Tab / Shift+J do not toggle jump mode.
- While a modal holds the app lock, or focus is in an editable field, `F` and leaderless jump tokens do not activate jump mode.
diff --git a/e2e/timed/shift-hold-event-hints.spec.ts b/e2e/timed/shift-hold-event-hints.spec.ts
index 574c35d669..185a3c9354 100644
--- a/e2e/timed/shift-hold-event-hints.spec.ts
+++ b/e2e/timed/shift-hold-event-hints.spec.ts
@@ -12,16 +12,16 @@ import {
type CalendarPage = Parameters[0];
const DAY_PREFIX_KEYS: Record = {
- SU: ["s", "u"],
- M: ["m"],
- T: ["t"],
- W: ["w"],
- R: ["r"],
- F: ["f"],
- SA: ["s", "a"],
+ su: ["s", "u"],
+ m: ["m"],
+ t: ["t"],
+ w: ["w"],
+ r: ["r"],
+ f: ["f"],
+ sa: ["s", "a"],
};
-const WEEKDAY_PREFIXES = ["SU", "M", "T", "W", "R", "F", "SA"];
+const WEEKDAY_PREFIXES = ["su", "m", "t", "w", "r", "f", "sa"];
/**
* Creates a timed event on today via `c`, then nudges it by quarter-hour
@@ -197,6 +197,51 @@ test("Shift+Tab does not toggle event jump keys", async ({ page }) => {
await expect(shiftHintOverlay(page)).toHaveCount(0);
});
+test("lowercase day prefix plus digit targets an event without a typed-time draft", async ({
+ page,
+}) => {
+ await prepareTimedCalendarPage(page);
+ await blurActive(page);
+ await page.keyboard.press("Escape");
+ await expect(shiftHintOverlay(page)).toHaveCount(0);
+
+ const titles: string[] = [];
+ for (let index = 0; index < 4; index++) {
+ const title = createEventTitle(`Ordinal Jump ${index + 1}`);
+ titles.push(title);
+ await createTimedEventOffsetBy(page, title, index);
+ }
+
+ await blurActive(page);
+ await page.keyboard.press("f");
+
+ const overlay = shiftHintOverlay(page);
+ await expect(overlay.locator(":scope > span")).toHaveCount(4);
+
+ const labels = await overlay.evaluate((root) =>
+ [...root.querySelectorAll(":scope > span")].map(
+ (node) => node.textContent?.trim() ?? "",
+ ),
+ );
+ expect(labels.every((label) => label === label.toLowerCase())).toBe(true);
+
+ const fourthLabel = labels.find((label) => label.endsWith("4")) ?? labels[3]!;
+ const fourthTitle = titles[3]!;
+ const fourthButton = page
+ .locator("#mainGrid")
+ .getByRole("button", { name: fourthTitle });
+
+ for (const char of fourthLabel) {
+ await page.keyboard.press(char);
+ }
+
+ await expect
+ .poll(async () =>
+ fourthButton.evaluate((el) => el === document.activeElement),
+ )
+ .toBe(true);
+});
+
test("fast Shift+J does not toggle event jump keys", async ({ page }) => {
await prepareTimedCalendarPage(page);
diff --git a/packages/core/src/shortcuts/keymap.ts b/packages/core/src/shortcuts/keymap.ts
index 238d57cdb1..6c4b863a6b 100644
--- a/packages/core/src/shortcuts/keymap.ts
+++ b/packages/core/src/shortcuts/keymap.ts
@@ -41,7 +41,7 @@ export const KEYMAP = {
keycaps: ["ArrowLeft", "ArrowUp", "ArrowDown", "ArrowRight"],
},
editTitle: { sequence: { leader: "e", second: "t" }, keycaps: ["E", "T"] },
- eventJump: { bareLetter: "f", keycaps: ["F"] },
+ eventJump: { bareLetter: "f", keycaps: ["f"] },
moveEvent: {
hotkeys: {
up: "Shift+ArrowUp",