npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-ui

Quick 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.