@bernierllc/content-editor-ui
v0.1.3
Published
TipTap editor with Tamagui UI components for content management
Readme
@bernierllc/content-editor-ui
TipTap editor with Tamagui UI components for content management.
Overview
This package provides a comprehensive content editor built with TipTap and Tamagui. It offers a rich WYSIWYG editing experience with customizable toolbars, status bars, auto-save functionality, and seamless integration with the content management suite.
Features
- TipTap Integration: Full-featured WYSIWYG editor with rich text capabilities
- Tamagui UI: Beautiful, accessible UI components with consistent theming
- Auto-save Support: Configurable auto-save with visual feedback
- Customizable Toolbar: Flexible toolbar with formatting, media, and custom tools
- Status Bar: Real-time statistics (word count, character count, reading time)
- Media Upload: Drag-and-drop media upload with progress tracking
- Content Type Support: Integration with content type registry
- Keyboard Shortcuts: Built-in shortcuts for common operations
- Theme Support: Light, dark, and auto themes
- Accessibility: Full accessibility support with ARIA labels and keyboard navigation
Installation
npm install @bernierllc/content-editor-uiQuick Start
import React from 'react';
import { ContentEditor } from '@bernierllc/content-editor-ui';
import { TamaguiProvider } from 'tamagui';
import { ContentData } from '@bernierllc/content-autosave-manager';
import { ContentType } from '@bernierllc/content-type-registry';
function MyContentEditor() {
const [content, setContent] = useState<ContentData>({
id: 'content-1',
data: { type: 'doc', content: [] },
metadata: { type: 'text' },
lastModified: new Date(),
version: 1
});
const contentType: ContentType = {
id: 'text',
name: 'Text Content',
schema: {},
editorConfig: { component: null, fields: [] },
storageAdapter: {},
displayRenderer: { component: null },
metadata: { description: 'Plain text content' }
};
const handleAutoSave = (content: ContentData) => {
console.log('Auto-saving content:', content);
};
const handleSave = (content: ContentData, saveType: 'draft' | 'publish') => {
console.log(`Saving as ${saveType}:`, content);
};
const handleChange = (content: ContentData) => {
setContent(content);
};
return (
<TamaguiProvider>
<ContentEditor
content={content}
contentType={contentType}
onAutoSave={handleAutoSave}
onSave={handleSave}
onChange={handleChange}
config={{
placeholder: 'Start writing your content...',
showWordCount: true,
showCharCount: true,
showAutoSaveStatus: true,
theme: 'light'
}}
/>
</TamaguiProvider>
);
}API Reference
ContentEditor
The main editor component.
Props
interface ContentEditorProps {
content: ContentData; // Content data to edit
contentType: ContentType; // Content type configuration
config?: Partial<ContentEditorUIConfig>; // Editor configuration
onAutoSave?: (content: ContentData) => void; // Auto-save callback
onSave?: (content: ContentData, saveType: 'draft' | 'publish') => void; // Save callback
onChange?: (content: ContentData) => void; // Content change callback
onFocus?: () => void; // Focus callback
onBlur?: () => void; // Blur callback
onError?: (error: Error) => void; // Error callback
readOnly?: boolean; // Whether editor is read-only
disabled?: boolean; // Whether editor is disabled
customToolbarItems?: ToolbarItem[]; // Custom toolbar items
customStatusItems?: StatusItem[]; // Custom status bar items
}Configuration Options
interface ContentEditorUIConfig {
enabled: boolean; // Whether editor is enabled
theme: 'light' | 'dark' | 'auto'; // Editor theme
placeholder?: string; // Placeholder text
showWordCount: boolean; // Whether to show word count
showCharCount: boolean; // Whether to show character count
showAutoSaveStatus: boolean; // Whether to show auto-save status
showToolbar: boolean; // Whether to show toolbar
showStatusBar: boolean; // Whether to show status bar
height?: string | number; // Editor height
width?: string | number; // Editor width
className?: string; // Custom CSS classes
style?: React.CSSProperties; // Custom styles
}ContentTypeSelector
Component for selecting content types.
interface ContentTypeSelectorProps {
contentTypes: ContentType[]; // Available content types
selectedType?: ContentType; // Currently selected type
onChange: (contentType: ContentType) => void; // Change callback
disabled?: boolean; // Whether selector is disabled
placeholder?: string; // Custom placeholder
showDescriptions?: boolean; // Whether to show descriptions
showIcons?: boolean; // Whether to show icons
}MediaUploader
Component for uploading media files.
interface MediaUploaderProps {
accept?: string[]; // Accepted file types
maxSize?: number; // Maximum file size in bytes
onUpload: (file: File) => Promise<string>; // Upload callback
onProgress?: (progress: number) => void; // Progress callback
onError?: (error: Error) => void; // Error callback
disabled?: boolean; // Whether uploader is disabled
buttonText?: string; // Upload button text
showDragDrop?: boolean; // Whether to show drag and drop area
}ContentEditorSettings
Component for configuring editor settings.
interface ContentEditorSettingsProps {
settings: Record<string, any>; // Current settings
onChange: (settings: Record<string, any>) => void; // Change callback
isOpen: boolean; // Whether settings panel is open
onClose: () => void; // Close callback
disabled?: boolean; // Whether settings are disabled
}Examples
Basic Editor with Auto-save
import React, { useState, useCallback } from 'react';
import { ContentEditor } from '@bernierllc/content-editor-ui';
import { ContentData } from '@bernierllc/content-autosave-manager';
function AutoSaveEditor() {
const [content, setContent] = useState<ContentData>({
id: 'content-1',
data: { type: 'doc', content: [] },
metadata: { type: 'text' },
lastModified: new Date(),
version: 1
});
const handleAutoSave = useCallback(async (content: ContentData) => {
try {
// Save to server
await fetch('/api/content/autosave', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(content)
});
console.log('Auto-saved successfully');
} catch (error) {
console.error('Auto-save failed:', error);
}
}, []);
const handleSave = useCallback(async (content: ContentData, saveType: 'draft' | 'publish') => {
try {
await fetch('/api/content/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...content, saveType })
});
console.log(`Saved as ${saveType}`);
} catch (error) {
console.error('Save failed:', error);
}
}, []);
return (
<ContentEditor
content={content}
contentType={textContentType}
onAutoSave={handleAutoSave}
onSave={handleSave}
onChange={setContent}
config={{
placeholder: 'Start writing...',
showAutoSaveStatus: true,
showWordCount: true,
showCharCount: true
}}
/>
);
}Editor with Custom Toolbar
import React from 'react';
import { ContentEditor, ToolbarItem } from '@bernierllc/content-editor-ui';
function CustomToolbarEditor() {
const customToolbarItems: ToolbarItem[] = [
{
id: 'custom-button',
type: 'button',
label: 'Custom Action',
icon: '⭐',
tooltip: 'Perform custom action',
onClick: () => {
console.log('Custom action triggered');
}
},
{
id: 'custom-separator',
type: 'separator'
},
{
id: 'custom-component',
type: 'custom',
component: CustomToolbarComponent,
props: { customProp: 'value' }
}
];
return (
<ContentEditor
content={content}
contentType={contentType}
customToolbarItems={customToolbarItems}
config={{
showToolbar: true,
showFormatting: true,
showHeadings: true,
showLists: true,
showLink: true,
showImage: true
}}
/>
);
}
function CustomToolbarComponent({ editor, customProp }: any) {
return (
<Button
onPress={() => {
// Custom toolbar action
editor.chain().focus().insertContent('Custom content').run();
}}
>
<Text>{customProp}</Text>
</Button>
);
}Editor with Media Upload
import React from 'react';
import { ContentEditor, MediaUploader } from '@bernierllc/content-editor-ui';
function MediaEditor() {
const handleMediaUpload = async (file: File): Promise<string> => {
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
return result.url;
};
const handleUploadProgress = (progress: number) => {
console.log(`Upload progress: ${progress}%`);
};
const handleUploadError = (error: Error) => {
console.error('Upload error:', error);
};
return (
<YStack gap="$4">
<ContentEditor
content={content}
contentType={contentType}
config={{
showImage: true,
showToolbar: true
}}
/>
<MediaUploader
accept={['image/*', 'video/*', 'audio/*']}
maxSize={10 * 1024 * 1024} // 10MB
onUpload={handleMediaUpload}
onProgress={handleUploadProgress}
onError={handleUploadError}
showDragDrop={true}
buttonText="Upload Media"
/>
</YStack>
);
}Editor with Content Type Selection
import React, { useState } from 'react';
import { ContentEditor, ContentTypeSelector } from '@bernierllc/content-editor-ui';
function MultiTypeEditor() {
const [selectedType, setSelectedType] = useState(contentTypes[0]);
const [content, setContent] = useState<ContentData>({
id: 'content-1',
data: { type: 'doc', content: [] },
metadata: { type: selectedType.id },
lastModified: new Date(),
version: 1
});
const handleTypeChange = (contentType: ContentType) => {
setSelectedType(contentType);
// Update content metadata
setContent(prev => ({
...prev,
metadata: { ...prev.metadata, type: contentType.id }
}));
};
return (
<YStack gap="$4">
<ContentTypeSelector
contentTypes={contentTypes}
selectedType={selectedType}
onChange={handleTypeChange}
showDescriptions={true}
showIcons={true}
placeholder="Select content type..."
/>
<ContentEditor
content={content}
contentType={selectedType}
onChange={setContent}
config={{
placeholder: `Start writing ${selectedType.name.toLowerCase()}...`,
showWordCount: true,
showCharCount: true
}}
/>
</YStack>
);
}Editor with Settings Panel
import React, { useState } from 'react';
import { ContentEditor, ContentEditorSettings } from '@bernierllc/content-editor-ui';
function ConfigurableEditor() {
const [settings, setSettings] = useState({
showWordCount: true,
showCharCount: true,
showAutoSaveStatus: true,
editorHeight: '400px',
placeholder: 'Start writing...',
theme: 'light'
});
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const handleSettingsChange = (newSettings: Record<string, any>) => {
setSettings(prev => ({ ...prev, ...newSettings }));
};
return (
<YStack gap="$4">
<XStack gap="$2" justifyContent="flex-end">
<Button onPress={() => setIsSettingsOpen(true)}>
<Text>Settings</Text>
</Button>
</XStack>
<ContentEditor
content={content}
contentType={contentType}
config={settings}
onChange={setContent}
/>
<ContentEditorSettings
settings={settings}
onChange={handleSettingsChange}
isOpen={isSettingsOpen}
onClose={() => setIsSettingsOpen(false)}
/>
</YStack>
);
}Editor with Custom Status Bar
import React from 'react';
import { ContentEditor, StatusItem } from '@bernierllc/content-editor-ui';
function CustomStatusEditor() {
const customStatusItems: StatusItem[] = [
{
id: 'custom-stat',
type: 'text',
label: 'Custom Stat',
value: '42',
color: '#007bff'
},
{
id: 'custom-progress',
type: 'progress',
label: 'Progress',
value: 75
},
{
id: 'custom-component',
type: 'custom',
component: CustomStatusComponent,
props: { data: 'custom data' }
}
];
return (
<ContentEditor
content={content}
contentType={contentType}
customStatusItems={customStatusItems}
config={{
showStatusBar: true,
showWordCount: true,
showCharCount: true,
showAutoSaveStatus: true
}}
/>
);
}
function CustomStatusComponent({ statistics, data }: any) {
return (
<XStack gap="$1" alignItems="center">
<Text fontSize="$1" color="$gray">
{data}:
</Text>
<Text fontSize="$1" fontWeight="bold">
{statistics.wordCount * 2}
</Text>
</XStack>
);
}Editor with Keyboard Shortcuts
import React from 'react';
import { ContentEditor } from '@bernierllc/content-editor-ui';
function KeyboardShortcutsEditor() {
const handleSave = (content: ContentData, saveType: 'draft' | 'publish') => {
console.log(`Saving as ${saveType}:`, content);
};
return (
<ContentEditor
content={content}
contentType={contentType}
onSave={handleSave}
config={{
placeholder: 'Use Ctrl+S to save draft, Ctrl+Shift+S to publish',
showToolbar: true,
showStatusBar: true
}}
/>
);
}Editor with Error Handling
import React, { useState } from 'react';
import { ContentEditor } from '@bernierllc/content-editor-ui';
import { Alert, Toast } from 'tamagui';
function ErrorHandlingEditor() {
const [error, setError] = useState<string | null>(null);
const handleError = (error: Error) => {
setError(error.message);
console.error('Editor error:', error);
};
const handleAutoSaveError = (error: Error) => {
console.error('Auto-save error:', error);
// Show toast notification
Toast.show({
message: 'Auto-save failed. Please save manually.',
type: 'error'
});
};
return (
<YStack gap="$4">
{error && (
<Alert type="error">
<Text>{error}</Text>
</Alert>
)}
<ContentEditor
content={content}
contentType={contentType}
onError={handleError}
onAutoSaveError={handleAutoSaveError}
config={{
showAutoSaveStatus: true,
showStatusBar: true
}}
/>
</YStack>
);
}Keyboard Shortcuts
The editor supports the following keyboard shortcuts:
- Ctrl/Cmd + S: Save as draft
- Ctrl/Cmd + Shift + S: Publish
- Ctrl/Cmd + B: Bold
- Ctrl/Cmd + I: Italic
- Ctrl/Cmd + U: Underline
- Ctrl/Cmd + K: Add link
- Ctrl/Cmd + Shift + K: Remove link
Theming
The editor supports Tamagui theming and can be customized with:
- Light Theme: Default light theme
- Dark Theme: Dark theme for low-light environments
- Auto Theme: Automatically switches based on system preference
Accessibility
The editor is fully accessible with:
- ARIA Labels: All interactive elements have proper labels
- Keyboard Navigation: Full keyboard support
- Screen Reader Support: Compatible with screen readers
- Focus Management: Proper focus handling
- High Contrast Support: Works with high contrast modes
TypeScript Support
This package is written in TypeScript and provides full type definitions. All interfaces and types are exported for use in your own code.
License
UNLICENSED - See LICENSE file for details.
Contributing
Contributions are welcome! Please see the contributing guidelines for more information.
Support
For support and questions, please open an issue on GitHub.
