feat: design system overhaul — sidebar, AI chats, settings, brainstorm, color cleanup
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
- Sidebar: dynamic brand-accent colors, brainstorm section restyled - AI chat general: popup panel with expand/collapse, hides when contextual AI open - AI chat contextual: tabs reordered (Actions first), X close button, height fix - Settings: all tabs restyled, 6 new color presets (sage, terracotta, iron, etc.) - Global color cleanup: emerald/orange hardcoded → brand-accent dynamic - Brainstorm page: orange → brand-accent throughout - PageEntry animation component added to key pages - Floating AI button: bg-brand-accent instead of hardcoded black - i18n: all 15 locales updated with new AI/billing keys - Billing: freemium quota tracking, BYOK, stripe subscription scaffolding - Admin: integrated into new design - AGENTS.md + CLAUDE.md project rules added
This commit is contained in:
@@ -59,6 +59,26 @@ type SlashItem = {
|
||||
command: (editor: Editor, range?: any) => void
|
||||
}
|
||||
|
||||
type SlashCategoryId = 'basic' | 'media' | 'formatting' | 'ai'
|
||||
|
||||
type SlashMenuItem = SlashItem & { categoryId: SlashCategoryId }
|
||||
|
||||
const ORDERED_SLASH_CATEGORIES: SlashCategoryId[] = ['basic', 'media', 'formatting', 'ai']
|
||||
|
||||
function slashCategoryLabel(id: SlashCategoryId, t: (key: string) => string): string {
|
||||
switch (id) {
|
||||
case 'basic': return t('richTextEditor.slashCatBasic')
|
||||
case 'media': return t('richTextEditor.slashCatMedia')
|
||||
case 'formatting': return t('richTextEditor.slashCatFormatting')
|
||||
case 'ai': return t('richTextEditor.slashCatAi')
|
||||
}
|
||||
}
|
||||
|
||||
/** Sent to /api/ai/reformulate as target language (unchanged for prompt compatibility). */
|
||||
const TRANSLATE_TARGET_API_VALUES = ['Francais', 'English', 'Espanol', 'Deutsch', 'Persan', 'Portugais', 'Italiano', 'Chinois', 'Japonais'] as const
|
||||
|
||||
const AI_REFORMULATE_FALLBACK = '__RICH_TEXT_AI_FALLBACK__'
|
||||
|
||||
const CustomImage = Image.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
@@ -129,7 +149,10 @@ async function aiReformulate(text: string, option: string, language?: string): P
|
||||
body: JSON.stringify({ text, option, format: 'html', language }),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (!res.ok) throw new Error(data.error || 'AI failed')
|
||||
if (!res.ok) {
|
||||
const serverMsg = typeof data?.error === 'string' && data.error.trim() ? data.error.trim() : AI_REFORMULATE_FALLBACK
|
||||
throw new Error(serverMsg)
|
||||
}
|
||||
return data.reformulatedText || data.text || text
|
||||
}
|
||||
|
||||
@@ -286,7 +309,7 @@ function ImageModal({ onConfirm, onCancel }: { onConfirm: (url: string) => void;
|
||||
value={url}
|
||||
onChange={(e) => { setUrl(e.target.value); setError(''); setPreview(null) }}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') handleConfirm(); if (e.key === 'Escape') onCancel() }}
|
||||
placeholder="https://example.com/image.png"
|
||||
placeholder={t('richTextEditor.imageUrlPlaceholder')}
|
||||
className="notion-modal-input"
|
||||
/>
|
||||
{url.trim() && !preview && (
|
||||
@@ -309,10 +332,14 @@ function ImageModal({ onConfirm, onCancel }: { onConfirm: (url: string) => void;
|
||||
)
|
||||
}
|
||||
|
||||
const AI_LANGS = ['Francais', 'English', 'Espanol', 'Deutsch', 'Persan', 'Portugais', 'Italiano', 'Chinois', 'Japonais']
|
||||
|
||||
function BubbleToolbar({ editor }: { editor: Editor | null }) {
|
||||
const { t, language } = useLanguage()
|
||||
|
||||
const toastAiError = (err: unknown) => {
|
||||
const msg = err instanceof Error ? err.message : ''
|
||||
toast.error(msg && msg !== AI_REFORMULATE_FALLBACK ? msg : t('richTextEditor.aiReformulateFailed'))
|
||||
}
|
||||
|
||||
const [, setTick] = useState(0)
|
||||
const [aiOpen, setAiOpen] = useState(false)
|
||||
const [aiLoading, setAiLoading] = useState(false)
|
||||
@@ -365,6 +392,7 @@ function BubbleToolbar({ editor }: { editor: Editor | null }) {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('AI error:', err)
|
||||
toastAiError(err)
|
||||
} finally {
|
||||
setAiLoading(false)
|
||||
}
|
||||
@@ -444,8 +472,10 @@ function BubbleToolbar({ editor }: { editor: Editor | null }) {
|
||||
<button className="notion-ai-subitem" onClick={() => setTranslateOpen(v => !v)}><Languages className="w-3.5 h-3.5 text-indigo-500" /><span>{t('ai.action.translate')}</span></button>
|
||||
{translateOpen && (
|
||||
<div className="notion-ai-lang-picker">
|
||||
{AI_LANGS.map(l => (
|
||||
<button key={l} className="notion-ai-lang-item" onClick={() => handleAI('translate', l)}>{l}</button>
|
||||
{TRANSLATE_TARGET_API_VALUES.map((apiValue) => (
|
||||
<button key={apiValue} className="notion-ai-lang-item" onClick={() => handleAI('translate', apiValue)}>
|
||||
{t(`richTextEditor.translateTargets.${apiValue}`)}
|
||||
</button>
|
||||
))}
|
||||
<div className="flex items-center gap-1 px-1 pt-1 w-full border-t border-border/40 mt-1">
|
||||
<input
|
||||
@@ -498,49 +528,42 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [query, setQuery] = useState('')
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>(null)
|
||||
const [activeCategory, setActiveCategory] = useState<SlashCategoryId | null>(null)
|
||||
const [coords, setCoords] = useState({ top: 0, left: 0 })
|
||||
const [aiLoading, setAiLoading] = useState(false)
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
const selectedItemRef = useRef<HTMLButtonElement>(null)
|
||||
const menuInteracting = useRef(false)
|
||||
|
||||
const CAT_LABELS: Record<string, string> = {
|
||||
'Basic blocks': t('richTextEditor.slashCatBasic'),
|
||||
'Media': t('richTextEditor.slashCatMedia'),
|
||||
'Formatting': t('richTextEditor.slashCatFormatting'),
|
||||
'IA Note': t('richTextEditor.slashCatAi'),
|
||||
}
|
||||
|
||||
const localCommands: SlashItem[] = [
|
||||
{ ...slashCommands[0], title: t('richTextEditor.slashText'), description: t('richTextEditor.slashTextDesc'), category: t('richTextEditor.slashCatBasic') },
|
||||
{ ...slashCommands[1], title: t('richTextEditor.slashH1'), description: t('richTextEditor.slashH1Desc'), category: t('richTextEditor.slashCatBasic') },
|
||||
{ ...slashCommands[2], title: t('richTextEditor.slashH2'), description: t('richTextEditor.slashH2Desc'), category: t('richTextEditor.slashCatBasic') },
|
||||
{ ...slashCommands[3], title: t('richTextEditor.slashH3'), description: t('richTextEditor.slashH3Desc'), category: t('richTextEditor.slashCatBasic') },
|
||||
{ ...slashCommands[4], title: t('richTextEditor.slashTable'), description: t('richTextEditor.slashTableDesc'), category: t('richTextEditor.slashCatBasic') },
|
||||
{ ...slashCommands[5], title: t('richTextEditor.slashBullet'), description: t('richTextEditor.slashBulletDesc'), category: t('richTextEditor.slashCatBasic') },
|
||||
{ ...slashCommands[6], title: t('richTextEditor.slashNumbered'), description: t('richTextEditor.slashNumberedDesc'), category: t('richTextEditor.slashCatBasic') },
|
||||
{ ...slashCommands[7], title: t('richTextEditor.slashTodo'), description: t('richTextEditor.slashTodoDesc'), category: t('richTextEditor.slashCatBasic') },
|
||||
{ ...slashCommands[8], title: t('richTextEditor.slashQuote'), description: t('richTextEditor.slashQuoteDesc'), category: t('richTextEditor.slashCatBasic') },
|
||||
{ ...slashCommands[9], title: t('richTextEditor.slashCode'), description: t('richTextEditor.slashCodeDesc'), category: t('richTextEditor.slashCatBasic') },
|
||||
{ ...slashCommands[10], title: t('richTextEditor.slashDivider'), description: t('richTextEditor.slashDividerDesc'), category: t('richTextEditor.slashCatBasic') },
|
||||
{ ...slashCommands[11], title: t('richTextEditor.slashImage'), description: t('richTextEditor.slashImageDesc'), category: t('richTextEditor.slashCatMedia') },
|
||||
{ ...slashCommands[12], title: t('richTextEditor.slashAlignLeft'), description: t('richTextEditor.slashAlignLeftDesc'), category: t('richTextEditor.slashCatFormatting') },
|
||||
{ ...slashCommands[13], title: t('richTextEditor.slashAlignCenter'), description: t('richTextEditor.slashAlignCenterDesc'), category: t('richTextEditor.slashCatFormatting') },
|
||||
{ ...slashCommands[14], title: t('richTextEditor.slashAlignRight'), description: t('richTextEditor.slashAlignRightDesc'), category: t('richTextEditor.slashCatFormatting') },
|
||||
{ ...slashCommands[15], title: t('richTextEditor.slashClarify'), description: t('richTextEditor.slashClarifyDesc'), category: t('richTextEditor.slashCatAi') },
|
||||
{ ...slashCommands[16], title: t('richTextEditor.slashShorten'), description: t('richTextEditor.slashShortenDesc'), category: t('richTextEditor.slashCatAi') },
|
||||
{ ...slashCommands[17], title: t('richTextEditor.slashImprove'), description: t('richTextEditor.slashImproveDesc'), category: t('richTextEditor.slashCatAi') },
|
||||
{ ...slashCommands[18], title: t('richTextEditor.slashExpand'), description: t('richTextEditor.slashExpandDesc'), category: t('richTextEditor.slashCatAi') },
|
||||
{ ...slashCommands[19], title: t('richTextEditor.bold'), description: t('richTextEditor.bold'), category: t('richTextEditor.slashCatFormatting') },
|
||||
{ ...slashCommands[20], title: t('richTextEditor.italic'), description: t('richTextEditor.italic'), category: t('richTextEditor.slashCatFormatting') },
|
||||
{ ...slashCommands[21], title: t('richTextEditor.underline'), description: t('richTextEditor.underline'), category: t('richTextEditor.slashCatFormatting') },
|
||||
{ ...slashCommands[22], title: t('richTextEditor.strike'), description: t('richTextEditor.strike'), category: t('richTextEditor.slashCatFormatting') },
|
||||
{ ...slashCommands[23], title: t('richTextEditor.highlight'), description: t('richTextEditor.highlight'), category: t('richTextEditor.slashCatFormatting') },
|
||||
{ ...slashCommands[24], title: t('richTextEditor.slashSuperscript'), description: t('richTextEditor.slashSuperscriptDesc'), category: t('richTextEditor.slashCatFormatting') },
|
||||
{ ...slashCommands[25], title: t('richTextEditor.slashSubscript'), description: t('richTextEditor.slashSubscriptDesc'), category: t('richTextEditor.slashCatFormatting') },
|
||||
{ ...slashCommands[26], title: t('richTextEditor.slashDiagram'), description: t('richTextEditor.slashDiagramDesc'), category: t('richTextEditor.slashCatAi') },
|
||||
{ ...slashCommands[27], title: t('richTextEditor.slashSlides'), description: t('richTextEditor.slashSlidesDesc'), category: t('richTextEditor.slashCatAi') },
|
||||
const localCommands: SlashMenuItem[] = [
|
||||
{ ...slashCommands[0], title: t('richTextEditor.slashText'), description: t('richTextEditor.slashTextDesc'), categoryId: 'basic' },
|
||||
{ ...slashCommands[1], title: t('richTextEditor.slashH1'), description: t('richTextEditor.slashH1Desc'), categoryId: 'basic' },
|
||||
{ ...slashCommands[2], title: t('richTextEditor.slashH2'), description: t('richTextEditor.slashH2Desc'), categoryId: 'basic' },
|
||||
{ ...slashCommands[3], title: t('richTextEditor.slashH3'), description: t('richTextEditor.slashH3Desc'), categoryId: 'basic' },
|
||||
{ ...slashCommands[4], title: t('richTextEditor.slashTable'), description: t('richTextEditor.slashTableDesc'), categoryId: 'basic' },
|
||||
{ ...slashCommands[5], title: t('richTextEditor.slashBullet'), description: t('richTextEditor.slashBulletDesc'), categoryId: 'basic' },
|
||||
{ ...slashCommands[6], title: t('richTextEditor.slashNumbered'), description: t('richTextEditor.slashNumberedDesc'), categoryId: 'basic' },
|
||||
{ ...slashCommands[7], title: t('richTextEditor.slashTodo'), description: t('richTextEditor.slashTodoDesc'), categoryId: 'basic' },
|
||||
{ ...slashCommands[8], title: t('richTextEditor.slashQuote'), description: t('richTextEditor.slashQuoteDesc'), categoryId: 'basic' },
|
||||
{ ...slashCommands[9], title: t('richTextEditor.slashCode'), description: t('richTextEditor.slashCodeDesc'), categoryId: 'basic' },
|
||||
{ ...slashCommands[10], title: t('richTextEditor.slashDivider'), description: t('richTextEditor.slashDividerDesc'), categoryId: 'basic' },
|
||||
{ ...slashCommands[11], title: t('richTextEditor.slashImage'), description: t('richTextEditor.slashImageDesc'), categoryId: 'media' },
|
||||
{ ...slashCommands[12], title: t('richTextEditor.slashAlignLeft'), description: t('richTextEditor.slashAlignLeftDesc'), categoryId: 'formatting' },
|
||||
{ ...slashCommands[13], title: t('richTextEditor.slashAlignCenter'), description: t('richTextEditor.slashAlignCenterDesc'), categoryId: 'formatting' },
|
||||
{ ...slashCommands[14], title: t('richTextEditor.slashAlignRight'), description: t('richTextEditor.slashAlignRightDesc'), categoryId: 'formatting' },
|
||||
{ ...slashCommands[15], title: t('richTextEditor.slashClarify'), description: t('richTextEditor.slashClarifyDesc'), categoryId: 'ai' },
|
||||
{ ...slashCommands[16], title: t('richTextEditor.slashShorten'), description: t('richTextEditor.slashShortenDesc'), categoryId: 'ai' },
|
||||
{ ...slashCommands[17], title: t('richTextEditor.slashImprove'), description: t('richTextEditor.slashImproveDesc'), categoryId: 'ai' },
|
||||
{ ...slashCommands[18], title: t('richTextEditor.slashExpand'), description: t('richTextEditor.slashExpandDesc'), categoryId: 'ai' },
|
||||
{ ...slashCommands[19], title: t('richTextEditor.bold'), description: t('richTextEditor.bold'), categoryId: 'formatting' },
|
||||
{ ...slashCommands[20], title: t('richTextEditor.italic'), description: t('richTextEditor.italic'), categoryId: 'formatting' },
|
||||
{ ...slashCommands[21], title: t('richTextEditor.underline'), description: t('richTextEditor.underline'), categoryId: 'formatting' },
|
||||
{ ...slashCommands[22], title: t('richTextEditor.strike'), description: t('richTextEditor.strike'), categoryId: 'formatting' },
|
||||
{ ...slashCommands[23], title: t('richTextEditor.highlight'), description: t('richTextEditor.highlight'), categoryId: 'formatting' },
|
||||
{ ...slashCommands[24], title: t('richTextEditor.slashSuperscript'), description: t('richTextEditor.slashSuperscriptDesc'), categoryId: 'formatting' },
|
||||
{ ...slashCommands[25], title: t('richTextEditor.slashSubscript'), description: t('richTextEditor.slashSubscriptDesc'), categoryId: 'formatting' },
|
||||
{ ...slashCommands[26], title: t('richTextEditor.slashDiagram'), description: t('richTextEditor.slashDiagramDesc'), categoryId: 'ai' },
|
||||
{ ...slashCommands[27], title: t('richTextEditor.slashSlides'), description: t('richTextEditor.slashSlidesDesc'), categoryId: 'ai' },
|
||||
]
|
||||
|
||||
const closeMenu = useCallback(() => {
|
||||
@@ -557,7 +580,11 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
|
||||
}
|
||||
}, [editor])
|
||||
|
||||
const handleSelect = useCallback(async (item: SlashItem) => {
|
||||
const handleSelect = useCallback(async (item: SlashMenuItem) => {
|
||||
const toastAi = (err: unknown) => {
|
||||
const msg = err instanceof Error ? err.message : ''
|
||||
toast.error(msg && msg !== AI_REFORMULATE_FALLBACK ? msg : t('richTextEditor.aiReformulateFailed'))
|
||||
}
|
||||
if (item.isImage) {
|
||||
deleteSlashText(); closeMenu(); onInsertImage(editor)
|
||||
} else if (item.isAi && item.aiOption) {
|
||||
@@ -567,31 +594,35 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
|
||||
if (!allText || allText.split(/\s+/).length < 5) return
|
||||
const result = await aiReformulate(allText, item.aiOption)
|
||||
editor.chain().focus().setContent(result).run()
|
||||
} catch (err) { console.error('AI slash error:', err) }
|
||||
} catch (err) {
|
||||
console.error('AI slash error:', err)
|
||||
toastAi(err)
|
||||
}
|
||||
finally { setAiLoading(false) }
|
||||
} else {
|
||||
deleteSlashText(); item.command(editor); closeMenu()
|
||||
}
|
||||
}, [editor, closeMenu, deleteSlashText, onInsertImage])
|
||||
}, [editor, closeMenu, deleteSlashText, onInsertImage, t])
|
||||
|
||||
const allCategories = Array.from(new Set(localCommands.map(c => c.category || 'Basic blocks')))
|
||||
const presentCategoryIds = new Set(localCommands.map(c => c.categoryId))
|
||||
const allCategories = ORDERED_SLASH_CATEGORIES.filter(id => presentCategoryIds.has(id))
|
||||
|
||||
const textFiltered = localCommands.filter(c => c.title.toLowerCase().includes(query.toLowerCase()) || c.description.toLowerCase().includes(query.toLowerCase()))
|
||||
const filtered = activeCategory ? textFiltered.filter(c => (c.category || 'Basic blocks') === activeCategory) : textFiltered
|
||||
const filtered = activeCategory ? textFiltered.filter(c => c.categoryId === activeCategory) : textFiltered
|
||||
|
||||
const availableCategoriesInSearch = textFiltered.reduce((acc, item) => {
|
||||
const cat = item.category || 'Basic blocks'
|
||||
if (!acc[cat]) acc[cat] = []
|
||||
acc[cat].push(item)
|
||||
const id = item.categoryId
|
||||
if (!acc[id]) acc[id] = []
|
||||
acc[id].push(item)
|
||||
return acc
|
||||
}, {} as Record<string, SlashItem[]>)
|
||||
}, {} as Record<SlashCategoryId, SlashMenuItem[]>)
|
||||
|
||||
const categories = filtered.reduce((acc, item) => {
|
||||
const cat = item.category || 'Basic blocks'
|
||||
if (!acc[cat]) acc[cat] = []
|
||||
acc[cat].push(item)
|
||||
const id = item.categoryId
|
||||
if (!acc[id]) acc[id] = []
|
||||
acc[id].push(item)
|
||||
return acc
|
||||
}, {} as Record<string, SlashItem[]>)
|
||||
}, {} as Record<SlashCategoryId, SlashMenuItem[]>)
|
||||
|
||||
// Auto-scroll selected item into view
|
||||
useEffect(() => {
|
||||
@@ -605,7 +636,7 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
|
||||
if (e.key === 'ArrowUp') { e.preventDefault(); setSelectedIndex(i => (i - 1 + filtered.length) % filtered.length); return }
|
||||
if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
|
||||
e.preventDefault()
|
||||
const availableTabs = [null, ...allCategories.filter(cat => availableCategoriesInSearch[cat])]
|
||||
const availableTabs = [null as SlashCategoryId | null, ...allCategories.filter(id => (availableCategoriesInSearch[id]?.length ?? 0) > 0)]
|
||||
const currentIndex = availableTabs.indexOf(activeCategory)
|
||||
const nextIndex = e.key === 'ArrowRight'
|
||||
? (currentIndex + 1) % availableTabs.length
|
||||
@@ -623,7 +654,7 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
|
||||
if (e.key === 'Escape') { e.preventDefault(); closeMenu(); return }
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault()
|
||||
const availableTabs = [null, ...allCategories.filter(cat => availableCategoriesInSearch[cat])]
|
||||
const availableTabs = [null as SlashCategoryId | null, ...allCategories.filter(id => (availableCategoriesInSearch[id]?.length ?? 0) > 0)]
|
||||
const nextIndex = (availableTabs.indexOf(activeCategory) + 1) % availableTabs.length
|
||||
setActiveCategory(availableTabs[nextIndex])
|
||||
setSelectedIndex(0)
|
||||
@@ -632,7 +663,7 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
|
||||
}
|
||||
document.addEventListener('keydown', handleKeyDown, true)
|
||||
return () => document.removeEventListener('keydown', handleKeyDown, true)
|
||||
}, [isOpen, selectedIndex, filtered, handleSelect, closeMenu, categories, activeCategory])
|
||||
}, [isOpen, selectedIndex, filtered, handleSelect, closeMenu, activeCategory, allCategories, availableCategoriesInSearch])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return
|
||||
@@ -683,7 +714,8 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
|
||||
if (!isOpen || filtered.length === 0) return null
|
||||
|
||||
let flatIndex = -1
|
||||
const totalVisible = Object.keys(categories).length
|
||||
const sectionIds = ORDERED_SLASH_CATEGORIES.filter(id => (categories[id]?.length ?? 0) > 0)
|
||||
const totalVisible = sectionIds.length
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
@@ -702,16 +734,16 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
|
||||
>
|
||||
{t('richTextEditor.slashTabAll')}
|
||||
</button>
|
||||
{allCategories.filter(cat => availableCategoriesInSearch[cat]).map(cat => (
|
||||
{allCategories.filter(id => (availableCategoriesInSearch[id]?.length ?? 0) > 0).map(catId => (
|
||||
<button
|
||||
key={cat}
|
||||
className={cn('notion-slash-tab', activeCategory === cat && 'notion-slash-tab-active', cat === 'IA Note' && 'notion-slash-tab-ai')}
|
||||
key={catId}
|
||||
className={cn('notion-slash-tab', activeCategory === catId && 'notion-slash-tab-active', catId === 'ai' && 'notion-slash-tab-ai')}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => { setActiveCategory(activeCategory === cat ? null : cat); setSelectedIndex(0) }}
|
||||
onClick={() => { setActiveCategory(activeCategory === catId ? null : catId); setSelectedIndex(0) }}
|
||||
>
|
||||
{cat === 'IA Note' && <Sparkles className="w-2.5 h-2.5" />}
|
||||
{cat}
|
||||
<span className="notion-slash-tab-count">{availableCategoriesInSearch[cat]?.length ?? 0}</span>
|
||||
{catId === 'ai' && <Sparkles className="w-2.5 h-2.5" />}
|
||||
{slashCategoryLabel(catId, t)}
|
||||
<span className="notion-slash-tab-count">{availableCategoriesInSearch[catId]?.length ?? 0}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -728,40 +760,43 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
|
||||
<span>{t('richTextEditor.slashLoading')}</span>
|
||||
</div>
|
||||
)}
|
||||
{!aiLoading && Object.entries(categories).map(([cat, items]) => (
|
||||
<div key={cat} className="notion-slash-section">
|
||||
<div className={cn('notion-slash-label', cat === 'IA Note' && 'notion-slash-label-ai')}>
|
||||
{cat === 'IA Note' && <Sparkles className="w-3 h-3 inline mr-1" />}
|
||||
{CAT_LABELS[cat] || cat}
|
||||
{!aiLoading && sectionIds.map((catId) => {
|
||||
const items = categories[catId]!
|
||||
return (
|
||||
<div key={catId} className="notion-slash-section">
|
||||
<div className={cn('notion-slash-label', catId === 'ai' && 'notion-slash-label-ai')}>
|
||||
{catId === 'ai' && <Sparkles className="w-3 h-3 inline mr-1" />}
|
||||
{slashCategoryLabel(catId, t)}
|
||||
</div>
|
||||
{items.map((item) => {
|
||||
flatIndex++
|
||||
const idx = flatIndex
|
||||
const isSelected = idx === selectedIndex
|
||||
return (
|
||||
<button
|
||||
key={`${catId}-${item.title}-${item.shortcut ?? ''}`}
|
||||
ref={isSelected ? selectedItemRef : null}
|
||||
className={cn('notion-slash-item', isSelected && 'notion-slash-item-selected')}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => handleSelect(item)}
|
||||
onMouseEnter={() => setSelectedIndex(idx)}
|
||||
>
|
||||
<div className={cn('notion-slash-icon', item.isAi && 'notion-slash-icon-ai')}>
|
||||
<item.icon className="w-4 h-4" />
|
||||
</div>
|
||||
<div className="notion-slash-content">
|
||||
<span className="notion-slash-title">{item.title}</span>
|
||||
<span className="notion-slash-desc">{item.description}</span>
|
||||
</div>
|
||||
{item.shortcut && (
|
||||
<span className="notion-slash-shortcut">{item.shortcut}</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{items.map((item) => {
|
||||
flatIndex++
|
||||
const idx = flatIndex
|
||||
const isSelected = idx === selectedIndex
|
||||
return (
|
||||
<button
|
||||
key={item.title}
|
||||
ref={isSelected ? selectedItemRef : null}
|
||||
className={cn('notion-slash-item', isSelected && 'notion-slash-item-selected')}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => handleSelect(item)}
|
||||
onMouseEnter={() => setSelectedIndex(idx)}
|
||||
>
|
||||
<div className={cn('notion-slash-icon', item.isAi && 'notion-slash-icon-ai')}>
|
||||
<item.icon className="w-4 h-4" />
|
||||
</div>
|
||||
<div className="notion-slash-content">
|
||||
<span className="notion-slash-title">{item.title}</span>
|
||||
<span className="notion-slash-desc">{item.description}</span>
|
||||
</div>
|
||||
{item.shortcut && (
|
||||
<span className="notion-slash-shortcut">{item.shortcut}</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>,
|
||||
document.body
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user