feat: add emoji icon picker for notes stored in frontmatter
Every note can now have an optional emoji icon (frontmatter `icon` field). The icon is displayed in the editor header, note list, search results, and Quick Open. Includes command palette commands "Set Note Icon" and "Remove Note Icon", plus full test coverage (Vitest + Playwright). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
39
src/utils/__tests__/emoji.test.ts
Normal file
39
src/utils/__tests__/emoji.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { isEmoji } from '../emoji'
|
||||
|
||||
describe('isEmoji', () => {
|
||||
it('returns true for common emoji', () => {
|
||||
expect(isEmoji('🎯')).toBe(true)
|
||||
expect(isEmoji('🔥')).toBe(true)
|
||||
expect(isEmoji('🚀')).toBe(true)
|
||||
expect(isEmoji('❤️')).toBe(true)
|
||||
expect(isEmoji('✨')).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false for Phosphor icon names', () => {
|
||||
expect(isEmoji('cooking-pot')).toBe(false)
|
||||
expect(isEmoji('file-text')).toBe(false)
|
||||
expect(isEmoji('rocket')).toBe(false)
|
||||
expect(isEmoji('star')).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false for empty string', () => {
|
||||
expect(isEmoji('')).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false for regular text', () => {
|
||||
expect(isEmoji('hello')).toBe(false)
|
||||
expect(isEmoji('ABC')).toBe(false)
|
||||
expect(isEmoji('123')).toBe(false)
|
||||
})
|
||||
|
||||
it('handles compound emoji (ZWJ sequences)', () => {
|
||||
expect(isEmoji('👨💻')).toBe(true)
|
||||
expect(isEmoji('🧑🔬')).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false for multi-emoji strings', () => {
|
||||
expect(isEmoji('🔥🚀')).toBe(false)
|
||||
expect(isEmoji('hi 🎯')).toBe(false)
|
||||
})
|
||||
})
|
||||
53
src/utils/emoji.ts
Normal file
53
src/utils/emoji.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Detects whether a string is a single emoji (as opposed to a Phosphor icon name).
|
||||
* Used to differentiate emoji note icons from kebab-case Phosphor icon names.
|
||||
*/
|
||||
export function isEmoji(value: string): boolean {
|
||||
if (!value) return false
|
||||
// Phosphor icon names are always lowercase ASCII with hyphens
|
||||
if (/^[a-z][a-z0-9-]*$/.test(value)) return false
|
||||
// Match a single emoji (including compound emoji with ZWJ, skin tones, variation selectors, flags)
|
||||
// Uses Unicode segmentation: a single emoji can be base + modifiers/ZWJ sequences
|
||||
const emojiRegex = /^(\p{Emoji_Presentation}|\p{Emoji}\ufe0f)(\u200d(\p{Emoji_Presentation}|\p{Emoji}\ufe0f)|\p{Emoji_Modifier})*$/u
|
||||
return emojiRegex.test(value)
|
||||
}
|
||||
|
||||
/** Curated emoji categories for the note icon picker. */
|
||||
export const EMOJI_CATEGORIES: { name: string; emojis: string[] }[] = [
|
||||
{
|
||||
name: 'Smileys',
|
||||
emojis: ['😀', '😃', '😄', '😁', '😆', '🥹', '😅', '🤣', '😂', '🙂', '😉', '😊', '😇', '🥰', '😍', '🤩', '😘', '😎', '🤓', '🧐', '🤔', '🤗', '🫡', '🤫', '🫠', '😶', '😑', '😬', '🙄', '😴'],
|
||||
},
|
||||
{
|
||||
name: 'Hands & People',
|
||||
emojis: ['👋', '🤚', '✋', '🖐️', '👌', '🤌', '✌️', '🤞', '🫰', '🤙', '👍', '👎', '👏', '🙌', '🫶', '🙏', '💪', '🧠', '👀', '👁️', '👤', '🧑💻', '🧑🎨', '🧑🔬', '🧑🚀', '🧑🏫', '🧑⚕️', '🧑🍳', '🏃', '🧘'],
|
||||
},
|
||||
{
|
||||
name: 'Nature',
|
||||
emojis: ['🌱', '🌿', '🍀', '🌵', '🌲', '🌳', '🌴', '🌸', '🌺', '🌻', '🌹', '💐', '🍂', '🍁', '🍃', '🌾', '🐝', '🦋', '🐛', '🐞', '🐦', '🦅', '🐺', '🦊', '🐻', '🐼', '🐨', '🐯', '🦁', '🐸'],
|
||||
},
|
||||
{
|
||||
name: 'Food & Drink',
|
||||
emojis: ['🍎', '🍐', '🍊', '🍋', '🍌', '🍉', '🍇', '🍓', '🫐', '🍒', '🍑', '🥝', '🍅', '🥑', '🌽', '🥕', '🧅', '🍞', '🥐', '🧁', '🍰', '🎂', '🍪', '☕', '🍵', '🧃', '🥤', '🍺', '🍷', '🥂'],
|
||||
},
|
||||
{
|
||||
name: 'Activities',
|
||||
emojis: ['⚽', '🏀', '🏈', '⚾', '🎾', '🏐', '🎱', '🏓', '🎮', '🕹️', '🎲', '🧩', '♟️', '🎯', '🎳', '🎸', '🎹', '🎺', '🎨', '🖌️', '📷', '🎬', '🎭', '🎤', '🎧', '📚', '📝', '✏️', '🖊️', '📖'],
|
||||
},
|
||||
{
|
||||
name: 'Travel & Places',
|
||||
emojis: ['🏠', '🏡', '🏢', '🏗️', '🏰', '🏛️', '⛪', '🕌', '🗼', '🗽', '⛲', '🎪', '🚀', '✈️', '🚂', '🚗', '🚲', '⛵', '🏔️', '🌋', '🏖️', '🏜️', '🗺️', '🌍', '🌎', '🌏', '🧭', '⛺', '🎡', '🎢'],
|
||||
},
|
||||
{
|
||||
name: 'Objects',
|
||||
emojis: ['💡', '🔦', '🕯️', '📱', '💻', '⌨️', '🖥️', '🖨️', '📸', '🔭', '🔬', '🧪', '💊', '🩺', '🔑', '🗝️', '🔒', '🔓', '🧲', '⚙️', '🔧', '🔨', '⛏️', '🪛', '🧰', '📦', '📮', '✉️', '📩', '🏷️'],
|
||||
},
|
||||
{
|
||||
name: 'Symbols',
|
||||
emojis: ['❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '💔', '❣️', '💕', '💝', '⭐', '🌟', '✨', '💫', '🔥', '💥', '🎉', '🎊', '🏆', '🥇', '🥈', '🥉', '🏅', '🎖️', '📌', '📍', '🚩', '🏁'],
|
||||
},
|
||||
{
|
||||
name: 'Flags & Signs',
|
||||
emojis: ['✅', '❌', '⭕', '❓', '❗', '💯', '🔴', '🟠', '🟡', '🟢', '🔵', '🟣', '⚫', '⚪', '🟤', '🔶', '🔷', '🔸', '🔹', '▶️', '⏸️', '⏹️', '⏺️', '⏭️', '🔀', '🔁', '🔂', '🔄', '➡️', '⬅️'],
|
||||
},
|
||||
]
|
||||
Reference in New Issue
Block a user