Skip to content

Commit 069e11d

Browse files
Merge pull request #161 from laststance/ui/compact-bookmark-remove
Compact dashboard bookmark remove button
2 parents 165f487 + 9ec76a5 commit 069e11d

2 files changed

Lines changed: 82 additions & 7 deletions

File tree

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { configureStore } from '@reduxjs/toolkit'
2+
import { Provider } from 'react-redux'
3+
import { describe, expect, it } from 'vitest'
4+
import { render } from 'vitest-browser-react'
5+
6+
import '@/renderer/src/styles/globals.css'
7+
import type { HttpUrl, RepositoryId, SkillName } from '@/shared/types'
8+
9+
/**
10+
* Render the real BookmarksWidget reducer with one bookmark.
11+
* @param name - Skill name shown in the widget row.
12+
* @returns Render screen plus the backing Redux store.
13+
*/
14+
async function renderBookmarksWidget(
15+
name: SkillName = 'very-long-skill-name' as SkillName,
16+
) {
17+
const [{ default: bookmarkReducer, addBookmark }, { BookmarksWidget }] =
18+
await Promise.all([
19+
import('@/renderer/src/redux/slices/bookmarkSlice'),
20+
import('./BookmarksWidget'),
21+
])
22+
const store = configureStore({
23+
reducer: {
24+
bookmarks: bookmarkReducer,
25+
},
26+
})
27+
28+
store.dispatch(
29+
addBookmark({
30+
name,
31+
repo: 'laststance/skills' as RepositoryId,
32+
url: 'https://skills.sh/very-long-skill-name' as HttpUrl,
33+
}),
34+
)
35+
36+
const screen = await render(
37+
<Provider store={store}>
38+
<div style={{ width: 160 }}>
39+
<BookmarksWidget />
40+
</div>
41+
</Provider>,
42+
)
43+
44+
return { screen, store }
45+
}
46+
47+
describe('BookmarksWidget', () => {
48+
it('keeps long bookmark text reachable alongside the remove control', async () => {
49+
const { screen } = await renderBookmarksWidget()
50+
51+
await expect.element(screen.getByText('very-long-skill-name')).toBeVisible()
52+
await expect.element(screen.getByText('laststance/skills')).toBeVisible()
53+
54+
const removeButton = screen
55+
.getByRole('button', { name: /Remove bookmark very-long-skill-name/i })
56+
.element() as HTMLButtonElement
57+
58+
expect(removeButton.title).toBe('Remove very-long-skill-name')
59+
})
60+
61+
it('still removes the bookmark through the compact control', async () => {
62+
const { screen, store } = await renderBookmarksWidget('task' as SkillName)
63+
const removeButton = screen
64+
.getByRole('button', { name: /Remove bookmark task/i })
65+
.element() as HTMLButtonElement
66+
67+
removeButton.click()
68+
69+
expect(store.getState().bookmarks.items).toEqual([])
70+
})
71+
})

src/renderer/src/components/dashboard/widgets/BookmarksWidget.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@ const BookmarkRow = React.memo(function BookmarkRow({
2323
onRemove,
2424
}: BookmarkRowProps): React.ReactElement {
2525
return (
26-
<li className="group flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-muted">
26+
<li className="group relative flex items-center gap-2 rounded-md px-2 py-1.5 pr-1 hover:bg-muted">
2727
<Bookmark className="h-3 w-3 shrink-0 text-primary" aria-hidden="true" />
2828
<a
2929
href={bookmark.url}
3030
target="_blank"
3131
rel="noopener noreferrer"
3232
className="
33-
flex-1 min-w-0 inline-flex items-center gap-1 text-xs
33+
flex-1 min-w-0 inline-flex items-center gap-1 pr-7 text-xs
3434
text-foreground hover:text-primary focus-visible:outline-none
3535
focus-visible:ring-2 focus-visible:ring-ring rounded
3636
"
@@ -50,13 +50,17 @@ const BookmarkRow = React.memo(function BookmarkRow({
5050
type="button"
5151
onClick={() => onRemove(bookmark.name)}
5252
aria-label={`Remove bookmark ${bookmark.name}`}
53-
// 44×44 hit area per HIG — the visible X stays small so it doesn't
54-
// dominate the row; opacity-0 until hover mirrors the SkillItem pattern.
53+
title={`Remove ${bookmark.name}`}
54+
data-bookmark-remove-button
55+
// Absolute placement keeps the destructive action out of row layout,
56+
// reserving 32px (28px button + 4px offset) on the right edge.
5557
className="
56-
min-h-11 min-w-11 flex items-center justify-center
57-
rounded-md text-muted-foreground hover:text-destructive hover:bg-destructive/10
58+
absolute right-1 top-1/2 z-10 flex size-7 -translate-y-1/2 items-center justify-center
59+
rounded-md text-muted-foreground hover:bg-destructive/10 hover:text-destructive
5860
opacity-0 group-hover:opacity-100 focus-visible:opacity-100
59-
transition-opacity
61+
transition-[opacity,background-color,color]
62+
after:pointer-events-none after:absolute after:-inset-1.5 after:content-['']
63+
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring
6064
"
6165
>
6266
<X className="h-3 w-3" aria-hidden="true" />

0 commit comments

Comments
 (0)