feat: improve slash command menu - new blocks, formatting, IA Note section, premium design
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 44s

This commit is contained in:
2026-05-02 21:55:11 +02:00
parent bd5391e3f2
commit 7b9534703c
2 changed files with 135 additions and 56 deletions

View File

@@ -1222,23 +1222,42 @@ html.font-system * {
} }
/* ============================================ /* ============================================
Slash Command Menu Slash Command Menu — Premium Design
============================================ */ ============================================ */
.notion-slash-menu { .notion-slash-menu {
position: fixed; position: fixed;
z-index: 9999; z-index: 9999;
background: var(--popover); background: var(--popover);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 8px; border-radius: 12px;
box-shadow: 0 4px 24px rgba(0,0,0,0.1); box-shadow: 0 8px 32px rgba(0,0,0,0.12), 0 2px 8px rgba(0,0,0,0.06);
padding: 4px; padding: 6px;
min-width: 300px; min-width: 320px;
max-height: 340px; max-height: 420px;
overflow-y: auto; overflow-y: auto;
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: var(--border) transparent;
animation: slash-enter 0.12s ease;
}
@keyframes slash-enter {
from { opacity: 0; transform: translateY(-4px) scale(0.98); }
to { opacity: 1; transform: translateY(0) scale(1); }
} }
.dark .notion-slash-menu { .dark .notion-slash-menu {
box-shadow: 0 4px 24px rgba(0,0,0,0.5); box-shadow: 0 8px 32px rgba(0,0,0,0.5), 0 2px 8px rgba(0,0,0,0.3);
}
/* Header hint */
.notion-slash-header {
display: flex;
align-items: center;
gap: 5px;
padding: 5px 10px 6px;
font-size: 10px;
color: var(--muted-foreground);
border-bottom: 1px solid var(--border);
margin-bottom: 4px;
opacity: 0.7;
} }
.notion-slash-section + .notion-slash-section { .notion-slash-section + .notion-slash-section {
@@ -1248,62 +1267,79 @@ html.font-system * {
} }
.notion-slash-label { .notion-slash-label {
padding: 6px 8px 2px; padding: 6px 10px 3px;
font-size: 11px; font-size: 10.5px;
font-weight: 600; font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--muted-foreground); color: var(--muted-foreground);
user-select: none; user-select: none;
} }
.notion-slash-label-ai {
color: oklch(0.55 0.15 270);
}
.dark .notion-slash-label-ai {
color: oklch(0.72 0.15 270);
}
.notion-slash-item { .notion-slash-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 10px;
width: 100%; width: 100%;
padding: 4px 8px; padding: 6px 8px;
border: none; border: none;
background: transparent; background: transparent;
border-radius: 4px; border-radius: 7px;
cursor: pointer; cursor: pointer;
text-align: left; text-align: left;
color: var(--foreground); color: var(--foreground);
transition: background 0.1s ease; transition: background 0.1s ease, transform 0.08s ease;
} }
.notion-slash-item:hover, .notion-slash-item:hover,
.notion-slash-item-selected { .notion-slash-item-selected {
background: var(--accent); background: var(--accent);
} }
.notion-slash-item-selected {
transform: none;
}
.notion-slash-icon { .notion-slash-icon {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 20px; width: 30px;
height: 20px; height: 30px;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 4px; border-radius: 7px;
background: var(--background); background: var(--background);
color: var(--muted-foreground); color: var(--foreground);
flex-shrink: 0; flex-shrink: 0;
transition: background 0.1s ease;
} }
.notion-slash-icon-ai { .notion-slash-item-selected .notion-slash-icon,
background: oklch(0.95 0.05 270); .notion-slash-item:hover .notion-slash-icon {
border-color: oklch(0.8 0.08 270); background: var(--popover);
color: oklch(0.55 0.15 270);
}
.dark .notion-slash-icon-ai {
background: oklch(0.25 0.05 270);
border-color: oklch(0.4 0.08 270);
color: oklch(0.75 0.15 270);
} }
.notion-slash-loading { .notion-slash-icon-ai {
background: oklch(0.95 0.04 270);
border-color: oklch(0.82 0.08 270);
color: oklch(0.52 0.18 270);
}
.dark .notion-slash-icon-ai {
background: oklch(0.22 0.05 270);
border-color: oklch(0.38 0.09 270);
color: oklch(0.75 0.16 270);
}
/* Stacked title + description */
.notion-slash-content {
display: flex; display: flex;
align-items: center; flex-direction: column;
gap: 6px; flex: 1;
padding: 8px 10px; min-width: 0;
font-size: 13px; gap: 1px;
color: var(--muted-foreground);
} }
.notion-slash-title { .notion-slash-title {
@@ -1312,7 +1348,7 @@ html.font-system * {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
flex: 0 1 auto; line-height: 1.3;
} }
.notion-slash-desc { .notion-slash-desc {
font-size: 11px; font-size: 11px;
@@ -1320,8 +1356,29 @@ html.font-system * {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
flex: 1 1 auto; line-height: 1.3;
text-align: right; }
/* Shortcut badge */
.notion-slash-shortcut {
font-size: 10px;
font-family: ui-monospace, monospace;
background: var(--muted);
color: var(--muted-foreground);
border: 1px solid var(--border);
border-radius: 4px;
padding: 1px 5px;
flex-shrink: 0;
white-space: nowrap;
}
.notion-slash-loading {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
font-size: 13px;
color: var(--muted-foreground);
} }
/* ============================================ /* ============================================

View File

@@ -23,6 +23,7 @@ import {
Quote, CodeXml, Minus, ImageIcon, Type, Highlighter, Link as LinkIcon, Quote, CodeXml, Minus, ImageIcon, Type, Highlighter, Link as LinkIcon,
Sparkles, Wand2, Scissors, Lightbulb, X, Check, ExternalLink, Sparkles, Wand2, Scissors, Lightbulb, X, Check, ExternalLink,
FileText, Pilcrow, MessageSquare, AlignLeft, AlignCenter, AlignRight, FileText, Pilcrow, MessageSquare, AlignLeft, AlignCenter, AlignRight,
Superscript as SuperscriptIcon, Subscript as SubscriptIcon, Expand,
} from 'lucide-react' } from 'lucide-react'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
@@ -42,6 +43,7 @@ type SlashItem = {
description: string description: string
icon: typeof Bold icon: typeof Bold
category?: string category?: string
shortcut?: string
isImage?: boolean isImage?: boolean
isAi?: boolean isAi?: boolean
aiOption?: 'clarify' | 'shorten' | 'improve' aiOption?: 'clarify' | 'shorten' | 'improve'
@@ -49,20 +51,26 @@ type SlashItem = {
} }
const slashCommands: SlashItem[] = [ const slashCommands: SlashItem[] = [
{ title: 'Text', description: 'Just start writing with plain text', icon: Pilcrow, category: 'Basic blocks', command: (e) => e.chain().focus().setParagraph().run() }, { 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', command: (e) => e.chain().focus().toggleHeading({ level: 1 }).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', command: (e) => e.chain().focus().toggleHeading({ level: 2 }).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', command: (e) => e.chain().focus().toggleHeading({ level: 3 }).run() }, { title: 'Heading 3', description: 'Small section heading', icon: Heading3, category: 'Basic blocks', shortcut: '###', command: (e) => e.chain().focus().toggleHeading({ level: 3 }).run() },
{ title: 'Bullet List', description: 'Create a simple bullet list', icon: List, category: 'Basic blocks', command: (e) => e.chain().focus().toggleBulletList().run() }, { title: 'Bullet List', description: 'Unordered list', icon: List, category: 'Basic blocks', shortcut: '-', command: (e) => e.chain().focus().toggleBulletList().run() },
{ title: 'Numbered List', description: 'Create a list with numbering', icon: ListOrdered, category: 'Basic blocks', command: (e) => e.chain().focus().toggleOrderedList().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: 'Track tasks with checkboxes', icon: CheckSquare, category: 'Basic blocks', command: (e) => e.chain().focus().toggleTaskList().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', command: (e) => e.chain().focus().toggleBlockquote().run() }, { title: 'Quote', description: 'Capture a quote', icon: Quote, category: 'Basic blocks', shortcut: '>', command: (e) => e.chain().focus().toggleBlockquote().run() },
{ title: 'Code Block', description: 'Capture a code snippet', icon: CodeXml, category: 'Basic blocks', command: (e) => e.chain().focus().toggleCodeBlock().run() }, { title: 'Code Block', description: 'Code snippet', icon: CodeXml, category: 'Basic blocks', shortcut: '```', command: (e) => e.chain().focus().toggleCodeBlock().run() },
{ title: 'Divider', description: 'Visually divide blocks', icon: Minus, category: 'Basic blocks', command: (e) => e.chain().focus().setHorizontalRule().run() }, { title: 'Divider', description: 'Horizontal separator', icon: Minus, category: 'Basic blocks', shortcut: '---', command: (e) => e.chain().focus().setHorizontalRule().run() },
{ title: 'Image', description: 'Embed an image from URL', icon: ImageIcon, category: 'Media', isImage: true, command: () => {} }, { title: 'Image', description: 'Embed image from URL', icon: ImageIcon, category: 'Media', isImage: true, command: () => {} },
{ title: 'Clarify', description: 'Make text clearer and easier to understand', icon: Lightbulb, category: 'AI', isAi: true, aiOption: 'clarify', command: () => {} }, { title: 'Align Left', description: 'Align text left', icon: AlignLeft, category: 'Formatting', command: (e) => e.chain().focus().setTextAlign('left').run() },
{ title: 'Shorten', description: 'Make text more concise', icon: Scissors, category: 'AI', isAi: true, aiOption: 'shorten', command: () => {} }, { title: 'Align Center', description: 'Center text', icon: AlignCenter, category: 'Formatting', command: (e) => e.chain().focus().setTextAlign('center').run() },
{ title: 'Improve', description: 'Improve writing style and flow', icon: Wand2, category: 'AI', isAi: true, aiOption: 'improve', command: () => {} }, { title: 'Align Right', description: 'Align text right', icon: AlignRight, category: 'Formatting', command: (e) => e.chain().focus().setTextAlign('right').run() },
{ title: 'Superscript', description: 'Raise text above baseline', icon: SuperscriptIcon, category: 'Formatting', command: (e) => e.chain().focus().toggleSuperscript().run() },
{ title: 'Subscript', description: 'Lower text below baseline', icon: SubscriptIcon, category: 'Formatting', command: (e) => e.chain().focus().toggleSubscript().run() },
{ 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: () => {} },
] ]
async function aiReformulate(text: string, option: 'clarify' | 'shorten' | 'improve'): Promise<string> { async function aiReformulate(text: string, option: 'clarify' | 'shorten' | 'improve'): Promise<string> {
@@ -417,30 +425,44 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
return ( return (
<div ref={menuRef} className="notion-slash-menu" style={{ top: coords.top, left: coords.left }} onClick={(e) => e.stopPropagation()}> <div ref={menuRef} className="notion-slash-menu" style={{ top: coords.top, left: coords.left }} onClick={(e) => e.stopPropagation()}>
{/* Header hint */}
<div className="notion-slash-header">
<Sparkles className="w-3 h-3" />
<span>Blocs utilisez pour naviguer, Entrée pour insérer</span>
</div>
{aiLoading && ( {aiLoading && (
<div className="notion-slash-loading"> <div className="notion-slash-loading">
<Sparkles className="w-3.5 h-3.5 animate-spin" /> <Sparkles className="w-3.5 h-3.5 animate-spin" />
<span>AI is thinking...</span> <span>IA Note réfléchit...</span>
</div> </div>
)} )}
{!aiLoading && Object.entries(categories).map(([cat, items]) => ( {!aiLoading && Object.entries(categories).map(([cat, items]) => (
<div key={cat} className="notion-slash-section"> <div key={cat} className="notion-slash-section">
<div className="notion-slash-label">{cat}</div> <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}
</div>
{items.map((item) => { {items.map((item) => {
flatIndex++ flatIndex++
const idx = flatIndex const idx = flatIndex
const isSelected = idx === selectedIndex
return ( return (
<button <button
key={item.title} key={item.title}
className={cn('notion-slash-item', idx === selectedIndex && 'notion-slash-item-selected')} className={cn('notion-slash-item', isSelected && 'notion-slash-item-selected')}
onClick={() => handleSelect(item)} onClick={() => handleSelect(item)}
onMouseEnter={() => setSelectedIndex(idx)} onMouseEnter={() => setSelectedIndex(idx)}
> >
<div className={cn('notion-slash-icon', item.isAi && 'notion-slash-icon-ai')}> <div className={cn('notion-slash-icon', item.isAi && 'notion-slash-icon-ai')}>
<item.icon className="w-3 h-3" /> <item.icon className="w-4 h-4" />
</div> </div>
<span className="notion-slash-title">{item.title}</span> <div className="notion-slash-content">
<span className="notion-slash-desc">{item.description}</span> <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> </button>
) )
})} })}