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 nachtifyOr with yarn:
yarn add nachtifyQuick 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 menux(number) - X positiony(number) - Y positioncolorScrollIndex(number) - Current scroll position in color paletteonColorScrollChange(function) - Called when scrolling colorsonColorSelect(function) - Called when a color is selectedonClose(function) - Called when menu should closemenuItems(array) - Custom menu itemsshowColorPalette(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
- Right-click on a table cell to open the context menu with color palette
- Click the white circle to clear/reset the cell color
- Click any colored circle to apply that color to the cell or group
- Use arrow buttons to scroll through more color options
- Custom menu items (Edit, Delete, etc.) appear below the color palette
- Colors are automatically persisted to localStorage
- 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.