Skip to content

Commit 28e805c

Browse files
authored
Polish planner sidebar UX (#1748)
* fix(web): hide day sidebar planning buckets * fix(web): polish sidebar calendar header * feat(web): add sidebar shortcuts overlay * Refine temporary account sidebar messaging * style(web): polish planner sidebar hierarchy * style(web): refine sidebar account typography * style(web): polish someday sidebar rows
1 parent cb86e9a commit 28e805c

41 files changed

Lines changed: 731 additions & 320 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/web/src/common/utils/shortcut/data/shortcuts.data.test.ts

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,16 @@ describe("shortcuts.data", () => {
66
it("should return default shortcuts when no config provided", () => {
77
const shortcuts = getShortcuts();
88

9-
expect(shortcuts.globalShortcuts).toHaveLength(6);
10-
expect(shortcuts.globalShortcuts[0]).toEqual({ k: "n", label: "Now" });
11-
expect(shortcuts.globalShortcuts[1]).toEqual({ k: "d", label: "Day" });
12-
expect(shortcuts.globalShortcuts[2]).toEqual({ k: "w", label: "Week" });
13-
expect(shortcuts.globalShortcuts[3]).toEqual({
14-
k: "r",
15-
label: "Edit reminder",
16-
});
17-
expect(shortcuts.globalShortcuts[4]).toEqual({ k: "z", label: "Log in" });
18-
expect(shortcuts.globalShortcuts[5]).toEqual({
19-
k: "Mod+k",
20-
label: "Command Palette",
21-
});
9+
expect(shortcuts.globalShortcuts).toEqual([
10+
{ k: "n", label: "Now" },
11+
{ k: "d", label: "Day" },
12+
{ k: "w", label: "Week" },
13+
{ k: "r", label: "Edit reminder" },
14+
{ k: "[", label: "Toggle sidebar" },
15+
{ k: "?", label: "Toggle shortcuts" },
16+
{ k: "z", label: "Log in" },
17+
{ k: "Mod+k", label: "Command Palette" },
18+
]);
2219

2320
expect(shortcuts.dayAgendaShortcuts).toHaveLength(2);
2421
expect(shortcuts.dayAgendaShortcuts[0]).toEqual({
@@ -40,7 +37,7 @@ describe("shortcuts.data", () => {
4037
it("shows logout when authenticated", () => {
4138
const shortcuts = getShortcuts({ isAuthenticated: true });
4239

43-
expect(shortcuts.globalShortcuts[4]).toEqual({ k: "z", label: "Logout" });
40+
expect(shortcuts.globalShortcuts[6]).toEqual({ k: "z", label: "Logout" });
4441
});
4542

4643
it("should show 'Scroll to now' when currentDate is today", () => {

packages/web/src/common/utils/shortcut/data/shortcuts.data.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export const getShortcuts = (config: ShortcutsConfig = {}) => {
2424
{ k: VIEW_SHORTCUTS.day.key, label: VIEW_SHORTCUTS.day.label },
2525
{ k: VIEW_SHORTCUTS.week.key, label: VIEW_SHORTCUTS.week.label },
2626
{ k: "r", label: "Edit reminder" },
27+
{ k: "[", label: "Toggle sidebar" },
28+
{ k: "?", label: "Toggle shortcuts" },
2729
{ k: "z", label: isAuthenticated ? "Logout" : "Log in" },
2830
{ k: "Mod+k", label: "Command Palette" },
2931
];

packages/web/src/components/DatePicker/DatePicker.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export interface Props extends Omit<ReactDatePickerProps, "autoFocus"> {
2727
bgColor?: string;
2828
headerActionsClassName?: string;
2929
headerClassName?: string;
30+
headerEndContent?: React.ReactNode;
3031
inputColor?: string;
3132
isOpen?: boolean;
3233
monthContainerClassName?: string;
@@ -48,6 +49,7 @@ export const DatePicker: React.FC<Props> = (datePickerProps) => {
4849
calendarClassName,
4950
headerActionsClassName,
5051
headerClassName,
52+
headerEndContent,
5153
inputColor,
5254
isOpen = true,
5355
monthContainerClassName,
@@ -108,7 +110,9 @@ export const DatePicker: React.FC<Props> = (datePickerProps) => {
108110
showPopperArrow={false}
109111
renderCustomHeader={(headerProps) => {
110112
const { customHeaderCount, monthDate } = headerProps;
111-
const selectedMonth = dayjs(monthDate).format("MMM YYYY");
113+
const selectedMonth = dayjs(monthDate).format(
114+
view === "sidebar" ? "MMMM YYYY" : "MMM YYYY",
115+
);
112116
const currentMonth = dayjs().format("MMM YYYY");
113117

114118
return (
@@ -136,6 +140,7 @@ export const DatePicker: React.FC<Props> = (datePickerProps) => {
136140
<MonthNavButton
137141
ariaLabel="Previous month"
138142
color={headerColor}
143+
isSidebarStyle={view === "sidebar"}
139144
onClick={() => {
140145
headerProps.decreaseMonth();
141146
}}
@@ -145,6 +150,7 @@ export const DatePicker: React.FC<Props> = (datePickerProps) => {
145150
<MonthNavButton
146151
ariaLabel="Next month"
147152
color={headerColor}
153+
isSidebarStyle={view === "sidebar"}
148154
onClick={() => {
149155
headerProps.increaseMonth();
150156
}}
@@ -168,6 +174,11 @@ export const DatePicker: React.FC<Props> = (datePickerProps) => {
168174
)}
169175
</Flex>
170176
)}
177+
{!customHeaderCount && headerEndContent ? (
178+
<div className="ml-auto flex items-center">
179+
{headerEndContent}
180+
</div>
181+
) : null}
171182
</StyledHeaderFlex>
172183
);
173184
}}

packages/web/src/components/DatePicker/MonthNavButton.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,43 @@ type MonthNavButtonProps = {
66
ariaLabel: string;
77
children: React.ReactNode;
88
color: string;
9+
isSidebarStyle?: boolean;
910
onClick: () => void;
1011
};
1112

1213
export const MonthNavButton = ({
1314
ariaLabel,
1415
children,
1516
color,
17+
isSidebarStyle = false,
1618
onClick,
1719
}: MonthNavButtonProps) => (
1820
<button
1921
aria-label={ariaLabel}
2022
onClick={onClick}
2123
onMouseEnter={(e) => {
24+
if (isSidebarStyle) return;
25+
2226
e.currentTarget.style.backgroundColor = MONTH_NAV_BUTTON_HOVER_COLOR;
2327
}}
2428
onMouseLeave={(e) => {
29+
if (isSidebarStyle) return;
30+
2531
e.currentTarget.style.backgroundColor = "transparent";
2632
}}
2733
style={{
2834
cursor: "pointer",
2935
color,
3036
background: "transparent",
31-
border: "none",
37+
border: "1px solid transparent",
3238
display: "flex",
3339
alignItems: "center",
3440
justifyContent: "center",
3541
width: "24px",
3642
height: "24px",
37-
borderRadius: "50%",
38-
transition: "background-color 0.2s",
43+
borderRadius: isSidebarStyle ? "4px" : "50%",
44+
opacity: isSidebarStyle ? 0.9 : 1,
45+
transition: "background-color 0.2s, border-color 0.2s, opacity 0.2s",
3946
}}
4047
type="button"
4148
>

packages/web/src/components/DatePicker/styled.ts

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,7 @@ const _hoverStyle = `
1313
`;
1414

1515
export const ChangeDayButtonsStyledFlex = styled(Flex)`
16-
& span {
17-
padding: 0 9px;
18-
border-radius: 50%;
19-
20-
&:hover {
21-
${_hoverStyle}
22-
}
23-
24-
&:first-child {
25-
margin-right: 10px;
26-
}
27-
}
16+
gap: 4px;
2817
`;
2918

3019
export const MonthContainerStyled = styled(Flex)`
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { render, screen } from "@testing-library/react";
2+
import userEvent from "@testing-library/user-event";
3+
import { beforeEach, describe, expect, it, mock } from "bun:test";
4+
5+
const mockOpenModal = mock();
6+
let mockEmail: string | undefined;
7+
8+
mock.module("@web/auth/compass/user/hooks/useUser", () => ({
9+
useUser: () => ({
10+
email: mockEmail,
11+
}),
12+
}));
13+
14+
mock.module("@web/components/AuthModal/hooks/useAuthModal", () => ({
15+
useAuthModal: () => ({
16+
openModal: mockOpenModal,
17+
}),
18+
}));
19+
20+
mock.module("@phosphor-icons/react", () => ({
21+
InfoIcon: () => <span aria-hidden="true">info</span>,
22+
}));
23+
24+
const { PlannerAccountSummary } =
25+
require("./PlannerAccountSummary") as typeof import("./PlannerAccountSummary");
26+
27+
describe("PlannerAccountSummary", () => {
28+
beforeEach(() => {
29+
mockEmail = undefined;
30+
mockOpenModal.mockClear();
31+
});
32+
33+
it("shows a sign up prompt for temporary accounts", async () => {
34+
const user = userEvent.setup();
35+
36+
render(<PlannerAccountSummary />);
37+
38+
await user.click(
39+
screen.getByRole("button", {
40+
name: "Temporary account. Sign up to save changes",
41+
}),
42+
);
43+
44+
expect(screen.getByText("Temporary account")).toBeTruthy();
45+
expect(screen.getByText("Sign up")).toBeTruthy();
46+
expect(mockOpenModal).toHaveBeenCalledWith("signUp");
47+
});
48+
49+
it("shows a plain account identity for authenticated accounts", () => {
50+
mockEmail = "ugur@example.com";
51+
52+
render(<PlannerAccountSummary />);
53+
54+
expect(screen.getByText("ugur@example.com")).toBeTruthy();
55+
expect(screen.queryByText("Changes saved")).toBeNull();
56+
expect(screen.queryByRole("button")).toBeNull();
57+
});
58+
});

packages/web/src/components/PlannerSidebar/PlannerAccountSummary/PlannerAccountSummary.tsx

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@ import { InfoIcon } from "@phosphor-icons/react";
22
import { type FC, useCallback } from "react";
33
import { useUser } from "@web/auth/compass/user/hooks/useUser";
44
import { useAuthModal } from "@web/components/AuthModal/hooks/useAuthModal";
5-
import { TooltipWrapper } from "@web/components/Tooltip/TooltipWrapper";
65

7-
const TEMPORARY_ACCOUNT_MESSAGE = "Sign up to save your changes";
6+
const TEMPORARY_ACCOUNT_MESSAGE = "Sign up to save changes";
87

98
export const PlannerAccountSummary: FC = () => {
109
const { email } = useUser();
@@ -15,32 +14,49 @@ export const PlannerAccountSummary: FC = () => {
1514
openModal("signUp");
1615
}, [openModal]);
1716

17+
if (isTemporaryAccount) {
18+
return (
19+
<div className="border-border-primary border-b px-1 pb-3">
20+
<button
21+
aria-label={`${accountLabel}. ${TEMPORARY_ACCOUNT_MESSAGE}`}
22+
className="group flex w-full min-w-0 items-center gap-2 py-1 text-left text-text-light transition-colors duration-150 hover:text-text-lighter focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-primary"
23+
onClick={handleOpenSignUp}
24+
title={TEMPORARY_ACCOUNT_MESSAGE}
25+
type="button"
26+
>
27+
<span className="flex size-5 shrink-0 items-center justify-center text-accent-primary">
28+
<InfoIcon aria-hidden="true" size={15} weight="bold" />
29+
</span>
30+
<span className="flex min-w-0 flex-1 items-baseline gap-1.5">
31+
<span className="truncate font-normal text-text-light text-xs leading-tight">
32+
{accountLabel}
33+
</span>
34+
<span
35+
aria-hidden="true"
36+
className="shrink-0 text-text-light-inactive text-xs"
37+
>
38+
·
39+
</span>
40+
<span className="shrink-0 font-medium text-accent-primary text-xs leading-tight transition-colors duration-150 group-hover:text-text-lighter">
41+
Sign up
42+
</span>
43+
</span>
44+
</button>
45+
</div>
46+
);
47+
}
48+
1849
return (
1950
<div
20-
className="flex min-w-0 items-center gap-2 border-border-primary border-b px-1 pb-3 text-text-light-inactive"
51+
className="flex min-w-0 items-center gap-2 border-border-primary border-b px-1 pb-3 text-text-light"
2152
title={accountLabel}
2253
>
2354
<span
24-
className="min-w-0 flex-1 truncate text-[11px] leading-tight"
55+
className="min-w-0 flex-1 truncate font-normal text-text-light text-xs leading-tight"
2556
translate="no"
2657
>
2758
{accountLabel}
2859
</span>
29-
30-
{isTemporaryAccount ? (
31-
<TooltipWrapper
32-
description={TEMPORARY_ACCOUNT_MESSAGE}
33-
onClick={handleOpenSignUp}
34-
>
35-
<button
36-
aria-label="Temporary account info"
37-
className="inline-flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-default text-text-light-inactive transition-colors hover:bg-bg-secondary hover:text-text-lighter focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-primary"
38-
type="button"
39-
>
40-
<InfoIcon aria-hidden="true" size={14} />
41-
</button>
42-
</TooltipWrapper>
43-
) : null}
4460
</div>
4561
);
4662
};

packages/web/src/components/PlannerSidebar/PlannerMonthPicker/PlannerMonthPicker.tsx

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,22 @@ import { type FC, useEffect, useRef, useState } from "react";
22
import styled from "styled-components";
33
import dayjs, { type Dayjs } from "@core/util/date/dayjs";
44
import { ID_DATEPICKER_SIDEBAR } from "@web/common/constants/web.constants";
5+
import { theme } from "@web/common/styles/theme";
56
import { DatePicker } from "@web/components/DatePicker/DatePicker";
7+
import { SidebarIcon } from "@web/components/Icons/Sidebar";
8+
import { TooltipWrapper } from "@web/components/Tooltip/TooltipWrapper";
69

710
interface Props {
811
monthsShown?: number;
12+
onToggleSidebar?: () => void;
913
onSelectDate: (date: Dayjs) => void;
1014
selectedDate: Dayjs;
1115
}
1216

1317
const plannerMonthPickerClassName =
1418
"[&_.calendar]:!w-full [&_.calendar]:!bg-transparent [&_.calendar]:!shadow-none [&_.react-datepicker]:!border-0 [&_.react-datepicker]:!bg-transparent [&_.react-datepicker]:!shadow-none [&_.react-datepicker\\_\\_day-names]:!mb-0 [&_.react-datepicker\\_\\_header.react-datepicker\\_\\_header]:!px-0 [&_.react-datepicker\\_\\_month-container.react-datepicker\\_\\_month-container]:!bg-transparent [&_.react-datepicker\\_\\_month-container.react-datepicker\\_\\_month-container]:!px-0";
1519

16-
const headerActionsClassName =
17-
"!absolute !inset-x-11 !justify-between [&>div:first-child]:!w-full [&>div:first-child]:!justify-between [&>span]:!hidden";
20+
const headerActionsClassName = "!ml-2.5";
1821

1922
const PlannerMonthPickerFieldset = styled.fieldset`
2023
.react-datepicker__month-container,
@@ -64,6 +67,7 @@ const PlannerMonthPickerFieldset = styled.fieldset`
6467
export const PlannerMonthPicker: FC<Props> = ({
6568
monthsShown,
6669
onSelectDate,
70+
onToggleSidebar,
6771
selectedDate,
6872
}) => {
6973
const selectedDateKey = selectedDate.format(
@@ -103,11 +107,24 @@ export const PlannerMonthPicker: FC<Props> = ({
103107
calendarClassName={ID_DATEPICKER_SIDEBAR}
104108
dayClassName={getPlannerDayClassName}
105109
headerActionsClassName={headerActionsClassName}
106-
headerClassName="!relative !justify-center !px-0 !pb-3"
110+
headerEndContent={
111+
onToggleSidebar ? (
112+
<TooltipWrapper
113+
description="Close sidebar"
114+
onClick={onToggleSidebar}
115+
shortcut="["
116+
>
117+
<span className="flex h-6 w-6 items-center justify-center">
118+
<SidebarIcon color={theme.color.text.light} size={21} />
119+
</span>
120+
</TooltipWrapper>
121+
) : null
122+
}
123+
headerClassName="!relative !justify-start !px-0 !pb-3"
107124
inline
108125
isOpen={true}
109126
monthContainerClassName="!w-auto"
110-
monthTextClassName="!text-xs"
127+
monthTextClassName="!text-l !font-medium"
111128
monthsShown={monthsShown}
112129
onChange={(date) => {
113130
if (!date) return;
@@ -120,7 +137,7 @@ export const PlannerMonthPicker: FC<Props> = ({
120137
selected={focusedDate.toDate()}
121138
shouldCloseOnSelect={false}
122139
view="sidebar"
123-
withTodayButton={true}
140+
withTodayButton={false}
124141
/>
125142
</PlannerMonthPickerFieldset>
126143
);

packages/web/src/components/PlannerSidebar/PlannerSidebar.test.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ const renderSidebar = (props?: { showSomedayEventSections?: boolean }) => {
2929
render(
3030
<PlannerSidebar
3131
calendarDate={dayjs("2026-05-12")}
32+
isShortcutsOpen={false}
33+
onCloseShortcuts={mock()}
34+
onToggleShortcuts={mock()}
3235
onSelectDate={mock()}
3336
shortcutSections={[]}
3437
viewEnd={dayjs("2026-05-16")}

0 commit comments

Comments
 (0)