UI Stabilization: Global color theme updates (#75B2D6), AI Assistant styling refactor, and navigation fixes

This commit is contained in:
Antigravity
2026-05-09 12:58:16 +00:00
parent 1446463f04
commit 60a3fe5453
47 changed files with 3585 additions and 2149 deletions

View File

@@ -14,6 +14,10 @@ import Image from '@tiptap/extension-image'
import TextAlign from '@tiptap/extension-text-align'
import TaskList from '@tiptap/extension-task-list'
import TaskItem from '@tiptap/extension-task-item'
import { Table } from '@tiptap/extension-table'
import { TableRow } from '@tiptap/extension-table-row'
import { TableCell } from '@tiptap/extension-table-cell'
import { TableHeader } from '@tiptap/extension-table-header'
import Superscript from '@tiptap/extension-superscript'
import Subscript from '@tiptap/extension-subscript'
import Typography from '@tiptap/extension-typography'
@@ -26,7 +30,8 @@ import {
Sparkles, Wand2, Scissors, Lightbulb, X, Check, ExternalLink,
FileText, Pilcrow, MessageSquare, AlignLeft, AlignCenter, AlignRight,
Superscript as SuperscriptIcon, Subscript as SubscriptIcon, Expand, Plus,
SpellCheck, Languages, BookOpen } from 'lucide-react'
SpellCheck, Languages, BookOpen, Presentation
} from 'lucide-react'
import { cn } from '@/lib/utils'
import { toast } from 'sonner'
@@ -45,13 +50,13 @@ interface RichTextEditorProps {
type SlashItem = {
title: string
description: string
icon: typeof Bold
icon: any
category?: string
shortcut?: string
isImage?: boolean
isAi?: boolean
aiOption?: 'clarify' | 'shorten' | 'improve'
command: (editor: Editor) => void
command: (editor: Editor, range?: any) => void
}
const CustomImage = Image.extend({
@@ -71,28 +76,50 @@ const CustomImage = Image.extend({
})
const slashCommands: SlashItem[] = [
// Basic blocks (indices 0-9)
// Basic blocks
{ title: 'Text', description: 'Plain paragraph', icon: Pilcrow, category: 'Basic blocks', shortcut: '¶', command: (e) => e.chain().focus().setParagraph().run() },
{ title: 'Heading 1', description: 'Big section heading', icon: Heading1, category: 'Basic blocks', shortcut: '#', command: (e) => e.chain().focus().toggleHeading({ level: 1 }).run() },
{ title: 'Heading 2', description: 'Medium section heading', icon: Heading2, category: 'Basic blocks', shortcut: '##', command: (e) => e.chain().focus().toggleHeading({ level: 2 }).run() },
{ title: 'Heading 3', description: 'Small section heading', icon: Heading3, category: 'Basic blocks', shortcut: '###', command: (e) => e.chain().focus().toggleHeading({ level: 3 }).run() },
{ title: 'Table', description: 'Insert a simple table', icon: () => <span className="text-xs font-bold border rounded px-1">TBL</span>, category: 'Basic blocks', command: (e) => e.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run() },
{ title: 'Bullet List', description: 'Unordered list', icon: List, category: 'Basic blocks', shortcut: '-', command: (e) => e.chain().focus().toggleBulletList().run() },
{ title: 'Numbered List', description: 'Ordered numbered list', icon: ListOrdered, category: 'Basic blocks', shortcut: '1.', command: (e) => e.chain().focus().toggleOrderedList().run() },
{ title: 'To-do List', description: 'Checkboxes for tasks', icon: CheckSquare, category: 'Basic blocks', shortcut: '[]', command: (e) => e.chain().focus().toggleTaskList().run() },
{ title: 'Quote', description: 'Capture a quote', icon: Quote, category: 'Basic blocks', shortcut: '>', command: (e) => e.chain().focus().toggleBlockquote().run() },
{ title: 'Code Block', description: 'Code snippet', icon: CodeXml, category: 'Basic blocks', shortcut: '```', command: (e) => e.chain().focus().toggleCodeBlock().run() },
{ title: 'Divider', description: 'Horizontal separator', icon: Minus, category: 'Basic blocks', shortcut: '---', command: (e) => e.chain().focus().setHorizontalRule().run() },
// Media (index 10)
{ title: 'Image', description: 'Embed image from URL', icon: ImageIcon, category: 'Media', isImage: true, command: () => {} },
// Formatting (indices 11-13) — super/subscript removed, use BubbleMenu
// Media
{ title: 'Image', description: 'Embed image from URL', icon: ImageIcon, category: 'Media', isImage: true, command: () => { } },
// Formatting
{ title: 'Align Left', description: 'Align text left', icon: AlignLeft, category: 'Formatting', command: (e) => e.chain().focus().setTextAlign('left').run() },
{ title: 'Align Center', description: 'Center text', icon: AlignCenter, category: 'Formatting', command: (e) => e.chain().focus().setTextAlign('center').run() },
{ title: 'Align Right', description: 'Align text right', icon: AlignRight, category: 'Formatting', command: (e) => e.chain().focus().setTextAlign('right').run() },
// IA Note (indices 14-17)
{ title: 'Clarifier', description: 'Rendre le texte plus clair', icon: Lightbulb, category: 'IA Note', isAi: true, aiOption: 'clarify', command: () => {} },
{ title: 'Raccourcir', description: 'Condenser le texte', icon: Scissors, category: 'IA Note', isAi: true, aiOption: 'shorten', command: () => {} },
{ title: 'Améliorer', description: 'Améliorer le style', icon: Wand2, category: 'IA Note', isAi: true, aiOption: 'improve', command: () => {} },
{ title: 'Développer', description: 'Élaborer et enrichir le texte', icon: Expand, category: 'IA Note', isAi: true, aiOption: 'clarify', command: () => {} },
// IA Note
{ title: 'Clarifier', description: 'Rendre le texte plus clair', icon: Lightbulb, category: 'IA Note', isAi: true, aiOption: 'clarify', command: () => { } },
{ title: 'Raccourcir', description: 'Condenser le texte', icon: Scissors, category: 'IA Note', isAi: true, aiOption: 'shorten', command: () => { } },
{ title: 'Améliorer', description: 'Améliorer le style', icon: Wand2, category: 'IA Note', isAi: true, aiOption: 'improve', command: () => { } },
{ title: 'Développer', description: 'Élaborer et enrichir le texte', icon: Expand, category: 'IA Note', isAi: true, aiOption: 'clarify', command: () => { } },
// Formatting extensions
{ title: 'Bold', description: 'Make text bold', icon: Bold, category: 'Formatting', command: (e) => e.chain().focus().toggleBold().run() },
{ title: 'Italic', description: 'Make text italic', icon: Italic, category: 'Formatting', command: (e) => e.chain().focus().toggleItalic().run() },
{ title: 'Underline', description: 'Underline text', icon: UnderlineIcon, category: 'Formatting', command: (e) => e.chain().focus().toggleUnderline().run() },
{ title: 'Strike', description: 'Strikethrough text', icon: Strikethrough, category: 'Formatting', command: (e) => e.chain().focus().toggleStrike().run() },
{ title: 'Highlight', description: 'Highlight text', icon: Highlighter, category: 'Formatting', command: (e) => e.chain().focus().toggleHighlight().run() },
{ title: 'Superscript', description: 'Text above the baseline', icon: SuperscriptIcon, category: 'Formatting', command: (e) => e.chain().focus().toggleSuperscript().run() },
{ title: 'Subscript', description: 'Text below the baseline', icon: SubscriptIcon, category: 'Formatting', command: (e) => e.chain().focus().toggleSubscript().run() },
// AI Tools
{
title: 'Diagramme', description: 'Générer un diagramme Excalidraw', icon: BookOpen, category: 'IA Note', command: (e) => {
const event = new CustomEvent('memento-open-ai', { detail: { tab: 'actions', scroll: 'diagram' } })
window.dispatchEvent(event)
}
},
{
title: 'Présentation', description: 'Générer des slides HTML/PPTX', icon: Presentation, category: 'IA Note', command: (e) => {
const event = new CustomEvent('memento-open-ai', { detail: { tab: 'actions', scroll: 'slides' } })
window.dispatchEvent(event)
}
},
]
async function aiReformulate(text: string, option: string, language?: string): Promise<string> {
@@ -146,6 +173,10 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorPro
TextAlign.configure({ types: ['heading', 'paragraph', 'image'] }),
TaskList,
TaskItem.configure({ nested: true }),
Table.configure({ resizable: true }),
TableRow,
TableHeader,
TableCell,
Superscript,
Subscript,
Typography,
@@ -202,7 +233,7 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorPro
editor={editor}
className="notion-bubble-menu"
{...({
tippyOptions: {
tippyOptions: {
appendTo: () => document.body,
zIndex: 99999,
fallbackPlacements: ['bottom', 'top']
@@ -278,7 +309,7 @@ function ImageModal({ onConfirm, onCancel }: { onConfirm: (url: string) => void;
)
}
const AI_LANGS = ['Francais','English','Espanol','Deutsch','Persan','Portugais','Italiano','Chinois','Japonais']
const AI_LANGS = ['Francais', 'English', 'Espanol', 'Deutsch', 'Persan', 'Portugais', 'Italiano', 'Chinois', 'Japonais']
function BubbleToolbar({ editor }: { editor: Editor | null }) {
const { t, language } = useLanguage()
@@ -472,10 +503,8 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
const [aiLoading, setAiLoading] = useState(false)
const menuRef = useRef<HTMLDivElement>(null)
const selectedItemRef = useRef<HTMLButtonElement>(null)
// Flag: true while user is interacting with the menu (prevents selectionUpdate from closing it)
const menuInteracting = useRef(false)
// Translated category names (keys match slashCommands category field)
const CAT_LABELS: Record<string, string> = {
'Basic blocks': t('richTextEditor.slashCatBasic'),
'Media': t('richTextEditor.slashCatMedia'),
@@ -483,26 +512,35 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
'IA Note': t('richTextEditor.slashCatAi'),
}
// Translated command list (keeps same order/icons/shortcuts as global slashCommands)
const localCommands: SlashItem[] = [
{ ...slashCommands[0], title: t('richTextEditor.slashText'), description: t('richTextEditor.slashTextDesc'), category: 'Basic blocks' },
{ ...slashCommands[1], title: t('richTextEditor.slashH1'), description: t('richTextEditor.slashH1Desc'), category: 'Basic blocks' },
{ ...slashCommands[2], title: t('richTextEditor.slashH2'), description: t('richTextEditor.slashH2Desc'), category: 'Basic blocks' },
{ ...slashCommands[3], title: t('richTextEditor.slashH3'), description: t('richTextEditor.slashH3Desc'), category: 'Basic blocks' },
{ ...slashCommands[4], title: t('richTextEditor.slashBullet'), description: t('richTextEditor.slashBulletDesc'), category: 'Basic blocks' },
{ ...slashCommands[5], title: t('richTextEditor.slashNumbered'), description: t('richTextEditor.slashNumberedDesc'), category: 'Basic blocks' },
{ ...slashCommands[6], title: t('richTextEditor.slashTodo'), description: t('richTextEditor.slashTodoDesc'), category: 'Basic blocks' },
{ ...slashCommands[7], title: t('richTextEditor.slashQuote'), description: t('richTextEditor.slashQuoteDesc'), category: 'Basic blocks' },
{ ...slashCommands[8], title: t('richTextEditor.slashCode'), description: t('richTextEditor.slashCodeDesc'), category: 'Basic blocks' },
{ ...slashCommands[9], title: t('richTextEditor.slashDivider'), description: t('richTextEditor.slashDividerDesc'), category: 'Basic blocks' },
{ ...slashCommands[10], title: t('richTextEditor.slashImage'), description: t('richTextEditor.slashImageDesc'), category: 'Media' },
{ ...slashCommands[11], title: t('richTextEditor.slashAlignLeft'), description: t('richTextEditor.slashAlignLeftDesc'), category: 'Formatting' },
{ ...slashCommands[12], title: t('richTextEditor.slashAlignCenter'), description: t('richTextEditor.slashAlignCenterDesc'), category: 'Formatting' },
{ ...slashCommands[13], title: t('richTextEditor.slashAlignRight'), description: t('richTextEditor.slashAlignRightDesc'), category: 'Formatting' },
{ ...slashCommands[14], title: t('richTextEditor.slashClarify'), description: t('richTextEditor.slashClarifyDesc'), category: 'IA Note' },
{ ...slashCommands[15], title: t('richTextEditor.slashShorten'), description: t('richTextEditor.slashShortenDesc'), category: 'IA Note' },
{ ...slashCommands[16], title: t('richTextEditor.slashImprove'), description: t('richTextEditor.slashImproveDesc'), category: 'IA Note' },
{ ...slashCommands[17], title: t('richTextEditor.slashExpand'), description: t('richTextEditor.slashExpandDesc'), category: 'IA Note' },
{ ...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 closeMenu = useCallback(() => {
@@ -536,13 +574,11 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
}
}, [editor, closeMenu, deleteSlashText, onInsertImage])
// All category names in order
const allCategories = Array.from(new Set(localCommands.map(c => c.category || 'Basic blocks')))
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
// Compute categories based on full search to keep tabs visible even when one is selected
const availableCategoriesInSearch = textFiltered.reduce((acc, item) => {
const cat = item.category || 'Basic blocks'
if (!acc[cat]) acc[cat] = []
@@ -571,8 +607,8 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
e.preventDefault()
const availableTabs = [null, ...allCategories.filter(cat => availableCategoriesInSearch[cat])]
const currentIndex = availableTabs.indexOf(activeCategory)
const nextIndex = e.key === 'ArrowRight'
? (currentIndex + 1) % availableTabs.length
const nextIndex = e.key === 'ArrowRight'
? (currentIndex + 1) % availableTabs.length
: (currentIndex - 1 + availableTabs.length) % availableTabs.length
setActiveCategory(availableTabs[nextIndex])
setSelectedIndex(0)
@@ -602,8 +638,17 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
if (!isOpen) return
const { from } = editor.state.selection
const c = editor.view.coordsAtPos(from)
setCoords({ top: c.bottom + 8, left: c.left })
}, [isOpen, editor, query])
// Check if menu would overflow bottom
const menuHeight = menuRef.current?.offsetHeight || 300
const wouldOverflow = c.bottom + menuHeight + 20 > window.innerHeight
if (wouldOverflow) {
setCoords({ top: c.top - menuHeight - 8, left: c.left })
} else {
setCoords({ top: c.bottom + 8, left: c.left })
}
}, [isOpen, editor, query, filtered.length])
useEffect(() => {
const handleClick = (e: MouseEvent) => {