feat: show note emoji icon in tab bar, breadcrumb, and pinned cards
Add emoji icon rendering before the note title in TabBar, BreadcrumbBar, and PinnedCard components to ensure consistent emoji display everywhere a note title appears. Sidebar, NoteItem, wikilinks, relationships, and backlinks already had emoji support from prior commits. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { memo } from 'react'
|
||||
import type { VaultEntry, NoteStatus } from '../types'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { isEmoji } from '../utils/emoji'
|
||||
import {
|
||||
MagnifyingGlass,
|
||||
GitBranch,
|
||||
@@ -179,7 +180,10 @@ export const BreadcrumbBar = memo(function BreadcrumbBar({
|
||||
<div className="flex items-center gap-1 min-w-0 whitespace-nowrap" style={{ fontSize: 12 }}>
|
||||
<span className="shrink-0 text-muted-foreground">{entry.isA || 'Note'}</span>
|
||||
<span className="shrink-0 text-muted-foreground" style={{ margin: '0 2px' }}>›</span>
|
||||
<span className="truncate font-medium text-foreground" style={{ maxWidth: '40vw' }}>{entry.title}</span>
|
||||
<span className="truncate font-medium text-foreground" style={{ maxWidth: '40vw' }}>
|
||||
{entry.icon && isEmoji(entry.icon) && <span className="mr-1">{entry.icon}</span>}
|
||||
{entry.title}
|
||||
</span>
|
||||
<span className="shrink-0 text-muted-foreground" style={{ margin: '0 4px' }}>·</span>
|
||||
<span className="shrink-0 text-muted-foreground">{wordCount.toLocaleString()} words</span>
|
||||
{noteStatus === 'pendingSave' && (
|
||||
|
||||
@@ -5,6 +5,7 @@ import { cn } from '@/lib/utils'
|
||||
import { X } from 'lucide-react'
|
||||
import { Plus, Columns, ArrowsOutSimple, ArrowLeft, ArrowRight } from '@phosphor-icons/react'
|
||||
import { computeTabMaxWidth } from '@/utils/tabLayout'
|
||||
import { isEmoji } from '../utils/emoji'
|
||||
|
||||
interface Tab {
|
||||
entry: VaultEntry
|
||||
@@ -242,6 +243,7 @@ function TabItem({ tab, isActive, isEditing, noteStatus, isDragging, showDropBef
|
||||
<InlineTabEdit initialValue={tab.entry.title} onSave={onRenameSave} onCancel={onRenameCancel} />
|
||||
) : (
|
||||
<span className="truncate" style={noteStatus === 'unsaved' ? { fontStyle: 'italic' } : undefined} onDoubleClick={(e) => { e.stopPropagation(); onDoubleClick() }}>
|
||||
{tab.entry.icon && isEmoji(tab.entry.icon) && <span className="mr-1">{tab.entry.icon}</span>}
|
||||
{tab.entry.title}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { VaultEntry } from '../../types'
|
||||
import { getTypeColor, getTypeLightColor } from '../../utils/typeColors'
|
||||
import { getTypeIcon } from '../NoteItem'
|
||||
import { relativeDate, getDisplayDate } from '../../utils/noteListHelpers'
|
||||
import { isEmoji } from '../../utils/emoji'
|
||||
|
||||
export function PinnedCard({ entry, typeEntryMap, onClickNote, showDate }: {
|
||||
entry: VaultEntry
|
||||
@@ -17,7 +18,10 @@ export function PinnedCard({ entry, typeEntryMap, onClickNote, showDate }: {
|
||||
<div className="relative cursor-pointer border-b border-[var(--border)]" style={{ backgroundColor: bgColor, padding: '14px 16px' }} onClick={(e: React.MouseEvent) => onClickNote(entry, e)}>
|
||||
{/* eslint-disable-next-line react-hooks/static-components */}
|
||||
<Icon width={16} height={16} className="absolute right-3 top-3.5" style={{ color }} data-testid="type-icon" />
|
||||
<div className="pr-6 text-[14px] font-bold" style={{ color }}>{entry.title}</div>
|
||||
<div className="pr-6 text-[14px] font-bold" style={{ color }}>
|
||||
{entry.icon && isEmoji(entry.icon) && <span className="mr-1">{entry.icon}</span>}
|
||||
{entry.title}
|
||||
</div>
|
||||
<div className="mt-1 text-[12px] leading-[1.5] opacity-80" style={{ color, display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>{entry.snippet}</div>
|
||||
{showDate && <div className="mt-1 text-[11px] opacity-60" style={{ color }}>{relativeDate(getDisplayDate(entry))}</div>}
|
||||
</div>
|
||||
|
||||
65
tests/smoke/emoji-icon-everywhere.spec.ts
Normal file
65
tests/smoke/emoji-icon-everywhere.spec.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('Emoji icon shown everywhere title appears', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForLoadState('networkidle')
|
||||
await page.waitForTimeout(2500)
|
||||
})
|
||||
|
||||
test('emoji icon appears in tab bar, breadcrumb, and note list after setting it', async ({ page }) => {
|
||||
// Open a note
|
||||
const noteItem = page.locator('[data-testid="note-list-container"] .cursor-pointer').first()
|
||||
await noteItem.waitFor({ timeout: 5000 })
|
||||
await noteItem.click()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Remove any existing icon first for a clean state
|
||||
const iconDisplay = page.locator('[data-testid="note-icon-display"]')
|
||||
if (await iconDisplay.isVisible()) {
|
||||
await iconDisplay.click()
|
||||
await page.locator('[data-testid="note-icon-remove"]').click()
|
||||
await page.waitForTimeout(300)
|
||||
}
|
||||
|
||||
// Set an emoji icon
|
||||
const iconArea = page.locator('[data-testid="note-icon-area"]')
|
||||
await iconArea.hover()
|
||||
await page.locator('[data-testid="note-icon-add"]').click()
|
||||
const firstEmoji = page.locator('[data-testid="emoji-option"]').first()
|
||||
const emojiText = await firstEmoji.textContent()
|
||||
expect(emojiText).toBeTruthy()
|
||||
await firstEmoji.click()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Verify emoji in note list item
|
||||
const noteListText = await noteItem.textContent()
|
||||
expect(noteListText).toContain(emojiText!)
|
||||
|
||||
// Verify emoji in the tab (active tab has the truncate span with title)
|
||||
const tabArea = page.locator('.group .truncate').first()
|
||||
const tabText = await tabArea.textContent()
|
||||
expect(tabText).toContain(emojiText!)
|
||||
})
|
||||
|
||||
test('note without emoji shows no emoji span in tab or note list', async ({ page }) => {
|
||||
// Open a note
|
||||
const noteItem = page.locator('[data-testid="note-list-container"] .cursor-pointer').first()
|
||||
await noteItem.waitFor({ timeout: 5000 })
|
||||
await noteItem.click()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Remove icon if present
|
||||
const iconDisplay = page.locator('[data-testid="note-icon-display"]')
|
||||
if (await iconDisplay.isVisible()) {
|
||||
await iconDisplay.click()
|
||||
await page.locator('[data-testid="note-icon-remove"]').click()
|
||||
await page.waitForTimeout(300)
|
||||
}
|
||||
|
||||
// The note list item title row should not contain an emoji span (mr-1)
|
||||
const titleRow = noteItem.locator('.truncate.text-foreground').first()
|
||||
const emojiSpans = titleRow.locator('.mr-1')
|
||||
await expect(emojiSpans).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user