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>
103 lines
2.9 KiB
TypeScript
103 lines
2.9 KiB
TypeScript
import { useState, useCallback, useEffect } from 'react'
|
|
import { EmojiPicker } from './EmojiPicker'
|
|
import { isEmoji } from '../utils/emoji'
|
|
|
|
interface NoteIconProps {
|
|
icon: string | null
|
|
editable?: boolean
|
|
onSetIcon: (emoji: string) => void
|
|
onRemoveIcon: () => void
|
|
}
|
|
|
|
export function NoteIcon({ icon, editable = true, onSetIcon, onRemoveIcon }: NoteIconProps) {
|
|
const [pickerOpen, setPickerOpen] = useState(false)
|
|
const [showRemove, setShowRemove] = useState(false)
|
|
|
|
const hasIcon = icon && isEmoji(icon)
|
|
|
|
// Listen for command palette "Set Note Icon" event
|
|
useEffect(() => {
|
|
if (!editable) return
|
|
const handler = () => setPickerOpen(true)
|
|
window.addEventListener('laputa:open-icon-picker', handler)
|
|
return () => window.removeEventListener('laputa:open-icon-picker', handler)
|
|
}, [editable])
|
|
|
|
const openPicker = useCallback(() => {
|
|
if (!editable) return
|
|
if (hasIcon) {
|
|
setShowRemove(prev => !prev)
|
|
} else {
|
|
setPickerOpen(true)
|
|
}
|
|
}, [editable, hasIcon])
|
|
|
|
const handleSelect = useCallback((emoji: string) => {
|
|
onSetIcon(emoji)
|
|
setPickerOpen(false)
|
|
setShowRemove(false)
|
|
}, [onSetIcon])
|
|
|
|
const handleRemove = useCallback(() => {
|
|
onRemoveIcon()
|
|
setShowRemove(false)
|
|
}, [onRemoveIcon])
|
|
|
|
const handleChangePicker = useCallback(() => {
|
|
setShowRemove(false)
|
|
setPickerOpen(true)
|
|
}, [])
|
|
|
|
return (
|
|
<div className="note-icon-area" data-testid="note-icon-area" style={{ position: 'relative' }}>
|
|
{hasIcon ? (
|
|
<button
|
|
className="note-icon-button note-icon-button--active"
|
|
onClick={openPicker}
|
|
data-testid="note-icon-display"
|
|
title={editable ? 'Change or remove icon' : undefined}
|
|
disabled={!editable}
|
|
>
|
|
{icon}
|
|
</button>
|
|
) : (
|
|
editable && (
|
|
<button
|
|
className="note-icon-button note-icon-button--add"
|
|
onClick={openPicker}
|
|
data-testid="note-icon-add"
|
|
title="Add icon"
|
|
>
|
|
<span className="note-icon-button__plus">+</span>
|
|
<span className="note-icon-button__label">Add icon</span>
|
|
</button>
|
|
)
|
|
)}
|
|
{showRemove && hasIcon && (
|
|
<div className="note-icon-menu" data-testid="note-icon-menu">
|
|
<button
|
|
className="note-icon-menu__item"
|
|
onClick={handleChangePicker}
|
|
data-testid="note-icon-change"
|
|
>
|
|
Change icon
|
|
</button>
|
|
<button
|
|
className="note-icon-menu__item note-icon-menu__item--danger"
|
|
onClick={handleRemove}
|
|
data-testid="note-icon-remove"
|
|
>
|
|
Remove icon
|
|
</button>
|
|
</div>
|
|
)}
|
|
{pickerOpen && (
|
|
<EmojiPicker
|
|
onSelect={handleSelect}
|
|
onClose={() => setPickerOpen(false)}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|