diff --git a/src/components/InspectorPanels.test.tsx b/src/components/InspectorPanels.test.tsx
index 55036414..4cf5ceb2 100644
--- a/src/components/InspectorPanels.test.tsx
+++ b/src/components/InspectorPanels.test.tsx
@@ -242,6 +242,36 @@ describe('DynamicRelationshipsPanel', () => {
expect(screen.getByText('My Cool Project')).toBeInTheDocument()
})
+ it('shows emoji icon before label when entry has an emoji', () => {
+ const emojiEntry = makeEntry({
+ path: '/vault/note/rocket.md', filename: 'rocket.md', title: 'Rocket Note', isA: 'Note', icon: '🚀',
+ })
+ render(
+
+ )
+ expect(screen.getByText('🚀')).toBeInTheDocument()
+ expect(screen.getByText('Rocket Note')).toBeInTheDocument()
+ })
+
+ it('does not show emoji when entry has no icon', () => {
+ render(
+
+ )
+ const link = screen.getByText('My Project')
+ const container = link.closest('.group\\/link')
+ expect(container?.textContent).not.toMatch(/^[\p{Emoji_Presentation}]/u)
+ })
+
describe('relation editing', () => {
const onUpdateProperty = vi.fn()
const onDeleteProperty = vi.fn()
@@ -649,6 +679,29 @@ describe('BacklinksPanel', () => {
fireEvent.click(screen.getByTestId('backlinks-toggle'))
expect(screen.queryByText('Note A')).not.toBeInTheDocument()
})
+
+ it('shows emoji icon before backlink title when entry has an emoji', () => {
+ const backlinks = [{
+ entry: makeEntry({ title: 'Starred Note', icon: '⭐' }),
+ context: null,
+ }]
+ render()
+ fireEvent.click(screen.getByTestId('backlinks-toggle'))
+ expect(screen.getByText('⭐')).toBeInTheDocument()
+ expect(screen.getByText('Starred Note')).toBeInTheDocument()
+ })
+
+ it('does not show emoji when backlink entry has no icon', () => {
+ const backlinks = [{ entry: makeEntry({ title: 'Plain Note' }), context: null }]
+ render()
+ fireEvent.click(screen.getByTestId('backlinks-toggle'))
+ expect(screen.getByText('Plain Note')).toBeInTheDocument()
+ const btn = screen.getByText('Plain Note').closest('button')
+ const spans = btn?.querySelectorAll('span.shrink-0')
+ // No emoji span should exist (only possible shrink-0 spans are trash icon, not emoji)
+ const emojiSpans = Array.from(spans ?? []).filter(s => /[\p{Emoji_Presentation}]/u.test(s.textContent ?? ''))
+ expect(emojiSpans).toHaveLength(0)
+ })
})
describe('ReferencedByPanel', () => {
diff --git a/src/components/editorSchema.tsx b/src/components/editorSchema.tsx
index 77d8bcf0..bc01e8c6 100644
--- a/src/components/editorSchema.tsx
+++ b/src/components/editorSchema.tsx
@@ -4,6 +4,7 @@ import { createReactInlineContentSpec } from '@blocknote/react'
import { resolveWikilinkColor as resolveColor } from '../utils/wikilinkColors'
import { resolveEntry } from '../utils/wikilink'
import type { VaultEntry } from '../types'
+import { isEmoji } from '../utils/emoji'
// Module-level cache so the WikiLink renderer (defined outside React) can access entries
export const _wikilinkEntriesRef: { current: VaultEntry[] } = { current: [] }
@@ -12,15 +13,22 @@ function resolveWikilinkColor(target: string) {
return resolveColor(_wikilinkEntriesRef.current, target)
}
-/** Resolve the display text for a wikilink target.
+/** Resolve the display text and optional emoji for a wikilink target.
* Priority: pipe display text → entry title → humanised path stem */
-function resolveDisplayText(target: string): string {
+function resolveDisplayInfo(target: string): { text: string; emoji: string | null } {
const pipeIdx = target.indexOf('|')
- if (pipeIdx !== -1) return target.slice(pipeIdx + 1)
+ if (pipeIdx !== -1) {
+ const entry = resolveEntry(_wikilinkEntriesRef.current, target.slice(0, pipeIdx))
+ const emoji = entry?.icon && isEmoji(entry.icon) ? entry.icon : null
+ return { text: target.slice(pipeIdx + 1), emoji }
+ }
const entry = resolveEntry(_wikilinkEntriesRef.current, target)
- if (entry) return entry.title
+ if (entry) {
+ const emoji = entry.icon && isEmoji(entry.icon) ? entry.icon : null
+ return { text: entry.title, emoji }
+ }
const last = target.split('/').pop() ?? target
- return last.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
+ return { text: last.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()), emoji: null }
}
export const WikiLink = createReactInlineContentSpec(
@@ -35,14 +43,15 @@ export const WikiLink = createReactInlineContentSpec(
render: (props) => {
const target = props.inlineContent.props.target
const { color, isBroken } = resolveWikilinkColor(target)
- const displayText = resolveDisplayText(target)
+ const { text, emoji } = resolveDisplayInfo(target)
return (
- {displayText}
+ {emoji && {emoji}{' '}}
+ {text}
)
},
diff --git a/src/components/inspector/BacklinksPanel.tsx b/src/components/inspector/BacklinksPanel.tsx
index 147b6818..cdb397d0 100644
--- a/src/components/inspector/BacklinksPanel.tsx
+++ b/src/components/inspector/BacklinksPanel.tsx
@@ -2,6 +2,7 @@ import { useState } from 'react'
import type { VaultEntry } from '../../types'
import { CaretRight, Trash } from '@phosphor-icons/react'
import { getTypeColor } from '../../utils/typeColors'
+import { isEmoji } from '../../utils/emoji'
import { entryStatusTitle } from './shared'
import { StatusSuffix } from './LinkButton'
@@ -29,6 +30,7 @@ function BacklinkEntry({ entry, context, typeEntryMap, onNavigate }: {
style={{ color: isDimmed ? 'var(--muted-foreground)' : getTypeColor(entry.isA, te?.color) }}
>
{entry.trashed && }
+ {entry.icon && isEmoji(entry.icon) && {entry.icon}}
{entry.title}
diff --git a/src/components/inspector/LinkButton.tsx b/src/components/inspector/LinkButton.tsx
index 6ccc7df4..95311f0a 100644
--- a/src/components/inspector/LinkButton.tsx
+++ b/src/components/inspector/LinkButton.tsx
@@ -7,8 +7,9 @@ export function StatusSuffix({ isArchived, isTrashed }: { isArchived: boolean; i
return null
}
-export function LinkButton({ label, typeColor, bgColor, isArchived, isTrashed, onClick, onRemove, title, TypeIcon }: {
+export function LinkButton({ label, emoji, typeColor, bgColor, isArchived, isTrashed, onClick, onRemove, title, TypeIcon }: {
label: string
+ emoji?: string | null
typeColor: string
bgColor?: string
isArchived: boolean
@@ -33,6 +34,7 @@ export function LinkButton({ label, typeColor, bgColor, isArchived, isTrashed, o
>
{isTrashed && }
+ {emoji && {emoji}}
{label}
diff --git a/src/components/inspector/shared.ts b/src/components/inspector/shared.ts
index a83f07e6..ace1ac9e 100644
--- a/src/components/inspector/shared.ts
+++ b/src/components/inspector/shared.ts
@@ -3,6 +3,7 @@ import type { VaultEntry } from '../../types'
import { getTypeColor, getTypeLightColor } from '../../utils/typeColors'
import { getTypeIcon } from '../NoteItem'
import { findEntryByTarget } from '../../utils/wikilinkColors'
+import { isEmoji } from '../../utils/emoji'
export function isWikilink(value: string): boolean {
return /^\[\[.*\]\]$/.test(value)
@@ -30,8 +31,10 @@ export function resolveRefProps(ref: string, entries: VaultEntry[], typeEntryMap
const resolved = resolveRef(ref, entries)
const refType = resolved?.isA ?? null
const te = typeEntryMap[refType ?? '']
+ const icon = resolved?.icon
return {
label: wikilinkDisplay(ref),
+ emoji: icon && isEmoji(icon) ? icon : null,
typeColor: getTypeColor(refType, te?.color),
bgColor: getTypeLightColor(refType, te?.color),
isArchived: resolved?.archived ?? false,