From 2ca49b85264ecbda7a28ddd6d709501a3f22d96d Mon Sep 17 00:00:00 2001 From: Test Date: Wed, 18 Mar 2026 22:31:04 +0100 Subject: [PATCH] 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) --- src/components/BreadcrumbBar.tsx | 6 ++- src/components/TabBar.tsx | 2 + src/components/note-list/PinnedCard.tsx | 6 ++- tests/smoke/emoji-icon-everywhere.spec.ts | 65 +++++++++++++++++++++++ 4 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 tests/smoke/emoji-icon-everywhere.spec.ts diff --git a/src/components/BreadcrumbBar.tsx b/src/components/BreadcrumbBar.tsx index ee3af859..af15c0d5 100644 --- a/src/components/BreadcrumbBar.tsx +++ b/src/components/BreadcrumbBar.tsx @@ -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({
{entry.isA || 'Note'} - {entry.title} + + {entry.icon && isEmoji(entry.icon) && {entry.icon}} + {entry.title} + · {wordCount.toLocaleString()} words {noteStatus === 'pendingSave' && ( diff --git a/src/components/TabBar.tsx b/src/components/TabBar.tsx index 89fcd8b1..cf61bf11 100644 --- a/src/components/TabBar.tsx +++ b/src/components/TabBar.tsx @@ -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 ) : ( { e.stopPropagation(); onDoubleClick() }}> + {tab.entry.icon && isEmoji(tab.entry.icon) && {tab.entry.icon}} {tab.entry.title} )} diff --git a/src/components/note-list/PinnedCard.tsx b/src/components/note-list/PinnedCard.tsx index db9bc09e..a7971fea 100644 --- a/src/components/note-list/PinnedCard.tsx +++ b/src/components/note-list/PinnedCard.tsx @@ -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 }: {
onClickNote(entry, e)}> {/* eslint-disable-next-line react-hooks/static-components */} -
{entry.title}
+
+ {entry.icon && isEmoji(entry.icon) && {entry.icon}} + {entry.title} +
{entry.snippet}
{showDate &&
{relativeDate(getDisplayDate(entry))}
}
diff --git a/tests/smoke/emoji-icon-everywhere.spec.ts b/tests/smoke/emoji-icon-everywhere.spec.ts new file mode 100644 index 00000000..746247e6 --- /dev/null +++ b/tests/smoke/emoji-icon-everywhere.spec.ts @@ -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) + }) +})