Skip to content

Commit c37b871

Browse files
committed
feat(webapp): remove favorites from the customize modal, custom action icons
Favorite rows in the customize sidebar modal gain a remove button next to the visibility toggle; removals stage in the modal and apply on Confirm (Cancel discards them). New custom icons replace the previous ones for Remove, Rename, and Customize sidebar. Slack and Vercel favorites render their brand logos at the same size as the organization menu, and favoriting a custom dashboard uses the dashboard chart icon.
1 parent c9eb226 commit c37b871

9 files changed

Lines changed: 180 additions & 27 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export function CrossIcon({ className }: { className?: string }) {
2+
return (
3+
<svg
4+
className={className}
5+
width="24"
6+
height="24"
7+
viewBox="0 0 24 24"
8+
fill="none"
9+
xmlns="http://www.w3.org/2000/svg"
10+
>
11+
<path d="M5 5L19 19M19 5L5 19" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
12+
</svg>
13+
);
14+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
export function RenameIcon({ className }: { className?: string }) {
2+
return (
3+
<svg
4+
className={className}
5+
width="24"
6+
height="24"
7+
viewBox="0 0 24 24"
8+
fill="none"
9+
xmlns="http://www.w3.org/2000/svg"
10+
>
11+
<path
12+
d="M11 4H7.2C6.0799 4 5.51984 4 5.09202 4.21799C4.71569 4.40973 4.40973 4.71569 4.21799 5.09202C4 5.51984 4 6.0799 4 7.2V16.8C4 17.9201 4 18.4802 4.21799 18.908C4.40973 19.2843 4.71569 19.5903 5.09202 19.782C5.51984 20 6.0799 20 7.2 20H16.8C17.9201 20 18.4802 20 18.908 19.782C19.2843 19.5903 19.5903 19.2843 19.782 18.908C20 18.4802 20 17.9201 20 16.8V13"
13+
stroke="currentColor"
14+
strokeWidth="2"
15+
strokeLinecap="round"
16+
strokeLinejoin="round"
17+
/>
18+
<path
19+
d="M9 14.9999V12.4142C9 12.1489 9.10536 11.8946 9.29289 11.707L17.3358 3.66416C18.1168 2.88311 19.3832 2.88311 20.1642 3.66416L20.3358 3.83573C21.1168 4.61678 21.1168 5.88311 20.3358 6.66416L12.2929 14.707C12.1054 14.8946 11.851 14.9999 11.5858 14.9999H9Z"
20+
stroke="currentColor"
21+
strokeWidth="2"
22+
strokeLinecap="square"
23+
strokeLinejoin="round"
24+
/>
25+
</svg>
26+
);
27+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export function SidebarCustomizeIcon({ className }: { className?: string }) {
2+
return (
3+
<svg
4+
className={className}
5+
width="24"
6+
height="24"
7+
viewBox="0 0 24 24"
8+
fill="none"
9+
xmlns="http://www.w3.org/2000/svg"
10+
>
11+
<path
12+
d="M11 5H5C3.89543 5 3 5.89543 3 7V17C3 18.1046 3.89543 19 5 19H11M11 5H19C20.1046 5 21 5.89543 21 7V17C21 18.1046 20.1046 19 19 19H11M11 5V19"
13+
stroke="currentColor"
14+
strokeWidth="2"
15+
strokeLinecap="square"
16+
strokeLinejoin="round"
17+
/>
18+
<path
19+
d="M6.125 8.75C6.125 9.23325 6.51675 9.625 7 9.625C7.48325 9.625 7.875 9.23325 7.875 8.75C7.875 8.26675 7.48325 7.875 7 7.875C6.51675 7.875 6.125 8.26675 6.125 8.75ZM6.125 12C6.125 12.4832 6.51675 12.875 7 12.875C7.48325 12.875 7.875 12.4832 7.875 12C7.875 11.5168 7.48325 11.125 7 11.125C6.51675 11.125 6.125 11.5168 6.125 12ZM6.125 15.25C6.125 15.7332 6.51675 16.125 7 16.125C7.48325 16.125 7.875 15.7332 7.875 15.25C7.875 14.7668 7.48325 14.375 7 14.375C6.51675 14.375 6.125 14.7668 6.125 15.25Z"
20+
fill="currentColor"
21+
stroke="currentColor"
22+
strokeWidth="0.75"
23+
/>
24+
</svg>
25+
);
26+
}

apps/webapp/app/components/navigation/CustomizeSidebarDialog.tsx

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ArrowDownIcon, ArrowUpIcon, EyeIcon, EyeSlashIcon } from "@heroicons/re
33
import { GripVerticalIcon } from "lucide-react";
44
import { useState } from "react";
55
import ReactGridLayout, { type Layout, useContainerWidth } from "react-grid-layout";
6+
import { CrossIcon } from "~/assets/icons/CrossIcon";
67
import { cn } from "~/utils/cn";
78
import { Button } from "../primitives/Buttons";
89
import { DialogContent, DialogFooter, DialogHeader } from "../primitives/Dialog";
@@ -16,6 +17,7 @@ export type CustomizeSidebarItem = {
1617
id: string;
1718
name: string;
1819
icon: RenderIcon;
20+
iconClassName?: string;
1921
defaultHidden?: boolean;
2022
/** Favorites get an inline-editable name in the modal. */
2123
isFavorite?: boolean;
@@ -40,6 +42,7 @@ export type SidebarCustomizationPayload = {
4042
hiddenItems: Record<string, boolean> | null;
4143
sectionItemOrder: Record<string, string[]> | null;
4244
favorites?: Array<{ id: string; label: string }>;
45+
removedFavoriteIds?: string[];
4346
};
4447

4548
type DialogState = {
@@ -50,6 +53,8 @@ type DialogState = {
5053
hidden: Record<string, boolean>;
5154
/** favorite id -> label being edited */
5255
labels: Record<string, string>;
56+
/** favorite ids staged for removal; applied on Confirm */
57+
removed: string[];
5358
};
5459

5560
const FAVORITES_SECTION_ID = "favorites";
@@ -84,7 +89,13 @@ function buildState(
8489
}
8590
}
8691

87-
return { sectionOrder: orderedSections.map((section) => section.id), itemOrders, hidden, labels };
92+
return {
93+
sectionOrder: orderedSections.map((section) => section.id),
94+
itemOrders,
95+
hidden,
96+
labels,
97+
removed: [],
98+
};
8899
}
89100

90101
function arraysEqual(a: string[], b: string[]) {
@@ -142,13 +153,25 @@ export function CustomizeSidebarDialog({
142153
setState((current) => ({ ...current, labels: { ...current.labels, [itemId]: label } }));
143154
};
144155

156+
const removeFavorite = (itemId: string) => {
157+
setState((current) => ({ ...current, removed: [...current.removed, itemId] }));
158+
};
159+
145160
// Reset restores the default layout (positions + visibility) but never touches favorite names
161+
// or staged removals; Cancel is the way out of those
146162
const reset = () =>
147-
setState((current) => ({ ...buildState(sections, undefined), labels: current.labels }));
163+
setState((current) => ({
164+
...buildState(sections, undefined),
165+
labels: current.labels,
166+
removed: current.removed,
167+
}));
148168

149169
const hasBlankLabels = sections.some((section) =>
150170
section.items.some(
151-
(item) => item.isFavorite && (state.labels[item.id] ?? item.name).trim().length === 0
171+
(item) =>
172+
item.isFavorite &&
173+
!state.removed.includes(item.id) &&
174+
(state.labels[item.id] ?? item.name).trim().length === 0
152175
)
153176
);
154177

@@ -158,6 +181,7 @@ export function CustomizeSidebarDialog({
158181
const hiddenOverrides: Record<string, boolean> = {};
159182
for (const section of sections) {
160183
for (const item of section.items) {
184+
if (state.removed.includes(item.id)) continue;
161185
const isHidden = state.hidden[item.id] ?? false;
162186
if (isHidden !== (item.defaultHidden ?? false)) {
163187
hiddenOverrides[item.id] = isHidden;
@@ -175,12 +199,17 @@ export function CustomizeSidebarDialog({
175199
}
176200

177201
const favoritesSection = sections.find((section) => section.id === FAVORITES_SECTION_ID);
178-
const favoriteOrder = state.itemOrders[FAVORITES_SECTION_ID] ?? [];
202+
const favoriteOrder = (state.itemOrders[FAVORITES_SECTION_ID] ?? []).filter(
203+
(id) => !state.removed.includes(id)
204+
);
179205
const favoritesChanged =
180206
favoritesSection !== undefined &&
181-
(!arraysEqual(favoriteOrder, defaults.itemOrders[FAVORITES_SECTION_ID] ?? []) ||
207+
(state.removed.length > 0 ||
208+
!arraysEqual(favoriteOrder, defaults.itemOrders[FAVORITES_SECTION_ID] ?? []) ||
182209
favoritesSection.items.some(
183-
(item) => (state.labels[item.id] ?? item.name).trim() !== item.name
210+
(item) =>
211+
!state.removed.includes(item.id) &&
212+
(state.labels[item.id] ?? item.name).trim() !== item.name
184213
));
185214

186215
// Parts equal to the defaults are sent as null so the stored preference is cleared, not pinned
@@ -193,6 +222,7 @@ export function CustomizeSidebarDialog({
193222
favorites: favoritesChanged
194223
? favoriteOrder.map((id) => ({ id, label: state.labels[id] ?? "" }))
195224
: undefined,
225+
removedFavoriteIds: state.removed.length > 0 ? state.removed : undefined,
196226
};
197227

198228
onConfirm(payload);
@@ -229,12 +259,15 @@ export function CustomizeSidebarDialog({
229259
</div>
230260
<SectionItemList
231261
section={section}
232-
order={state.itemOrders[section.id] ?? section.items.map((item) => item.id)}
262+
order={(state.itemOrders[section.id] ?? section.items.map((item) => item.id)).filter(
263+
(id) => !state.removed.includes(id)
264+
)}
233265
hidden={state.hidden}
234266
labels={state.labels}
235267
onReorder={(itemIds) => reorderItems(section.id, itemIds)}
236268
onToggleHidden={toggleHidden}
237269
onLabelChange={setLabel}
270+
onRemove={removeFavorite}
238271
/>
239272
</div>
240273
))}
@@ -289,6 +322,7 @@ function SectionItemList({
289322
onReorder,
290323
onToggleHidden,
291324
onLabelChange,
325+
onRemove,
292326
}: {
293327
section: CustomizeSidebarSection;
294328
order: string[];
@@ -297,6 +331,7 @@ function SectionItemList({
297331
onReorder: (itemIds: string[]) => void;
298332
onToggleHidden: (itemId: string) => void;
299333
onLabelChange: (itemId: string, label: string) => void;
334+
onRemove: (itemId: string) => void;
300335
}) {
301336
const { width, containerRef } = useContainerWidth({ initialWidth: 416 });
302337

@@ -321,6 +356,7 @@ function SectionItemList({
321356
draggable={options.draggable}
322357
onToggleHidden={() => onToggleHidden(item.id)}
323358
onLabelChange={(label) => onLabelChange(item.id, label)}
359+
onRemove={() => onRemove(item.id)}
324360
/>
325361
);
326362

@@ -359,13 +395,15 @@ function ModalItemRow({
359395
draggable,
360396
onToggleHidden,
361397
onLabelChange,
398+
onRemove,
362399
}: {
363400
item: CustomizeSidebarItem;
364401
isHidden: boolean;
365402
label: string | undefined;
366403
draggable: boolean;
367404
onToggleHidden: () => void;
368405
onLabelChange: (label: string) => void;
406+
onRemove: () => void;
369407
}) {
370408
return (
371409
<div
@@ -378,7 +416,10 @@ function ModalItemRow({
378416
isHidden && "opacity-50"
379417
)}
380418
>
381-
<Icon icon={item.icon} className="size-5 shrink-0 text-text-dimmed" />
419+
<Icon
420+
icon={item.icon}
421+
className={cn("size-5 shrink-0 text-text-dimmed", item.iconClassName)}
422+
/>
382423
{item.isFavorite ? (
383424
<>
384425
<Input
@@ -398,6 +439,16 @@ function ModalItemRow({
398439
)}
399440
</div>
400441
<div className="flex shrink-0 items-center gap-1">
442+
{item.isFavorite && (
443+
<button
444+
type="button"
445+
onClick={onRemove}
446+
aria-label={`Remove ${item.name}`}
447+
className="flex size-7 items-center justify-center rounded text-text-dimmed transition-colors hover:bg-error/10 hover:text-error focus-custom"
448+
>
449+
<CrossIcon className="size-4" />
450+
</button>
451+
)}
401452
<button
402453
type="button"
403454
onClick={onToggleHidden}

apps/webapp/app/components/navigation/FavoritesSection.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { EllipsisHorizontalIcon, PencilSquareIcon, TrashIcon } from "@heroicons/react/20/solid";
1+
import { EllipsisHorizontalIcon } from "@heroicons/react/20/solid";
22
import { useLocation, useNavigation } from "@remix-run/react";
33
import { useEffect, useRef, useState } from "react";
4+
import { CrossIcon } from "~/assets/icons/CrossIcon";
5+
import { RenameIcon } from "~/assets/icons/RenameIcon";
46
import { type FavoritePage } from "~/services/dashboardPreferences.server";
57
import { cn } from "~/utils/cn";
68
import { Icon, type RenderIcon } from "../primitives/Icon";
@@ -14,6 +16,7 @@ import {
1416
favoriteLinkTo,
1517
favoritePageActiveColor,
1618
favoritePageIcon,
19+
favoritePageIconClassName,
1720
isFavoriteActive,
1821
} from "./favoritePages";
1922
import { SideMenuItem } from "./SideMenuItem";
@@ -62,6 +65,7 @@ export function FavoriteMenuItem({
6265
<FavoriteRenameRow
6366
label={favorite.label}
6467
icon={icon}
68+
iconClassName={favoritePageIconClassName(favorite.icon)}
6569
onSubmit={submitRename}
6670
onCancel={() => setIsEditing(false)}
6771
/>
@@ -72,6 +76,7 @@ export function FavoriteMenuItem({
7276
<SideMenuItem
7377
name={favorite.label}
7478
icon={icon}
79+
iconClassName={favoritePageIconClassName(favorite.icon)}
7580
activeIconColor={favoritePageActiveColor(favorite.icon)}
7681
inactiveIconColor="text-text-dimmed"
7782
to={favoriteLinkTo(favorite)}
@@ -100,7 +105,7 @@ export function FavoriteMenuItem({
100105
>
101106
<div className="flex flex-col gap-1">
102107
<PopoverMenuItem
103-
icon={PencilSquareIcon}
108+
icon={RenameIcon}
104109
title="Rename"
105110
leadingIconClassName={SIDE_MENU_POPOVER_ITEM_ICON}
106111
className={SIDE_MENU_POPOVER_ITEM_LABEL}
@@ -110,7 +115,7 @@ export function FavoriteMenuItem({
110115
}}
111116
/>
112117
<PopoverMenuItem
113-
icon={TrashIcon}
118+
icon={CrossIcon}
114119
title="Remove"
115120
danger
116121
leadingIconClassName="h-5 w-5"
@@ -136,11 +141,13 @@ export function FavoriteMenuItem({
136141
function FavoriteRenameRow({
137142
label,
138143
icon,
144+
iconClassName,
139145
onSubmit,
140146
onCancel,
141147
}: {
142148
label: string;
143149
icon: RenderIcon;
150+
iconClassName?: string;
144151
onSubmit: (value: string) => void;
145152
onCancel: () => void;
146153
}) {
@@ -155,7 +162,7 @@ function FavoriteRenameRow({
155162

156163
return (
157164
<div className="flex h-8 w-full items-center gap-2 rounded bg-background-hover pl-1.75 pr-2">
158-
<Icon icon={icon} className="size-5 shrink-0 text-text-dimmed" />
165+
<Icon icon={icon} className={cn("size-5 shrink-0 text-text-dimmed", iconClassName)} />
159166
<input
160167
ref={inputRef}
161168
autoFocus

0 commit comments

Comments
 (0)