JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 2
  • Score
    100M100P100Q33330F
  • License MIT

A reusable React library for color palette context menu with column-based color application

Package Exports

  • nachtify

Readme

Nachtify

A production-ready React library for adding interactive color palette context menus to data tables and displays. Nachtify provides column-based and group-based color application with automatic localStorage persistence.

Features

  • Context menu with integrated color palette triggered by right-click
  • Column-based color application for individual cells
  • Group-based color application for row categories
  • Automatic localStorage persistence across sessions
  • 11 predefined theme colors with circular swatch UI
  • White circle as clear/reset button to remove colors
  • Scrollable color palette with arrow navigation
  • Fully dynamic and customizable menu items
  • JSON-based color scheme export and import
  • No CSS dependencies - works out of the box
  • Compact, inline styling - no external CSS required

Installation

npm install nachtify

Or with yarn:

yarn add nachtify

Quick Start

1. Import the component and library

import {
  ColorPaletteContextMenu,
  THEME_COLORS,
  loadColors,
  saveColors,
  applyColumnColor,
  getColumnColor,
  applyGroupColor,
  getGroupColor
} from 'nachtify';

2. Set up state

import { useState, useEffect } from 'react';

export default function MyTable() {
  const [colors, setColors] = useState(() => loadColors());
  const [contextMenu, setContextMenu] = useState({ visible: false, x: 0, y: 0, row: null, column: null });
  const [colorScrollIndex, setColorScrollIndex] = useState(0);

  // Auto-save colors to localStorage
  useEffect(() => {
    saveColors(colors);
  }, [colors]);

  const handleRowContextMenu = (e, rowId, column) => {
    e.preventDefault();
    setContextMenu({
      visible: true,
      x: e.clientX,
      y: e.clientY,
      row: rowId,
      column: column
    });
  };

  const closeContextMenu = () => {
    setContextMenu({ visible: false, x: 0, y: 0, row: null, column: null });
  };

  return (
    <div>
      <table>
        <tbody>
          {data.map(row => (
            <tr
              key={row.id}
              onContextMenu={(e) => handleRowContextMenu(e, row.id, 'name')}
              style={{ backgroundColor: getGroupColor(colors, `group-${row.group}`) }}
            >
              <td style={{ backgroundColor: getColumnColor(colors, row.id, 'name') }}>
                {row.name}
              </td>
            </tr>
          ))}
        </tbody>
      </table>

      <ColorPaletteContextMenu
        visible={contextMenu.visible}
        x={contextMenu.x}
        y={contextMenu.y}
        colorScrollIndex={colorScrollIndex}
        onColorScrollChange={setColorScrollIndex}
        onColorSelect={(color) => {
          setColors(prev => applyColumnColor(prev, contextMenu.row, contextMenu.column, color));
          closeContextMenu();
        }}
        onClose={closeContextMenu}
        menuItems={[
          {
            label: 'Edit',
            icon: <Edit3 size={16} />,
            onClick: () => console.log('Edit'),
            hasBorder: true
          },
          {
            label: 'Delete',
            icon: <Trash2 size={16} />,
            onClick: () => console.log('Delete'),
            isDanger: true
          }
        ]}
      />
    </div>
  );
}

API Reference

Components

ColorPaletteContextMenu

A context menu component with integrated color palette.

Props:

  • visible (boolean) - Show/hide the menu
  • x (number) - X position
  • y (number) - Y position
  • colorScrollIndex (number) - Current scroll position in color palette
  • onColorScrollChange (function) - Called when scrolling colors
  • onColorSelect (function) - Called when a color is selected
  • onClose (function) - Called when menu should close
  • menuItems (array) - Custom menu items
  • showColorPalette (boolean) - Show/hide color palette (default: true)
  • title (string) - Color palette section title (default: 'Theme Color')

Menu Item Structure:

{
  label: 'Item Label',
  icon: <IconComponent />,
  onClick: () => {},
  hasBorder: true,      // Show border below item
  isDanger: false       // Red color for dangerous actions
}

Dynamic Menu Items

The menuItems prop accepts an array of menu items that you define. This gives you complete control over what actions appear in the context menu.

Example: Basic Menu

menuItems={[
  { label: 'Edit', onClick: () => handleEdit() },
  { label: 'Delete', onClick: () => handleDelete(), isDanger: true }
]}

Example: With Icons (using lucide-react)

import { Edit3, Trash2, ExternalLink, Copy } from 'lucide-react';

menuItems={[
  { label: 'Edit', icon: <Edit3 size={16} />, onClick: () => handleEdit() },
  { label: 'Delete', icon: <Trash2 size={16} />, onClick: () => handleDelete(), isDanger: true },
  { label: 'Open in New Tab', icon: <ExternalLink size={16} />, onClick: () => window.open(url, '_blank') },
  { label: 'Copy', icon: <Copy size={16} />, onClick: () => navigator.clipboard.writeText(text) }
]}

Example: Conditional Menu Items

const getMenuItems = (row) => {
  const items = [
    { label: 'View Details', onClick: () => viewDetails(row.id) }
  ];
  
  if (row.canEdit) {
    items.push({ label: 'Edit', onClick: () => editRow(row.id) });
  }
  
  if (row.canDelete) {
    items.push({ label: 'Delete', onClick: () => deleteRow(row.id), isDanger: true });
  }
  
  return items;
};

<ColorPaletteContextMenu
  menuItems={getMenuItems(selectedRow)}
  // ... other props
/>

Example: Color Palette Only (No Menu Items)

<ColorPaletteContextMenu
  visible={contextMenu.visible}
  x={contextMenu.x}
  y={contextMenu.y}
  colorScrollIndex={colorScrollIndex}
  onColorScrollChange={setColorScrollIndex}
  onColorSelect={handleColorSelect}
  onClose={closeContextMenu}
  menuItems={[]}  // Empty array = no menu items
/>

Example: Menu Items Only (No Color Palette)

<ColorPaletteContextMenu
  visible={contextMenu.visible}
  x={contextMenu.x}
  y={contextMenu.y}
  showColorPalette={false}  // Hide color palette
  onClose={closeContextMenu}
  menuItems={[
    { label: 'Edit', onClick: handleEdit },
    { label: 'Delete', onClick: handleDelete, isDanger: true }
  ]}
/>

Library Functions

loadColors()

Load colors from localStorage.

const colors = loadColors();

saveColors(colors)

Save colors to localStorage.

saveColors(colors);

clearColors()

Clear all colors from localStorage.

clearColors();

applyColumnColor(colors, rowId, column, color)

Apply color to a specific row and column.

const updated = applyColumnColor(colors, 1, 'name', THEME_COLORS[2]);

getColumnColor(colors, rowId, column, fallbackColor)

Get color for a specific row and column.

const color = getColumnColor(colors, 1, 'name', 'transparent');

applyGroupColor(colors, groupKey, color)

Apply color to all rows in a group.

const updated = applyGroupColor(colors, 'gender-Male', THEME_COLORS[2]);

getGroupColor(colors, groupKey)

Get color for a group.

const color = getGroupColor(colors, 'gender-Male');

exportColorsToFile(colors, filename)

Export colors to a JSON file.

exportColorsToFile(colors, 'my-colors.json');

importColorsFromFile(file)

Import colors from a JSON file.

const colors = await importColorsFromFile(fileInput.files[0]);

Constants

THEME_COLORS

Array of 11 predefined colors:

[
  { name: 'Default', value: 'default', primary: '#FFFFFF', ... },
  { name: 'Indigo', value: 'indigo', primary: '#4F46E5', ... },
  { name: 'Blue', value: 'blue', primary: '#3B82F6', ... },
  { name: 'Purple', value: 'purple', primary: '#9333EA', ... },
  { name: 'Pink', value: 'pink', primary: '#EC4899', ... },
  { name: 'Red', value: 'red', primary: '#EF4444', ... },
  { name: 'Orange', value: 'orange', primary: '#F97316', ... },
  { name: 'Amber', value: 'amber', primary: '#F59E0B', ... },
  { name: 'Green', value: 'green', primary: '#10B981', ... },
  { name: 'Teal', value: 'teal', primary: '#14B8A6', ... },
  { name: 'Cyan', value: 'cyan', primary: '#06B6D4', ... }
]

localStorage Structure

Colors are stored under the key colorPaletteData:

{
  "1-name": "#4F46E5",
  "2-name": "#3B82F6",
  "gender-Male": "#10B981",
  "gender-Female": "#EC4899"
}

Example: Complete Table with Color Palette

import { useState, useEffect } from 'react';
import { Edit3, Trash2 } from 'lucide-react';
import {
  ColorPaletteContextMenu,
  loadColors,
  saveColors,
  applyColumnColor,
  getColumnColor,
  applyGroupColor,
  getGroupColor
} from 'nachtify';

export default function DataTable() {
  const [colors, setColors] = useState(() => loadColors());
  const [contextMenu, setContextMenu] = useState({ visible: false, x: 0, y: 0, row: null, column: null });
  const [colorScrollIndex, setColorScrollIndex] = useState(0);

  const data = [
    { id: 1, name: 'John Doe', gender: 'Male', department: 'Sales' },
    { id: 2, name: 'Jane Smith', gender: 'Female', department: 'Marketing' },
    { id: 3, name: 'Bob Johnson', gender: 'Male', department: 'IT' }
  ];

  useEffect(() => {
    saveColors(colors);
  }, [colors]);

  const handleContextMenu = (e, rowId, column) => {
    e.preventDefault();
    setContextMenu({
      visible: true,
      x: e.clientX,
      y: e.clientY,
      row: rowId,
      column: column
    });
  };

  const closeContextMenu = () => {
    setContextMenu({ visible: false, x: 0, y: 0, row: null, column: null });
  };

  return (
    <div className="p-8">
      <table className="w-full border-collapse">
        <thead>
          <tr className="bg-gray-100">
            <th className="px-4 py-2 text-left">Name</th>
            <th className="px-4 py-2 text-left">Gender</th>
            <th className="px-4 py-2 text-left">Department</th>
          </tr>
        </thead>
        <tbody>
          {data.map(row => (
            <tr
              key={row.id}
              style={{ backgroundColor: getGroupColor(colors, `gender-${row.gender}`) }}
            >
              <td
                className="px-4 py-2 cursor-context-menu"
                onContextMenu={(e) => handleContextMenu(e, row.id, 'name')}
                style={{ backgroundColor: getColumnColor(colors, row.id, 'name') }}
              >
                {row.name}
              </td>
              <td className="px-4 py-2">{row.gender}</td>
              <td className="px-4 py-2">{row.department}</td>
            </tr>
          ))}
        </tbody>
      </table>

      <ColorPaletteContextMenu
        visible={contextMenu.visible}
        x={contextMenu.x}
        y={contextMenu.y}
        colorScrollIndex={colorScrollIndex}
        onColorScrollChange={setColorScrollIndex}
        onColorSelect={(color) => {
          setColors(prev => applyColumnColor(prev, contextMenu.row, contextMenu.column, color));
          closeContextMenu();
        }}
        onClose={closeContextMenu}
        menuItems={[
          {
            label: 'Edit',
            icon: <Edit3 size={16} />,
            onClick: () => alert('Edit clicked'),
            hasBorder: true
          },
          {
            label: 'Delete',
            icon: <Trash2 size={16} />,
            onClick: () => alert('Delete clicked'),
            isDanger: true
          }
        ]}
      />
    </div>
  );
}

How It Works

  1. Right-click on a table cell to open the context menu with color palette
  2. Click the white circle to clear/reset the cell color
  3. Click any colored circle to apply that color to the cell or group
  4. Use arrow buttons to scroll through more color options
  5. Custom menu items (Edit, Delete, etc.) appear below the color palette
  6. Colors are automatically persisted to localStorage
  7. On page refresh, previously saved colors are restored

Styling

The component uses inline styles and requires no external CSS framework. It works out of the box with any React project.

Browser Support

  • Chrome/Edge 90+
  • Firefox 88+
  • Safari 14+

License

MIT

Contributing

Contributions are welcome. Please submit pull requests with clear descriptions of changes and ensure all tests pass.

Support

For issues, feature requests, or questions, please visit the project repository.