@@ -3,6 +3,7 @@ import { ArrowDownIcon, ArrowUpIcon, EyeIcon, EyeSlashIcon } from "@heroicons/re
33import { GripVerticalIcon } from "lucide-react" ;
44import { useState } from "react" ;
55import ReactGridLayout , { type Layout , useContainerWidth } from "react-grid-layout" ;
6+ import { CrossIcon } from "~/assets/icons/CrossIcon" ;
67import { cn } from "~/utils/cn" ;
78import { Button } from "../primitives/Buttons" ;
89import { 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
4548type 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
5560const 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
90101function 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 }
0 commit comments