Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
1cc66ae
feat(web): add tests for onboarding improvements
tyler-dane Jan 2, 2026
bcd9704
fix(web): skip task seeding in test environment
tyler-dane Jan 2, 2026
6cf674d
fix(web): fix test for cmd+k keyboard event handling
tyler-dane Jan 2, 2026
367fd91
fix(backend): prevent duplicate instance when COUNT is specified in r…
tyler-dane Jan 2, 2026
c714c57
refactor(web): extract onboarding useEffects into separate custom hooks
tyler-dane Jan 2, 2026
889f94a
fix(web): fix failing onboarding tests
tyler-dane Jan 2, 2026
82e4fe2
feat(web): implement command palette guide for onboarding
tyler-dane Jan 2, 2026
64a29d8
feat(web): enhance onboarding with command palette guide tests
tyler-dane Jan 2, 2026
92b182a
refactor(web): remove OnboardingOverlay component and integrate CmdPa…
tyler-dane Jan 2, 2026
b94777b
feat(web): enhance onboarding guide with step completion tracking
tyler-dane Jan 2, 2026
f151324
feat(web): refactor onboarding storage management and enhance progres…
tyler-dane Jan 2, 2026
bb54bf1
feat(web): update CmdPaletteGuide to display contextual welcome messages
tyler-dane Jan 2, 2026
1559e68
feat(web): extend CmdPaletteGuide with step 4 and enhance onboarding …
tyler-dane Jan 2, 2026
e5b122c
feat(web): refactor onboarding steps to use constants for improved cl…
tyler-dane Jan 2, 2026
d5cf873
feat(web): enhance onboarding experience with new steps and improved …
tyler-dane Jan 2, 2026
410794c
refactor(web): standardize onboarding storage utilities and improve s…
tyler-dane Jan 3, 2026
c4fd05c
fix(web): update CmdPaletteGuide step counts and remove deprecated step
tyler-dane Jan 3, 2026
b972901
feat(web): integrate CmdPaletteGuide into CalendarView and enhance su…
tyler-dane Jan 3, 2026
5ac651d
feat(web): integrate CmdPaletteGuide into AuthenticatedLayout and enh…
tyler-dane Jan 3, 2026
5cc9ad4
Merge branch 'main' into feat/onboarding
tyler-dane Jan 3, 2026
77b0ba9
Update packages/web/src/auth/UserProvider.tsx
tyler-dane Jan 3, 2026
3601507
fix(web): address PR review comments for onboarding flow
tyler-dane Jan 3, 2026
811ab9f
refactor(web): improve useAuthPrompt and extract useUser hook
tyler-dane Jan 3, 2026
701a39a
chore: revert `gcal.event.rrule.ts`
tyler-dane Jan 3, 2026
895c81f
chore: add dexie and dexie-react-hooks dependencies to package.json
tyler-dane Jan 3, 2026
64d91b2
chore: add baseline-browser-mapping and fake-indexeddb dependencies t…
tyler-dane Jan 3, 2026
5ba256a
feat(web): enhance event creation saga for unauthenticated users
tyler-dane Jan 4, 2026
9e083f0
feat(web): add unit tests for getUserId function in auth.util
tyler-dane Jan 4, 2026
eeb824f
feat(web): implement IndexedDB storage for events and enhance saga fo…
tyler-dane Jan 4, 2026
5f6205d
test(web): enhance tests for Sidebar interactions and session handling
tyler-dane Jan 4, 2026
bac8b27
feat(web): implement EventRepository with local and remote storage ha…
tyler-dane Jan 4, 2026
eb7ed12
feat(web): add event repository utilities for local and remote storage
tyler-dane Jan 4, 2026
18b7a52
refactor(tests): remove Calendar.render.test.tsx file
tyler-dane Jan 4, 2026
f1adaa8
feat(tests): add unit tests for LocalEventRepository and LocalTaskRep…
tyler-dane Jan 4, 2026
d322254
Update packages/web/src/routers/loaders.ts
tyler-dane Jan 4, 2026
5351e68
fix(tests): remove showCmdPaletteTutorial from useAuthPrompt test
tyler-dane Jan 4, 2026
cac83ab
Update packages/web/src/common/repositories/event/event.repository.in…
tyler-dane Jan 4, 2026
d8b7a96
feat(tests): refactor Jest configuration to support multiple projects
tyler-dane Jan 4, 2026
e1eebd3
feat(onboarding): implement DayOnboardingOverlays component and relat…
tyler-dane Jan 4, 2026
92c5ef6
feat(onboarding): integrate onboarding overlays and related components
tyler-dane Jan 4, 2026
83ada8a
feat(onboarding): enhance CmdPaletteGuide with dynamic instructions a…
tyler-dane Jan 4, 2026
38139f8
feat(onboarding): introduce NAVIGATE_TO_DAY step and update onboardin…
tyler-dane Jan 5, 2026
7148725
refactor(onboarding): streamline CmdPaletteGuide and onboarding step …
tyler-dane Jan 5, 2026
07a4d82
refactor(event): remove isOptimistic flag from event handling
tyler-dane Jan 5, 2026
cd921a6
refactor(event): simplify event saga by removing unauthenticated user…
tyler-dane Jan 5, 2026
739fa8a
feat(auth): add AUTH_PROMPT_DISMISSED key to local storage management
tyler-dane Jan 5, 2026
6c7464a
feat(auth): refactor local storage management for authentication states
tyler-dane Jan 5, 2026
cf2f477
feat(auth): implement useIsSignupComplete hook and refactor onboardin…
tyler-dane Jan 5, 2026
5a9d041
Merge branch 'main' into feat/onboarding
tyler-dane Jan 5, 2026
e40ca95
feat(auth): update authentication flow to redirect to Day view
tyler-dane Jan 5, 2026
76cb0fe
feat(event): refactor event editing to utilize session and repository
tyler-dane Jan 5, 2026
9a612d4
refactor(event): update event repository methods and remove unused Re…
tyler-dane Jan 5, 2026
4d6cfb5
feat(sync): implement event syncing and enhance session management
tyler-dane Jan 7, 2026
01d1b8e
feat(database): initialize database before application startup and en…
tyler-dane Jan 11, 2026
05b7b0e
fix(web): ensure events display and can be edited
tyler-dane Jan 11, 2026
d2fd159
refactor(auth): update AuthPrompt text and onboarding notices
tyler-dane Jan 11, 2026
ef4667c
refactor(event): enhance event fetching logic with detailed logging a…
tyler-dane Jan 11, 2026
f0ccfb5
feat(onboarding): introduce onboarding progress schema and default state
tyler-dane Jan 11, 2026
12ccd37
feat(auth): enhance authentication state management with Zod validation
tyler-dane Jan 11, 2026
3ed7c68
test(e2e): add passing CRUD tests, part I
tyler-dane Jan 6, 2026
0ac9183
test(e2e): adding failing e2e tests (skipped)
tyler-dane Jan 6, 2026
c588194
chore: update playwright config with TEST_PORT
tyler-dane Jan 6, 2026
6bb0e2a
chore: update playwright and webpack configs for e2e tests
tyler-dane Jan 7, 2026
a2d4098
fix(a11y): update web for expected behavior
tyler-dane Jan 7, 2026
3e5e37c
fix(test): add isMobile skip
tyler-dane Jan 7, 2026
964dc28
chore(config): update web server command for development environment
tyler-dane Jan 11, 2026
a4cace0
chore(config): change default port for web server to 9080
tyler-dane Jan 11, 2026
dca5f6d
feat(auth): integrate Google Calendar login in command palettes
tyler-dane Jan 11, 2026
37d1ffa
refactor(auth): streamline Google Calendar sync process and update UI…
tyler-dane Jan 11, 2026
ddc5928
feat(auth): implement session management and enhance authentication c…
tyler-dane Jan 11, 2026
56d1337
refactor(event): remove console logs from event repository and authen…
tyler-dane Jan 11, 2026
75fc3f4
fix(api): enhance error handling for /user/profile 404 responses
tyler-dane Jan 11, 2026
85adcdc
test(tasks): add regression test to prevent localStorage overwrite on…
tyler-dane Jan 11, 2026
15c8a78
refactor(storage): remove logging functions and streamline database e…
tyler-dane Jan 11, 2026
7a1893a
feat(calendar): enhance Google Calendar import functionality and UI
tyler-dane Jan 15, 2026
2dfe7e0
feat(app): enhance database initialization and error handling
tyler-dane Jan 15, 2026
b3476ea
feat(app): improve database initialization and error handling
tyler-dane Jan 15, 2026
9d51126
test(sync): enhance SyncController tests to validate import result st…
tyler-dane Jan 15, 2026
0abc2dd
fix(config): update port configuration for web server
tyler-dane Jan 15, 2026
700ce5b
feat(tests): enhance event test utilities with retry logic and timeou…
tyler-dane Jan 15, 2026
217dd69
feat(auth): implement UserContext and UserProvider for user state man…
tyler-dane Jan 16, 2026
5904478
feat(auth): introduce useSession hook for session management
tyler-dane Jan 16, 2026
b5b6885
feat(modal): add CalendarImportCompleteModal component and associated…
tyler-dane Jan 16, 2026
f15c1d1
refactor(checkbox): remove CheckBox component and associated files
tyler-dane Jan 16, 2026
dd7ec2b
fix(sagas): improve error handling in event sagas and update imports
tyler-dane Jan 16, 2026
2da6826
refactor(sagas): enhance type safety and simplify action handling
tyler-dane Jan 16, 2026
491220d
fix(loaders): integrate task seeding for initial date setup
tyler-dane Jan 16, 2026
a846208
refactor(login): remove Login component and styles; update Logout view
tyler-dane Jan 16, 2026
5598452
refactor(NotFound): remove unused React import; update task tests to …
tyler-dane Jan 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions packages/backend/src/event/classes/gcal.event.rrule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,29 @@ export class GcalEventRRule extends RRule {
index < GCAL_MAX_RECURRENCES,
): Date[] {
const dates = super.all(iterator);

// If no dates were generated, return empty array
if (dates.length === 0) {
return [];
}

const firstInstance = dates[0];
const firstInstanceStartDate = dayjs(firstInstance).tz(this.#timezone);
const includesDtStart = this.#startDate.isSame(firstInstanceStartDate);
const rDates = includesDtStart ? [] : [this.#startDate.toDate()];

// Check if dtstart is already included in the generated dates
// Use a more lenient comparison to handle timezone precision issues
const includesDtStart =
this.#startDate.isSame(firstInstanceStartDate, "minute") ||
dates.some((date) => {
const dateInTz = dayjs(date).tz(this.#timezone);
return this.#startDate.isSame(dateInTz, "minute");
});
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check for includesDtStart performs a potentially expensive operation on every element in the dates array using dates.some(). This is redundant when the first check this.#startDate.isSame(firstInstanceStartDate, "minute") already succeeds. The dates.some() should only execute if the first condition is false. Use an if-else pattern or short-circuit evaluation to avoid unnecessary iteration.

Suggested change
const includesDtStart =
this.#startDate.isSame(firstInstanceStartDate, "minute") ||
dates.some((date) => {
const dateInTz = dayjs(date).tz(this.#timezone);
return this.#startDate.isSame(dateInTz, "minute");
});
const includesDtStart = this.#startDate.isSame(
firstInstanceStartDate,
"minute",
)
? true
: dates.some((date) => {
const dateInTz = dayjs(date).tz(this.#timezone);
return this.#startDate.isSame(dateInTz, "minute");
});

Copilot uses AI. Check for mistakes.

// Only add dtstart if it's not already included and COUNT is not specified
// When COUNT is specified, RRule already includes dtstart in the generated dates
const hasCount = this.options.count !== undefined;
const rDates =
includesDtStart || hasCount ? [] : [this.#startDate.toDate()];

return rDates.concat(dates);
}
Expand Down
15 changes: 13 additions & 2 deletions packages/web/src/auth/UserProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@ export const UserProvider = ({ children }: { children: ReactNode }) => {
profile.current = userProfile;
})
.catch((e) => {
console.error("Failed to get user profile", e);
// For unauthenticated users, this is expected - don't show error
// Only log if it's not a 401/403 (unauthorized) error
const status = (e as { response?: { status?: number } })?.response
?.status;
if (status !== 401 && status !== 403) {
console.error("Failed to get user profile", e);
}
})
.finally(() => {
setIsLoadingUser(false);
Expand All @@ -50,7 +56,12 @@ export const UserProvider = ({ children }: { children: ReactNode }) => {
}
}, [userId, email, posthog]);

if (isLoadingUser || userId === null) {
// Allow unauthenticated users to proceed without blocking
// Only show loader briefly while checking auth status
// Unauthenticated users will have profile.current === null, which is fine
if (isLoadingUser && profile.current === null) {
// Brief loading state - but don't block indefinitely
// The route loader handles auth redirects
return <AbsoluteOverflowLoader />;
}

Expand Down
22 changes: 20 additions & 2 deletions packages/web/src/common/constants/storage.constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
export const STORAGE_KEYS = {
import { z } from "zod";

export const StorageKeySchema = z.enum([
"compass.reminder",
"compass.auth.hasCompletedSignup",
"compass.auth.skipOnboarding",
"compass.onboarding",
]);

export type StorageKey = z.infer<typeof StorageKeySchema>;

export const STORAGE_KEYS: Record<
| "REMINDER"
| "HAS_COMPLETED_SIGNUP"
| "SKIP_ONBOARDING"
| "ONBOARDING_PROGRESS",
StorageKey
> = {
REMINDER: "compass.reminder",
HAS_COMPLETED_SIGNUP: "compass.auth.hasCompletedSignup",
SKIP_ONBOARDING: "compass.auth.skipOnboarding",
};
ONBOARDING_PROGRESS: "compass.onboarding",
} as const;
9 changes: 6 additions & 3 deletions packages/web/src/common/utils/storage/storage.util.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import dayjs from "@core/util/date/dayjs";
import { Task, isTask } from "@web/common/types/task.types";
import {
getOnboardingProgress,
updateOnboardingProgress,
} from "@web/views/Onboarding/utils/onboardingStorage.util";

export const TODAY_TASKS_STORAGE_KEY_PREFIX = "compass.today.tasks";
const STORAGE_INFO_SEEN_KEY = "compass.day.storage-info-seen";
export const COMPASS_TASKS_SAVED_EVENT_NAME = "compass.tasks.saved" as const;

/**
Expand Down Expand Up @@ -104,12 +107,12 @@ export function hasSeenStorageInfo(): boolean {
if (typeof window === "undefined") {
return true;
}
return localStorage.getItem(STORAGE_INFO_SEEN_KEY) === "true";
return getOnboardingProgress().isStorageWarningSeen;
}

export function markStorageInfoAsSeen(): void {
if (typeof window === "undefined") {
return;
}
localStorage.setItem(STORAGE_INFO_SEEN_KEY, "true");
updateOnboardingProgress({ isStorageWarningSeen: true });
}
Comment on lines 107 to 118
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hasSeenStorageInfo and markStorageInfoAsSeen functions have been updated to use the onboarding progress storage, but the old STORAGE_INFO_SEEN_KEY constant is no longer used. This creates a breaking change - users who have already dismissed the storage warning using the old key will see it again. Consider adding migration logic to check the old key and migrate it to the new storage format.

Copilot uses AI. Check for mistakes.
88 changes: 88 additions & 0 deletions packages/web/src/common/utils/storage/task-seeding.util.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Task } from "@web/common/types/task.types";
import {
getDateKey,
loadTasksFromStorage,
saveTasksToStorage,
} from "@web/common/utils/storage/storage.util";
import { seedInitialTasks } from "./task-seeding.util";

// Mock localStorage
const localStorageMock = (() => {
let store: Record<string, string> = {};

return {
getItem: (key: string) => store[key] || null,
setItem: (key: string, value: string) => {
store[key] = value.toString();
},
removeItem: (key: string) => {
delete store[key];
},
clear: () => {
store = {};
},
};
})();

Object.defineProperty(window, "localStorage", {
value: localStorageMock,
});

describe("task-seeding.util", () => {
beforeEach(() => {
localStorageMock.clear();
});

describe("seedInitialTasks", () => {
it("should seed initial tasks when none exist", () => {
const dateKey = getDateKey();
const tasks = seedInitialTasks(dateKey);

expect(tasks).toHaveLength(2);
expect(tasks[0].title).toBe("Review project proposal");
expect(tasks[1].title).toBe("Write weekly report");
expect(tasks[0].status).toBe("todo");
expect(tasks[1].status).toBe("todo");
expect(tasks[0].id).toBeDefined();
expect(tasks[1].id).toBeDefined();
});

it("should return existing tasks if they already exist", () => {
const dateKey = getDateKey();
const existingTask: Task = {
id: "existing-id",
title: "Existing task",
status: "todo",
createdAt: new Date().toISOString(),
order: 0,
};

saveTasksToStorage(dateKey, [existingTask]);
const tasks = seedInitialTasks(dateKey);

expect(tasks).toHaveLength(1);
expect(tasks[0].id).toBe("existing-id");
expect(tasks[0].title).toBe("Existing task");
});

it("should save tasks to localStorage", () => {
const dateKey = getDateKey();
seedInitialTasks(dateKey);

const storedTasks = loadTasksFromStorage(dateKey);
expect(storedTasks).toHaveLength(2);
});

it("should work with different date keys", () => {
const dateKey1 = "2024-01-01";
const dateKey2 = "2024-01-02";

const tasks1 = seedInitialTasks(dateKey1);
const tasks2 = seedInitialTasks(dateKey2);

expect(tasks1).toHaveLength(2);
expect(tasks2).toHaveLength(2);
expect(tasks1[0].id).not.toBe(tasks2[0].id);
});
});
});
39 changes: 39 additions & 0 deletions packages/web/src/common/utils/storage/task-seeding.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { v4 as uuidv4 } from "uuid";
import { Task } from "@web/common/types/task.types";
import {
loadTasksFromStorage,
saveTasksToStorage,
} from "@web/common/utils/storage/storage.util";

/**
* Initial task titles to seed for new users
*/
const INITIAL_TASK_TITLES = ["Review project proposal", "Write weekly report"];

/**
* Seeds initial tasks for a given date if no tasks exist
* @param dateKey - Date key in format YYYY-MM-DD
* @returns Array of seeded tasks
*/
export function seedInitialTasks(dateKey: string): Task[] {
const existingTasks = loadTasksFromStorage(dateKey);

// If tasks already exist, return them
if (existingTasks.length > 0) {
return existingTasks;
}

// Create initial tasks
const initialTasks: Task[] = INITIAL_TASK_TITLES.map((title, index) => ({
id: uuidv4(),
title,
status: "todo" as const,
createdAt: new Date().toISOString(),
order: index,
}));

// Save to localStorage
saveTasksToStorage(dateKey, initialTasks);

return initialTasks;
}
14 changes: 14 additions & 0 deletions packages/web/src/components/GuestLayout/GuestLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Outlet } from "react-router-dom";
import { useGlobalShortcuts } from "@web/views/Calendar/hooks/shortcuts/useGlobalShortcuts";

/**
* Layout component for unauthenticated/guest users
* Provides the same global shortcuts (like cmd+k) as authenticated users
* but without requiring authentication
*/
export const GuestLayout = () => {
// Enable global shortcuts for guest users (including cmd+k palette)
useGlobalShortcuts();

return <Outlet />;
};
4 changes: 2 additions & 2 deletions packages/web/src/routers/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export const router = createBrowserRouter(
import(
/* webpackChunkName: "onboarding" */ "@web/views/Onboarding/OnboardingFlow"
).then((module) => ({
Component: module.OnboardingFlow,
Component: module.default || module.OnboardingFlow,
})),
},
{
Expand All @@ -90,7 +90,7 @@ export const router = createBrowserRouter(
import(
/* webpackChunkName: "onboarding" */ "@web/views/Onboarding/OnboardingFlow"
).then((module) => ({
Component: module.OnboardingFlow,
Component: module.default || module.OnboardingFlow,
})),
},
{
Expand Down
29 changes: 28 additions & 1 deletion packages/web/src/routers/loaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,23 +43,41 @@ export async function loadLogoutData() {
export async function loadLoginData() {
const { authenticated } = await loadAuthenticated();
const { skipOnboarding } = loadOnboardingData();
const { hasCompletedSignup } = loadHasCompletedSignup();

if (authenticated) {
return redirect(skipOnboarding ? ROOT_ROUTES.ROOT : ROOT_ROUTES.ONBOARDING);
}

// For new users (no signup completed), redirect to day view immediately
if (!hasCompletedSignup) {
const { dateString } = loadTodayData();
return redirect(`${ROOT_ROUTES.DAY}/${dateString}`);
}

return { authenticated, skipOnboarding };
}

export async function loadLoggedInData() {
export async function loadLoggedInData({ request }: LoaderFunctionArgs) {
const { authenticated } = await loadAuthenticated();
const { skipOnboarding } = loadOnboardingData();
const { hasCompletedSignup } = loadHasCompletedSignup();

const { USER_SESSION_EXPIRED } = AUTH_FAILURE_REASONS;
const loginRoute = `${ROOT_ROUTES.LOGIN}?reason=${USER_SESSION_EXPIRED}`;

// Allow unauthenticated access to day view for new users
// Check if we're accessing the day route
const url = new URL(request.url);
const pathname = url.pathname;
const isDayRoute = pathname.startsWith(ROOT_ROUTES.DAY);

if (!authenticated) {
// Allow unauthenticated access to day view for new users
if (isDayRoute && !hasCompletedSignup) {
return { authenticated: false, skipOnboarding, hasCompletedSignup };
}

return redirect(
skipOnboarding || hasCompletedSignup
? loginRoute
Expand Down Expand Up @@ -91,6 +109,15 @@ export async function loadSpecificDayData({

if (!success) return redirect(ROOT_ROUTES.DAY);

// Seed initial tasks for this date if none exist (works for both authenticated and unauthenticated users)
// Skip seeding in test environment to avoid interfering with tests
if (typeof process !== "undefined" && process.env.NODE_ENV !== "test") {
const { seedInitialTasks } = await import(
"@web/common/utils/storage/task-seeding.util"
);
seedInitialTasks(dateString);
}

return Promise.resolve({
dateString,
dateInView: dayjs(dateString, dayjs.DateFormat.YEAR_MONTH_DAY_FORMAT),
Expand Down
23 changes: 23 additions & 0 deletions packages/web/src/socket/SocketProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,29 @@ socket.on("error", onError);
const SocketProvider = ({ children }: { children: ReactNode }) => {
const dispatch = useDispatch();

// Only connect socket if user is authenticated
useEffect(() => {
const checkAuthAndConnect = async () => {
try {
const { session } = await import("@web/common/classes/Session");
const authenticated = await session.doesSessionExist();

if (authenticated && !socket.connected) {
socket.connect();
} else if (!authenticated && socket.connected) {
socket.disconnect();
}
} catch (error) {
// If session check fails, assume unauthenticated
if (socket.connected) {
socket.disconnect();
}
}
};

checkAuthAndConnect();
}, []);

const onImportStart = useCallback(
(importing = true) => {
dispatch(importGCalSlice.actions.importing(importing));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,9 @@ export const Reminder = forwardRef(
const handleReminderKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === "Enter") {
e.preventDefault();
e.stopPropagation(); // Prevent the Enter key from bubbling up to global shortcuts
// Blur the input immediately to prevent global Enter handlers from firing
e.currentTarget.blur();
setIsEditing(false);
// Save to localStorage on ENTER
const latestValue = e.currentTarget.textContent || "";
Expand Down
Loading