mirror of
https://github.com/Mail-0/Zero.git
synced 2026-06-28 23:06:54 +00:00
# READ CAREFULLY THEN REMOVE Remove bullet points that are not relevant. PLEASE REFRAIN FROM USING AI TO WRITE YOUR CODE AND PR DESCRIPTION. IF YOU DO USE AI TO WRITE YOUR CODE PLEASE PROVIDE A DESCRIPTION AND REVIEW IT CAREFULLY. MAKE SURE YOU UNDERSTAND THE CODE YOU ARE SUBMITTING USING AI. - Pull requests that do not follow these guidelines will be closed without review or comment. - If you use AI to write your PR description your pr will be close without review or comment. - If you are unsure about anything, feel free to ask for clarification. ## Description Please provide a clear description of your changes. --- ## Type of Change Please delete options that are not relevant. - [ ] 🐛 Bug fix (non-breaking change which fixes an issue) - [ ] ✨ New feature (non-breaking change which adds functionality) - [ ] 💥 Breaking change (fix or feature with breaking changes) - [ ] 📝 Documentation update - [ ] 🎨 UI/UX improvement - [ ] 🔒 Security enhancement - [ ] ⚡ Performance improvement ## Areas Affected Please check all that apply: - [ ] Email Integration (Gmail, IMAP, etc.) - [ ] User Interface/Experience - [ ] Authentication/Authorization - [ ] Data Storage/Management - [ ] API Endpoints - [ ] Documentation - [ ] Testing Infrastructure - [ ] Development Workflow - [ ] Deployment/Infrastructure ## Testing Done Describe the tests you've done: - [ ] Unit tests added/updated - [ ] Integration tests added/updated - [ ] Manual testing performed - [ ] Cross-browser testing (if UI changes) - [ ] Mobile responsiveness verified (if UI changes) ## Security Considerations For changes involving data or authentication: - [ ] No sensitive data is exposed - [ ] Authentication checks are in place - [ ] Input validation is implemented - [ ] Rate limiting is considered (if applicable) ## Checklist - [ ] I have read the [CONTRIBUTING](https://github.com/Mail-0/Zero/blob/staging/.github/CONTRIBUTING.md) document - [ ] My code follows the project's style guidelines - [ ] I have performed a self-review of my code - [ ] I have commented my code, particularly in complex areas - [ ] I have updated the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix/feature works - [ ] All tests pass locally - [ ] Any dependent changes are merged and published ## Additional Notes Add any other context about the pull request here. ## Screenshots/Recordings Add screenshots or recordings here if applicable. --- _By submitting this pull request, I confirm that my contribution is made under the terms of the project's license._ <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced a new localization system replacing translation hooks with direct message function calls. * Added localization project configuration and Inlang plugins for improved message formatting. * **Bug Fixes** * Enhanced pluralization handling across all supported languages for accurate message display. * **Refactor** * Unified translation and locale management across components by removing hook-based translations. * Removed obsolete navigation and email signature settings pages. * Simplified query and server provider logic, removing connection-specific and internationalization context code. * Disabled server-side rendering in the mail app. * Removed Cloudflare Worker request handler and related environment augmentations. * **Chores** * Updated dependencies and scripts, adding Inlang CLI and removing unused packages. * Cleaned up configuration files including Vite, Wrangler, and i18n settings. * Added `.gitignore` for localization cache. * Updated static asset handling and environment configurations for Cloudflare deployments. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
610 lines
20 KiB
TypeScript
610 lines
20 KiB
TypeScript
import {
|
|
Bold,
|
|
Italic,
|
|
Strikethrough,
|
|
Underline,
|
|
Code,
|
|
Link as LinkIcon,
|
|
List,
|
|
ListOrdered,
|
|
Heading1,
|
|
Heading2,
|
|
Heading3,
|
|
Paperclip,
|
|
Plus,
|
|
} from 'lucide-react';
|
|
import {
|
|
EditorCommand,
|
|
EditorCommandEmpty,
|
|
EditorCommandItem,
|
|
EditorCommandList,
|
|
EditorContent,
|
|
EditorRoot,
|
|
useEditor,
|
|
type JSONContent,
|
|
} from 'novel';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
import { useEditor as useEditorContext } from '@/components/providers/editor-provider';
|
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
import { Editor as TiptapEditor, useCurrentEditor } from '@tiptap/react';
|
|
import { suggestionItems } from '@/components/create/slash-command';
|
|
import { defaultExtensions } from '@/components/create/extensions';
|
|
import { ImageResizer, handleCommandNavigation } from 'novel';
|
|
import { handleImageDrop, handleImagePaste } from 'novel';
|
|
import EditorMenu from '@/components/create/editor-menu';
|
|
import { UploadedFileIcon } from './uploaded-file-icon';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import { useReducer, useRef, useEffect } from 'react';
|
|
import { AutoComplete } from './editor-autocomplete';
|
|
import { Editor as CoreEditor } from '@tiptap/core';
|
|
import { cn, truncateFileName } from '@/lib/utils';
|
|
import { TextSelection } from 'prosemirror-state';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { EditorView } from 'prosemirror-view';
|
|
import { Markdown } from 'tiptap-markdown';
|
|
import { Slice } from 'prosemirror-model';
|
|
import { m } from '@/paraglide/messages';
|
|
import { useState } from 'react';
|
|
import React from 'react';
|
|
|
|
export const defaultEditorContent = {
|
|
type: 'doc',
|
|
content: [
|
|
{
|
|
type: 'paragraph',
|
|
content: [],
|
|
},
|
|
],
|
|
};
|
|
|
|
interface EditorProps {
|
|
initialValue?: JSONContent;
|
|
onChange: (content: string) => void;
|
|
placeholder?: string;
|
|
onFocus?: () => void;
|
|
onBlur?: () => void;
|
|
className?: string;
|
|
onCommandEnter?: () => void;
|
|
onAttachmentsChange?: (attachments: File[]) => void;
|
|
myInfo?: {
|
|
name?: string;
|
|
email?: string;
|
|
};
|
|
senderInfo?: {
|
|
name?: string;
|
|
email?: string;
|
|
};
|
|
onTab?: () => boolean;
|
|
onEditorReady?: (editor: TiptapEditor) => void;
|
|
includeSignature?: boolean;
|
|
onSignatureToggle?: (include: boolean) => void;
|
|
signature?: string;
|
|
hasSignature?: boolean;
|
|
readOnly?: boolean;
|
|
hideToolbar?: boolean;
|
|
}
|
|
|
|
interface EditorState {
|
|
openNode: boolean;
|
|
openColor: boolean;
|
|
openLink: boolean;
|
|
openAI: boolean;
|
|
}
|
|
|
|
type EditorAction =
|
|
| { type: 'TOGGLE_NODE'; payload: boolean }
|
|
| { type: 'TOGGLE_COLOR'; payload: boolean }
|
|
| { type: 'TOGGLE_LINK'; payload: boolean }
|
|
| { type: 'TOGGLE_AI'; payload: boolean };
|
|
|
|
function editorReducer(state: EditorState, action: EditorAction): EditorState {
|
|
switch (action.type) {
|
|
case 'TOGGLE_NODE':
|
|
return { ...state, openNode: action.payload };
|
|
case 'TOGGLE_COLOR':
|
|
return { ...state, openColor: action.payload };
|
|
case 'TOGGLE_LINK':
|
|
return { ...state, openLink: action.payload };
|
|
case 'TOGGLE_AI':
|
|
return { ...state, openAI: action.payload };
|
|
default:
|
|
return state;
|
|
}
|
|
}
|
|
|
|
// Update the MenuBar component with icons
|
|
interface MenuBarProps {
|
|
onAttachmentsChange?: (attachments: File[]) => void;
|
|
includeSignature?: boolean;
|
|
onSignatureToggle?: (include: boolean) => void;
|
|
hasSignature?: boolean;
|
|
}
|
|
|
|
const MenuBar = () => {
|
|
const { editor } = useCurrentEditor();
|
|
|
|
const [linkDialogOpen, setLinkDialogOpen] = useState(false);
|
|
const [linkUrl, setLinkUrl] = useState('');
|
|
|
|
if (!editor) {
|
|
return null;
|
|
}
|
|
|
|
// Replace the old setLink function with this new implementation
|
|
const handleLinkDialogOpen = () => {
|
|
// If a link is already active, pre-fill the input with the current URL
|
|
if (editor.isActive('link')) {
|
|
const attrs = editor.getAttributes('link');
|
|
setLinkUrl(attrs.href || '');
|
|
} else {
|
|
setLinkUrl('');
|
|
}
|
|
setLinkDialogOpen(true);
|
|
};
|
|
|
|
const handleSaveLink = () => {
|
|
// empty
|
|
if (linkUrl === '') {
|
|
editor.chain().focus().unsetLink().run();
|
|
} else {
|
|
// Format the URL with proper protocol if missing
|
|
let formattedUrl = linkUrl;
|
|
if (formattedUrl && !/^https?:\/\//i.test(formattedUrl)) {
|
|
formattedUrl = `https://${formattedUrl}`;
|
|
}
|
|
// set link
|
|
editor.chain().focus().setLink({ href: formattedUrl }).run();
|
|
}
|
|
setLinkDialogOpen(false);
|
|
};
|
|
|
|
const handleRemoveLink = () => {
|
|
editor.chain().focus().unsetLink().run();
|
|
setLinkDialogOpen(false);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<TooltipProvider>
|
|
<div className="control-group mb-2 overflow-x-auto">
|
|
<div className="button-group ml-0 mt-1 flex flex-wrap gap-1 border-b pb-2">
|
|
<div className="mr-2 flex items-center gap-1">
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
type="button"
|
|
tabIndex={-1}
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => editor.chain().focus().toggleBold().run()}
|
|
disabled={!editor.can().chain().focus().toggleBold().run()}
|
|
className={`h-auto w-auto rounded p-1.5 ${editor.isActive('bold') ? 'bg-muted' : 'bg-background'}`}
|
|
title="Bold"
|
|
>
|
|
<Bold className="h-4 w-4" />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>{m.pages.createEmail.editor.menuBar.bold()}</TooltipContent>
|
|
</Tooltip>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
type="button"
|
|
tabIndex={-1}
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => editor.chain().focus().toggleItalic().run()}
|
|
disabled={!editor.can().chain().focus().toggleItalic().run()}
|
|
className={`h-auto w-auto rounded p-1.5 ${editor.isActive('italic') ? 'bg-muted' : 'bg-background'}`}
|
|
>
|
|
<Italic className="h-4 w-4" />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>{m.pages.createEmail.editor.menuBar.italic()}</TooltipContent>
|
|
</Tooltip>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
type="button"
|
|
tabIndex={-1}
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => editor.chain().focus().toggleStrike().run()}
|
|
disabled={!editor.can().chain().focus().toggleStrike().run()}
|
|
className={`h-auto w-auto rounded p-1.5 ${editor.isActive('strike') ? 'bg-muted' : 'bg-background'}`}
|
|
>
|
|
<Strikethrough className="h-4 w-4" />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
{m.pages.createEmail.editor.menuBar.strikethrough()}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
type="button"
|
|
tabIndex={-1}
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
|
className={`h-auto w-auto rounded p-1.5 ${editor.isActive('underline') ? 'bg-muted' : 'bg-background'}`}
|
|
>
|
|
<Underline className="h-4 w-4" />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>{m.pages.createEmail.editor.menuBar.underline()}</TooltipContent>
|
|
</Tooltip>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
type="button"
|
|
tabIndex={-1}
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={handleLinkDialogOpen}
|
|
className={`h-auto w-auto rounded p-1.5 ${editor.isActive('link') ? 'bg-muted' : 'bg-background'}`}
|
|
>
|
|
<LinkIcon className="h-4 w-4" />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>{m.pages.createEmail.editor.menuBar.link()}</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
|
|
<Separator orientation="vertical" className="relative right-1 top-0.5 h-6" />
|
|
|
|
<div className="flex items-center gap-1">
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
type="button"
|
|
tabIndex={-1}
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
|
className={`h-auto w-auto rounded p-1.5 ${editor.isActive('bulletList') ? 'bg-muted' : 'bg-background'}`}
|
|
>
|
|
<List className="h-4 w-4" />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>{m.pages.createEmail.editor.menuBar.bulletList()}</TooltipContent>
|
|
</Tooltip>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
type="button"
|
|
tabIndex={-1}
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
|
className={`h-auto w-auto rounded p-1.5 ${editor.isActive('orderedList') ? 'bg-muted' : 'bg-background'}`}
|
|
>
|
|
<ListOrdered className="h-4 w-4" />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>{m.pages.createEmail.editor.menuBar.orderedList()}</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</TooltipProvider>
|
|
|
|
<Dialog open={linkDialogOpen} onOpenChange={setLinkDialogOpen}>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>{m.pages.createEmail.addLink()}</DialogTitle>
|
|
<DialogDescription>{m.pages.createEmail.addUrlToCreateALink()}</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="flex flex-col gap-4 py-2">
|
|
<div className="flex flex-col gap-2">
|
|
<label htmlFor="url" className="text-sm font-medium">
|
|
URL
|
|
</label>
|
|
<Input
|
|
id="url"
|
|
value={linkUrl}
|
|
onChange={(e) => setLinkUrl(e.target.value)}
|
|
placeholder="https://example.com"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<DialogFooter className="flex justify-between sm:justify-between">
|
|
<Button variant="outline" onClick={handleRemoveLink} type="button">
|
|
{m.common.actions.cancel()}
|
|
</Button>
|
|
<Button onClick={handleSaveLink} type="button">
|
|
{m.common.actions.save()}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default function Editor({
|
|
initialValue,
|
|
onChange,
|
|
placeholder = 'Start your email here',
|
|
onFocus,
|
|
onBlur,
|
|
className,
|
|
onCommandEnter,
|
|
onTab,
|
|
onAttachmentsChange,
|
|
senderInfo,
|
|
myInfo,
|
|
readOnly,
|
|
hideToolbar,
|
|
}: EditorProps) {
|
|
const [state, dispatch] = useReducer(editorReducer, {
|
|
openNode: false,
|
|
openColor: false,
|
|
openLink: false,
|
|
openAI: false,
|
|
});
|
|
|
|
const contentRef = useRef<string>('');
|
|
const [editor, setEditor] = useState<TiptapEditor | null>(null);
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
const { openNode, openColor, openLink, openAI } = state;
|
|
|
|
// Function to focus the editor
|
|
const focusEditor = () => {
|
|
if (editor && !readOnly) {
|
|
editor.commands.focus('end');
|
|
}
|
|
};
|
|
|
|
// Function to clear editor content
|
|
const clearEditorContent = React.useCallback(() => {
|
|
if (editor) {
|
|
editor.commands.clearContent(true);
|
|
// Also update our reference and notify parent
|
|
contentRef.current = '';
|
|
onChange('');
|
|
}
|
|
}, [editor, onChange]);
|
|
|
|
// Reset editor content when initialValue changes
|
|
React.useEffect(() => {
|
|
// We need to make sure both the editor reference exists AND initialValue is provided
|
|
if (editor && initialValue) {
|
|
try {
|
|
// Make sure the editor is ready before setting content
|
|
setTimeout(() => {
|
|
// Double-check that the editor still exists in case of unmounting
|
|
if (editor?.commands?.setContent) {
|
|
editor.commands.setContent(initialValue);
|
|
|
|
// Important: after setting content, manually trigger an update
|
|
// to ensure the parent component gets the latest content
|
|
const html = editor.getHTML();
|
|
contentRef.current = html;
|
|
onChange(html);
|
|
}
|
|
}, 0);
|
|
} catch (error) {
|
|
console.error('Error setting editor content:', error);
|
|
}
|
|
}
|
|
}, [initialValue, editor, onChange]);
|
|
|
|
// Handle command+enter or ctrl+enter
|
|
const handleCommandEnter = React.useCallback(() => {
|
|
// Call the parent's onCommandEnter
|
|
onCommandEnter?.();
|
|
|
|
// Clear the editor content after sending
|
|
setTimeout(() => {
|
|
if (editor?.commands?.clearContent) {
|
|
clearEditorContent();
|
|
}
|
|
}, 200);
|
|
}, [onCommandEnter, clearEditorContent, editor]);
|
|
|
|
return (
|
|
<div
|
|
className={`relative w-full ${className || ''}`}
|
|
onClick={focusEditor}
|
|
onKeyDown={(e) => {
|
|
if (readOnly) return;
|
|
// Handle tab key
|
|
if (e.key === 'Tab' && !e.shiftKey) {
|
|
if (onTab && onTab()) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.stopPropagation();
|
|
}
|
|
|
|
// Handle Command+Enter (Mac) or Ctrl+Enter (Windows/Linux)
|
|
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
handleCommandEnter();
|
|
}
|
|
}}
|
|
>
|
|
<EditorRoot>
|
|
<EditorContent
|
|
immediatelyRender={false}
|
|
initialContent={initialValue || defaultEditorContent}
|
|
extensions={[
|
|
...defaultExtensions,
|
|
Markdown,
|
|
AutoComplete.configure({
|
|
suggestions: {
|
|
openers: [
|
|
'Hi there,',
|
|
'Hello,',
|
|
'Dear',
|
|
'Greetings,',
|
|
'Good morning,',
|
|
'Good afternoon,',
|
|
'Good evening,',
|
|
],
|
|
closers: [
|
|
'Best regards,',
|
|
'Kind regards,',
|
|
'Sincerely,',
|
|
'Thanks,',
|
|
'Thank you,',
|
|
'Cheers,',
|
|
],
|
|
custom: [
|
|
'I hope this email finds you well.',
|
|
'I look forward to hearing from you.',
|
|
'Please let me know if you have any questions.',
|
|
],
|
|
},
|
|
sender: senderInfo,
|
|
myInfo: myInfo,
|
|
}),
|
|
]}
|
|
ref={containerRef}
|
|
className="no-scrollbar relative max-h-[500px] min-h-[220px] cursor-text overflow-auto"
|
|
editorProps={{
|
|
editable: () => !readOnly,
|
|
handleDOMEvents: {
|
|
mousedown: (view, event) => {
|
|
if (readOnly) return false;
|
|
focusEditor();
|
|
const coords = view.posAtCoords({
|
|
left: event.clientX,
|
|
top: event.clientY,
|
|
});
|
|
|
|
if (coords) {
|
|
const pos = coords.pos;
|
|
const tr = view.state.tr;
|
|
const selection = TextSelection.create(view.state.doc, pos);
|
|
tr.setSelection(selection);
|
|
view.dispatch(tr);
|
|
}
|
|
|
|
// Let the default handler also run
|
|
return false;
|
|
},
|
|
keydown: (view, event) => {
|
|
if (readOnly) return false;
|
|
if (event.key === 'Tab' && !event.shiftKey) {
|
|
if (onTab && onTab()) {
|
|
event.preventDefault();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Prevent Command+Enter from adding a new line
|
|
if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
|
|
event.preventDefault();
|
|
return true;
|
|
}
|
|
|
|
return handleCommandNavigation(event);
|
|
},
|
|
focus: () => {
|
|
if (!readOnly) onFocus?.();
|
|
return false;
|
|
},
|
|
blur: () => {
|
|
if (!readOnly) onBlur?.();
|
|
return false;
|
|
},
|
|
},
|
|
handleDrop: (view, event, _slice, moved) => {
|
|
if (readOnly) return false;
|
|
return handleImageDrop(view, event, moved, (file) => {
|
|
onAttachmentsChange?.([file]);
|
|
});
|
|
},
|
|
attributes: {
|
|
class: cn(
|
|
'prose dark:prose-invert prose-headings:font-title focus:outline-none max-w-full min-h-[200px]',
|
|
readOnly && 'pointer-events-none select-text',
|
|
),
|
|
'data-placeholder': placeholder,
|
|
},
|
|
}}
|
|
onCreate={({ editor: ed }) => {
|
|
setEditor(ed);
|
|
}}
|
|
onDestroy={() => {
|
|
setEditor(null);
|
|
}}
|
|
onUpdate={({ editor: ed }) => {
|
|
if (readOnly) return;
|
|
// Store the content in the ref to prevent losing it
|
|
contentRef.current = ed.getHTML();
|
|
onChange(ed.getHTML());
|
|
}}
|
|
slotAfter={null}
|
|
>
|
|
{/* Make sure the command palette doesn't cause a refresh */}
|
|
<EditorCommand
|
|
className="border-muted bg-background z-50 h-auto max-h-[330px] overflow-y-auto rounded-md border px-1 py-2 shadow-md transition-all"
|
|
onKeyDown={(e) => {
|
|
// Prevent form submission on any key that might trigger it
|
|
if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
}}
|
|
>
|
|
{/* Rest of the command palette */}
|
|
<EditorCommandEmpty className="text-muted-foreground px-2">
|
|
No results
|
|
</EditorCommandEmpty>
|
|
<EditorCommandList>
|
|
{suggestionItems.map((item) => (
|
|
<EditorCommandItem
|
|
value={item.title}
|
|
onCommand={(val) => {
|
|
// Prevent default behavior that might cause refresh
|
|
item.command?.(val);
|
|
return false;
|
|
}}
|
|
className="hover:bg-accent aria-selected:bg-accent flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-[10px]"
|
|
key={item.title}
|
|
>
|
|
<div className="border-muted bg-background flex h-8 w-8 items-center justify-center rounded-md border">
|
|
{item.icon}
|
|
</div>
|
|
<div>
|
|
<p className="text-xs font-medium">{item.title}</p>
|
|
<p className="text-muted-foreground text-[8px]">{item.description}</p>
|
|
</div>
|
|
</EditorCommandItem>
|
|
))}
|
|
</EditorCommandList>
|
|
</EditorCommand>
|
|
|
|
{/* Replace the default editor menu with just our TextButtons */}
|
|
<EditorMenu
|
|
open={openAI}
|
|
onOpenChange={(open) => dispatch({ type: 'TOGGLE_AI', payload: open })}
|
|
>
|
|
{/* Empty children to satisfy the type requirement */}
|
|
<div></div>
|
|
</EditorMenu>
|
|
</EditorContent>
|
|
</EditorRoot>
|
|
</div>
|
|
);
|
|
}
|