tacel-calendar
v1.9.1
Published
Universal calendar module for Tacel Electron apps. Month, week, day, coming-up, and category views. Canadian statutory holidays (Ontario) with automatic calculation for any year, interactive step CRUD with step completion markers, task-level and per-step
Maintainers
Readme
Tacel Calendar Module
A universal, app-agnostic calendar component for Tacel Electron apps. Supports month, week, day, and coming-up views with drag-and-drop, multi-user filtering, entry detail popups, right-click context menus, permissions, interactive step CRUD, task-level and per-step file attachments with scoped clipboard paste, 34 built-in themes, dashboard header layout preset, cell task/step counters with click-to-list modals, theme dropdown selector, and always-readable pill text. Fully responsive from tiny (~300px) to large displays.
Zero app-specific code — all data fetching, persistence, and app logic is injected via callbacks. Any Electron app can drop this in and have a fully functional calendar in minutes.
Table of Contents
- Quick Start
- Installation
- Configuration
- Views
- Mini Calendar Navigator
- Search / Filter Bar
- Undo Toast (Drag-and-Drop)
- Color Coding Legend
- File Attachments
- Custom Header
- Theming
- Built-in Themes (34)
- Custom Themes
- CSS Variables Reference
- Universal Entry Format
- Callbacks
- Permissions
- Detail Actions
- Context Menu
- Steps / Subtasks
- Auto-Refresh
- Keyboard Shortcuts
- Responsive Design
- Public API
- File Structure
- Adapter Examples
- Test App
Quick Start
const { TacelCalendar } = require('tacel-calendar');
const calendar = TacelCalendar.initialize(document.getElementById('calendar'), {
theme: 'dark', // Pick any of the 34 built-in themes
views: ['month', 'week', 'day'],
defaultView: 'month',
weekStartDay: 1, // Monday
users: [
{ id: 'u1', name: 'Pierre', role: 'Developer' },
{ id: 'u2', name: 'Sarah', role: 'Designer' },
],
currentUserId: 'u1',
features: {
dragDrop: true,
userPicker: 'multi',
addEntry: true,
entryDetail: true,
},
onFetchEntries: async (range, selectedUserIds) => {
const tasks = await myApp.fetchTasks(range.start, range.end, selectedUserIds);
return tasks.map(toCalendarEntry); // Your adapter function
},
onAddEntry: (date) => myApp.openAddTaskModal(date),
onEntryDrop: async (entry, newDate) => myApp.rescheduleTask(entry.id, newDate),
});<link rel="stylesheet" href="./node_modules/tacel-calendar/calendar.css">
<div id="calendar" style="height: 100%;"></div>Installation
npm install tacel-calendarOr reference the module files directly (no bundler needed):
const { TacelCalendar } = require('../path/to/calendar-module/calendar');<link rel="stylesheet" href="../path/to/calendar-module/calendar.css">Configuration
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| views | string[] | ['month','week','day'] | Which views to enable |
| defaultView | string | 'month' | Starting view |
| weekStartDay | number | 1 | 0=Sunday, 1=Monday (ISO) |
| users | object[] | [] | [{ id, name, role? }] — selectable users |
| currentUserId | string | null | Pre-selected user ID |
| theme | string\|object\|null | null | Preset name, CSS variable object, or null for default |
| features | object | see below | Feature toggles |
| detailFields | string[] | null | Which fields to show in the detail popup (null = all) |
| detailActions | fn\|array\|null | null | Detail popup action buttons (see Detail Actions) |
| contextMenuItems | fn\|array\|null | null | Right-click menu items (see Context Menu) |
| permissions | fn\|null | null | Permissions callback (see Permissions) |
| entryFilter | fn\|null | null | (entry) => bool — return false to exclude an entry from display. Applied after fetch and permissions. |
| persistenceKey | string | 'tacel-calendar' | Namespace for localStorage keys. Prevents collisions when multiple apps use the module. |
| autoRefresh | number\|false | 5000 | Silent refresh interval in ms. false or 0 to disable. (see Auto-Refresh) |
| headerRenderer | fn\|null | null | Custom header layout function. (cal, parts) => HTMLElement. See Custom Header. |
Features Object
| Feature | Type | Default | Description |
|---------|------|---------|-------------|
| dragDrop | boolean | false | Enable drag-and-drop rescheduling |
| comingUp | boolean | false | Show "Coming Up" list view |
| userPicker | string\|false | false | 'single', 'multi', or false |
| addEntry | boolean | false | Show FAB (Floating Action Button) |
| entryDetail | boolean | true | Show built-in detail popup on entry click |
| todayButton | boolean | true | Show "Today" navigation button |
| keyboardShortcuts | boolean | true | Enable keyboard shortcuts (see Keyboard Shortcuts) |
| shortcutHelp | boolean | false | Show ? button in header to open shortcut help popup |
| persistTheme | boolean | false | Persist selected theme in localStorage across sessions |
| miniCalendar | boolean | false | Show mini calendar navigator sidebar (month-at-a-glance, click to jump) |
| searchBar | boolean | false | Show search/filter bar in header (real-time entry filtering) |
| undoToast | boolean | true | Show undo toast after drag-and-drop moves (5-second window) |
| colorLegend | boolean | false | Show collapsible color coding legend panel |
| attachments | boolean | false | Show file attachments section in entry detail (requires onGetFiles callback) |
| categoryView | boolean | false | Show Category View — entries grouped by section/category with tab switching and inline filter bar |
| cellCounters | boolean | true | Show task/step count badges in the top-right of each month cell. Clickable — opens a modal listing all tasks or steps. |
| themeDropdown | boolean | true | Show theme dropdown selector in the header. Lists all 34 built-in themes. |
| miniCalendarCollapsed | boolean | true | Start mini calendar sidebar collapsed (only applies when miniCalendar is true) |
Views
Month View
Standard 6×7 grid. Day cells show entry pills with color coding. Overflow entries show a "+N more" button that opens a day popup. Today is highlighted. Other-month days are dimmed.
Week View
Outlook-style 7-column layout with hourly time slots (0:00–23:00, 60px per hour). All-day entries appear in a top banner. Current time indicator (red line) on today's column. Overlapping entries are laid out side-by-side.
Day View
Single-column time grid with a sidebar listing all entries for the day. Same hourly slots as week view. All-day entries in a top banner.
Coming Up View (optional)
Flat sorted list of upcoming entries grouped by date. No grid, no drag-and-drop. Click any entry to see its detail.
Mini Calendar Navigator
A collapsible month-at-a-glance sidebar on the left side of the calendar. Enable with features.miniCalendar: true.
- Click any date to jump directly to it in the main view
- Dot indicators on dates that have entries
- Independent navigation — browse months without affecting the main view
- Collapsible — click the calendar icon to collapse to a thin strip
- Selected date highlighted with accent color
- Today shown in bold accent text
The mini calendar automatically refreshes when entries change or the main view navigates.
Search / Filter Bar
A real-time search input in the header. Enable with features.searchBar: true.
- Instant filtering — type to filter entries by title, description, category, location, or user
- Visual feedback — matching entries get a highlight ring, non-matches are dimmed to 20% opacity
- Clear button — appears when text is entered, click to reset
- Keyboard shortcut — press
/to focus the search bar,Escapeto clear and blur - Debounced — 150ms debounce to avoid excessive DOM updates
Works across all views (month, week, day, upcoming).
Undo Toast (Drag-and-Drop)
After dragging an entry to a new date/time, a toast notification appears at the bottom of the screen. Enabled by default (features.undoToast: true).
You can also undo/redo moves with keyboard shortcuts: Ctrl+Z (undo) and Ctrl+Y (redo). Up to 10 moves are tracked. Performing a new move clears the redo stack.
- 5-second countdown with animated progress bar
- Undo button — reverts the entry to its original date by calling
onEntryDropwith the original date - Hover to pause — countdown pauses while hovering the toast
- Move history — tracks last 10 moves internally
- Smooth animations — slide-in/out transitions
- Theme-aware — uses inverted calendar colors (dark text on light bg, or vice versa)
Color Coding Legend
A collapsible panel on the right side showing entry colors and their categories. Enable with features.colorLegend: true.
- Auto-generated from visible entries — groups by
entry.colorandentry.category - Full section list — if
onGetSectionscallback is provided, the legend merges entry-based sections with all available sections (including custom ones), ensuring the filter shows every section even if no entries currently exist for it - Multi-select filtering — click colors to toggle them on/off; multiple colors can be active at once. Non-matching entries dim to 15% opacity
- Works on all views — month, week, day, and upcoming views all support legend filtering
- Entry counts shown next to each color
- Clear button to reset the filter (appears only when a filter is active)
- Collapsible — click the globe icon to expand/collapse
- Syncs with data — refreshes automatically when entries change
- Responsive — on small screens (≤560px), the legend switches from a vertical sidebar to a horizontal strip below the view with wrapping items
Entries should have a color property (hex string) and optionally a category property (used as the label). If no category is set, the legend auto-generates a label from the color value.
File Attachments
Attach files to any calendar entry. Enable with features.attachments: true. The module handles no file I/O — the app provides all storage logic via callbacks.
Task-Level File Callbacks
| Callback | Signature | Description |
|----------|-----------|-------------|
| onGetFiles | async (entry) => file[] | Fetch files for an entry. Returns [{ id, name, size, type, path?, uploadedAt? }] |
| onUploadFiles | async (entry, files) => { success } | Upload files. files is a FileList from the browser |
| onOpenFile | (entry, file) => void | Open/preview a file (e.g. shell.openPath() in Electron) |
| onDeleteFile | async (entry, file) => { success } | Delete a file |
Step-Level File Callbacks
| Callback | Signature | Description |
|----------|-----------|-------------|
| onGetStepFiles | async (entry, step) => file[] | Fetch files for a specific step. Returns same format as onGetFiles. |
| onUploadStepFiles | async (entry, step, files) => { success } | Upload files to a specific step. |
| onDeleteStepFile | async (entry, step, file) => { success } | Delete a file from a specific step. |
Scoped Clipboard Paste
All file upload dropzones (task-level and per-step) support Ctrl+V screenshot paste. The paste is scoped — only the dropzone the user has clicked or hovered receives the pasted image. A visual indicator (blue border + hint text) shows which dropzone is active. This prevents screenshots from being pasted into every dropzone at once.
UI
The attachments section appears in the entry detail popup (below steps/subtasks):
- File list — each file shows icon (color-coded by type), name, size, upload date
- Action buttons — Open and Delete (appear on hover), gated by permissions
- Upload dropzone — drag-and-drop or click-to-browse, with spinner during upload
- Count badge — shows total file count in the section header
- Empty state — "No files attached" when no files exist
- Error handling — upload failures show a temporary error message
How the App Provides Storage
The module is completely agnostic about where files are stored. The app decides:
TacelCalendar.initialize(container, {
features: { attachments: true },
// App provides the shared network path, DB logic, etc.
onGetFiles: async (entry) => {
return await ipcRenderer.invoke('calendar-files:list', entry.id);
},
onUploadFiles: async (entry, files) => {
const fileData = await Promise.all([...files].map(async f => ({
name: f.name, size: f.size, type: f.type,
data: Buffer.from(await f.arrayBuffer()).toString('base64')
})));
return await ipcRenderer.invoke('calendar-files:upload', entry.id, fileData);
},
onOpenFile: (entry, file) => {
ipcRenderer.invoke('calendar-files:open', file.path);
},
onDeleteFile: async (entry, file) => {
return await ipcRenderer.invoke('calendar-files:delete', entry.id, file.id);
},
});Permissions
- Upload and Delete buttons only appear if
permissions()returnscanEdit: truefor the entry - Open/View is always available if
onOpenFileis provided - Step-level file operations follow the same permission rules
File Type Icons
Color-coded SVG icons for: PDF (red), Word (blue), Excel (green), Images (purple), CSV (green), ZIP (amber), TXT (default). Unknown types get a generic file icon.
Custom Header
By default the module renders a standard header row: title on the left, view toggle in the center, user picker + navigation + help button on the right. Apps can completely replace this layout while still using the module's built-in header pieces.
headerRenderer Config
Pass a function that receives the calendar instance and an object of pre-built DOM elements, and returns your own header element:
TacelCalendar.initialize(container, {
features: { searchBar: true, shortcutHelp: true, userPicker: 'multi' },
headerRenderer: (cal, parts) => {
// parts = { title, viewToggle, nav, userPicker, helpButton, searchBar }
// Each is a ready-to-use DOM element (or null if the feature is disabled).
// Arrange them however you want:
const header = document.createElement('div');
header.className = 'my-app-header';
// Left: nav + title
const left = document.createElement('div');
left.appendChild(parts.nav);
left.appendChild(parts.title);
header.appendChild(left);
// Center: search
if (parts.searchBar) header.appendChild(parts.searchBar);
// Right: view toggle + picker + help + your own custom button
const right = document.createElement('div');
right.appendChild(parts.viewToggle);
if (parts.userPicker) right.appendChild(parts.userPicker);
if (parts.helpButton) right.appendChild(parts.helpButton);
// Add your own app-specific element
const myBtn = document.createElement('button');
myBtn.textContent = 'Export';
myBtn.onclick = () => myApp.exportCalendar();
right.appendChild(myBtn);
header.appendChild(right);
return header;
},
});Available Parts
| Part | Class Name | Description |
|------|------------|-------------|
| parts.title | .tc-header-title | Date title (auto-updates on navigation) |
| parts.viewToggle | .tc-view-toggle | Month/Week/Day/Coming Up buttons |
| parts.nav | .tc-nav | ◀ Today ▶ navigation buttons |
| parts.userPicker | .tc-user-picker | User dropdown (null if userPicker is false) |
| parts.helpButton | .tc-shortcut-help-btn | "?" keyboard shortcuts button (null if shortcutHelp is false) |
| parts.searchBar | .tc-search-bar | Search input (null if searchBar is false) |
| parts.stepMarkersToggle | .tc-step-markers-toggle | Step markers on/off toggle (null if stepMarkers is not 'user') |
| parts.themeDropdown | .tc-theme-dropdown-wrap | Theme selector dropdown (null if themeDropdown is false) |
| parts.pageTitle | .tc-page-title | Page title with calendar icon + text |
| parts.exportButton | .tc-header-action-wrap | Export entries dropdown (CSV/clipboard) |
| parts.statsButton | .tc-header-action-wrap | Task statistics popup |
| parts.refreshButton | .tc-header-action-wrap | Manual refresh button with spin animation |
| parts.popoutButton | .tc-header-action-wrap | Open in new window button (Electron only) |
cal.headerParts
The calendar instance also exposes headerParts — an object of factory functions that create fresh header elements on demand:
const cal = TacelCalendar.initialize(container, { ... });
// Create a standalone nav element at any time
const nav = cal.headerParts.nav();
document.querySelector('.my-sidebar').appendChild(nav);Available factories: title(), viewToggle(), nav(), userPicker(), helpButton(), searchBar(), stepMarkersToggle(), themeDropdown(), pageTitle(), exportButton(), statsButton(), refreshButton(), popoutButton().
Rules
- The returned element gets the
tc-headerclass automatically - The module looks for
.tc-header-title,.tc-nav, and.tc-view-toggleinside your header to auto-update the title text and active view button — make sure you include the parts if you want those updates - You can omit any part you don't want — the module won't break
- You can add any custom elements (buttons, badges, dropdowns) alongside the built-in parts
- Style your header with your own CSS — the module's default header styles only apply to
.tc-headerchildren - If
headerRendererisnull(default), the standard header layout is used
Theming
The calendar is fully themeable. Every color, background, border, shadow, and font is controlled by CSS custom properties. Apps can theme the calendar in three ways:
1. Use a Built-in Preset (Easiest)
TacelCalendar.initialize(container, {
theme: 'dracula' // Any of the 34 preset names
});2. Extend a Preset with Overrides
TacelCalendar.initialize(container, {
theme: {
...TacelCalendar.themes.dark,
'--cal-accent': '#ff6600', // Override just the accent color
'--cal-today-border': '#ff6600',
}
});3. Fully Custom Theme
TacelCalendar.initialize(container, {
theme: {
'--cal-bg': '#1a1a2e',
'--cal-header-bg': '#16213e',
'--cal-border': '#0f3460',
'--cal-text': '#e0e0e0',
'--cal-accent': '#e94560',
// ... set as many or as few variables as you want
// unset variables fall back to the CSS defaults (light theme)
}
});Switching Themes at Runtime
// Switch theme without destroying the calendar
calendar.setTheme('midnight');
// Or destroy and reinitialize
calendar.destroy();
const calendar = TacelCalendar.initialize(container, { ...config, theme: 'midnight' });The setTheme() method cleans up old CSS variables and applies new ones. If persistTheme is enabled, the choice is saved to localStorage.
Theme Dropdown
The module includes a built-in theme dropdown selector. Enable with features.themeDropdown: true (default). It appears in the header and lists all 34 themes. Clicking a theme applies it instantly.
Apps can disable it with features.themeDropdown: false.
Pill Text Readability
Entry pill text uses the --cal-pill-text CSS variable to ensure readability regardless of the entry's color. Light themes default to near-black (#1f2937), dark themes automatically use their light text color. The colored background and left border still show the entry color.
Built-in Themes (34)
All themes are accessible via TacelCalendar.themes or by passing the name string to the theme config option.
Core Themes
| # | Name | Description |
|---|------|-------------|
| 1 | default | Clean white — CSS defaults, no overrides |
| 2 | dark | Catppuccin Mocha — deep purple-blue dark theme |
| 3 | warm | Earthy tones — browns and oranges |
| 4 | ocean | Cool blues and teals |
Nature Themes
| # | Name | Description |
|---|------|-------------|
| 5 | forest | Deep greens — woodland palette |
| 6 | sunset | Orange and pink — warm gradient feel |
| 7 | emerald | Rich green — jewel-toned |
| 8 | mint | Fresh green-white — clean and airy |
| 9 | ice | Frosty light blue — crisp and cool |
| 10 | sand | Desert tones — muted earth colors |
| 11 | coral | Warm pink-orange — tropical feel |
Dark Themes
| # | Name | Description |
|---|------|-------------|
| 12 | midnight | Deep dark blue — night sky |
| 13 | charcoal | Neutral dark gray — understated |
| 14 | coffee | Dark brown — rich and warm |
| 15 | abyss | Ultra dark — near-black with cyan accents |
| 16 | neon | Bright on black — electric green accents |
Editor Themes
| # | Name | Description |
|---|------|-------------|
| 17 | dracula | Classic Dracula — purple accents on dark |
| 18 | monokai | Monokai — green accents on dark olive |
| 19 | cobalt | Cobalt — yellow accents on deep blue |
| 20 | nord | Nord Light — Nordic muted palette |
| 21 | nord-dark | Nord Dark — Nordic dark palette |
| 22 | solarized-light | Solarized Light — Ethan Schoonover's classic |
| 23 | solarized-dark | Solarized Dark — dark variant |
| 24 | github-light | GitHub Light — clean and familiar |
| 25 | github-dark | GitHub Dark — dark mode |
Colorful Themes
| # | Name | Description |
|---|------|-------------|
| 26 | lavender | Soft purples — gentle and calming |
| 27 | rose | Soft pinks — romantic palette |
| 28 | cherry | Deep red — bold and striking |
| 29 | grape | Deep purple — rich violet tones |
| 30 | bubblegum | Playful pastels — bright pink |
| 31 | amber | Golden warmth — honey tones |
Professional Themes
| # | Name | Description |
|---|------|-------------|
| 32 | slate | Professional gray-blue — corporate clean |
| 33 | steel | Industrial blue-gray — sturdy and reliable |
| 34 | paper | Minimal off-white — ink-on-paper feel |
Custom Themes
To build a fully custom theme, set any combination of the CSS variables below. Any variable you don't set will fall back to the default light theme values defined in calendar.css.
Example: Company Brand Theme
const myBrandTheme = {
'--cal-bg': '#fafafa',
'--cal-header-bg': '#003366', // Company navy
'--cal-border': '#ccc',
'--cal-border-light': '#e0e0e0',
'--cal-text': '#1a1a1a',
'--cal-text-secondary': '#555',
'--cal-text-muted': '#999',
'--cal-accent': '#ff6600', // Company orange
'--cal-accent-hover': '#cc5200',
'--cal-today-bg': '#fff3e6',
'--cal-today-border': '#ff6600',
'--cal-cell-bg': '#ffffff',
'--cal-other-month-bg': '#f5f5f5',
'--cal-other-month-text': '#bbb',
};
TacelCalendar.initialize(container, { theme: myBrandTheme });Example: Extend a Preset
// Start with the dark theme but change the accent to your brand color
const customDark = {
...TacelCalendar.themes.dark,
'--cal-accent': '#ff6600',
'--cal-accent-hover': '#cc5200',
'--cal-today-border': '#ff6600',
};
TacelCalendar.initialize(container, { theme: customDark });Theming App-Owned Modals
The calendar's theme only applies inside the .tacel-calendar container. If your app creates its own modals (e.g. Add Entry, Edit Entry) outside the container, use applyTheme() to propagate the theme CSS variables:
const overlay = document.createElement('div');
overlay.innerHTML = '...'; // your modal HTML
document.body.appendChild(overlay);
// Apply the calendar's current theme to the modal
calendar.applyTheme(overlay);Your modal CSS should use var(--cal-*, fallback) syntax so it works with or without a theme:
.my-modal {
background: var(--cal-bg, #fff);
color: var(--cal-text, #333);
border: 1px solid var(--cal-border, #e5e7eb);
}
.my-modal .btn-primary {
background: var(--cal-accent, #1a73e8);
}You can also read the raw variables with getThemeVars() for custom logic:
const vars = calendar.getThemeVars();
// { '--cal-bg': '#1a1a2e', '--cal-text': '#e0e0e0', ... }CSS Variables Reference
Every CSS variable the calendar uses, with its default value:
Layout & Base
| Variable | Default | Description |
|----------|---------|-------------|
| --cal-bg | #ffffff | Main background |
| --cal-header-bg | #f8f9fa | Header, day-name row, time gutter background |
| --cal-cell-bg | #ffffff | Month cell, week/day column background |
| --cal-border | #e2e5e9 | Primary borders |
| --cal-border-light | #f0f1f3 | Subtle inner borders |
| --cal-bg-secondary | #f9fafb | Secondary background (file rows, cards, panels) |
| --cal-bg-hover | #f3f4f6 | Hover state background (buttons, dropzones, interactive elements) |
| --cal-font | System font stack | Font family |
| --cal-font-size | 13px | Base font size |
| --cal-radius | 8px | Border radius (large) |
| --cal-radius-sm | 4px | Border radius (small) |
| --cal-shadow | Light shadow | Box shadow (normal) |
| --cal-shadow-lg | Larger shadow | Box shadow (popups) |
| --cal-transition | 0.15s ease | Transition timing |
Text
| Variable | Default | Description |
|----------|---------|-------------|
| --cal-text | #1f2937 | Primary text color |
| --cal-text-secondary | #6b7280 | Secondary text |
| --cal-text-muted | #9ca3af | Muted/disabled text |
Accent & Today
| Variable | Default | Description |
|----------|---------|-------------|
| --cal-accent | #1a73e8 | Accent color (buttons, active states) |
| --cal-accent-hover | #1557b0 | Accent hover state |
| --cal-today-bg | #e8f0fe | Today cell background |
| --cal-today-border | #1a73e8 | Today date circle color |
| --cal-today-bg-subtle | rgba(...) | Today column subtle tint (week/day view) |
Other Month
| Variable | Default | Description |
|----------|---------|-------------|
| --cal-other-month-bg | #fafafa | Other-month cell background |
| --cal-other-month-text | #b0b5bd | Other-month text color |
Priority Colors
| Variable | Default | Description |
|----------|---------|-------------|
| --cal-priority-high | #dc2626 | High priority color |
| --cal-priority-high-bg | #fef2f2 | High priority background |
| --cal-priority-medium | #f59e0b | Medium priority color |
| --cal-priority-medium-bg | #fffbeb | Medium priority background |
| --cal-priority-low | #16a34a | Low priority color |
| --cal-priority-low-bg | #f0fdf4 | Low priority background |
Entry Colors
| Variable | Default | Description |
|----------|---------|-------------|
| --cal-color-red | #dc2626 | Red entry color |
| --cal-color-red-bg | #fef2f2 | Red entry background |
| --cal-color-blue | #2563eb | Blue entry color |
| --cal-color-blue-bg | #eff6ff | Blue entry background |
| --cal-color-green | #16a34a | Green entry color |
| --cal-color-green-bg | #f0fdf4 | Green entry background |
| --cal-color-yellow | #ca8a04 | Yellow entry color |
| --cal-color-yellow-bg | #fefce8 | Yellow entry background |
| --cal-color-purple | #9333ea | Purple entry color |
| --cal-color-purple-bg | #faf5ff | Purple entry background |
| --cal-color-orange | #ea580c | Orange entry color |
| --cal-color-orange-bg | #fff7ed | Orange entry background |
Drag & Drop
| Variable | Default | Description |
|----------|---------|-------------|
| --cal-pill-text | #1f2937 | Entry pill text color — always readable regardless of entry color. Dark themes set this to their light text color. |
| --cal-drag-bg | rgba(...) | Drag target highlight background |
| --cal-drag-border | rgba(...) | Drag target highlight border |
Universal Entry Format
The module works with a normalized entry format. Each app's adapter converts its own data into this shape:
{
id: string | number, // Unique identifier
title: string, // Display title
description: string, // Optional description
start: Date, // Start date/time
end: Date | null, // End date/time (null = point-in-time)
isAllDay: boolean, // True = spans the whole day
color: string, // 'red', 'blue', 'green', 'yellow', 'purple', 'orange'
type: string, // 'task', 'event', 'meeting', etc.
status: string | null, // 'not_started', 'in_progress', 'completed'
priority: string | null, // 'high', 'medium', 'low'
section: string | null, // Grouping/category label
user: string | null, // Primary owner/assignee name (backward compat)
users: string[] | null, // All assigned users (supports multi-user entries like meetings)
completed_at: string | null, // ISO timestamp of completion
meta: object // Arbitrary app-specific data (preserved, never read)
}Callbacks
| Callback | Signature | Description |
|----------|-----------|-------------|
| onFetchEntries | async (range, selectedUserIds) => entry[] | Required. Fetch entries for a date range. range = { start: Date, end: Date } |
| onEntryClick | (entry, targetEl) => void | Override the built-in detail popup with custom behavior |
| onEntryDrop | async (entry, newDate) => { success } | Called when an entry is dragged to a new date/time |
| onAddEntry | (date) => void | Called when user clicks FAB or empty cell area |
| onEditEntry | (entry) => void | Called when user clicks "Edit" in detail popup or context menu. If provided, an Edit button appears automatically in default actions. |
| onGetAssignableUsers | () => [{ id, name }] | Returns users the current user can assign entries to. Controls the user picker in add/edit modals. |
| onMarkComplete | async (entry) => { success } | Called from default detail popup "Mark Complete" button |
| onMarkUncomplete | async (entry) => { success } | Called from default detail popup "Undo Complete" button |
| onAddStep | async (entry, stepText) => { success, step } | Add a new step to an entry (enables interactive step CRUD) |
| onUpdateStep | async (entry, step, updates) => { success } | Update a step's text or completed status |
| onDeleteStep | async (entry, step) => { success } | Delete a step from an entry |
| onGetStepFiles | async (entry, step) => file[] | Fetch files attached to a specific step |
| onUploadStepFiles | async (entry, step, files) => { success } | Upload files to a specific step |
| onDeleteStepFile | async (entry, step, file) => { success } | Delete a file from a specific step |
| onGetSections | async () => string[] | Return all available sections (default + custom). Used by the color legend filter to show a complete section list even when no entries exist for some sections. |
Permissions
The permissions system controls who can view and edit whose entries. It's completely optional — if not set, all users have full access.
How It Works
You provide a single callback function:
permissions: (currentUserId, entryOwnerId) => ({ canView: boolean, canEdit: boolean })The module calls this for every entry and enforces the result:
| Permission | Effect |
|-----------|--------|
| canView: false | Entry is hidden — filtered out after fetch, never rendered |
| canView: true, canEdit: false | Entry is visible but read-only — no drag-and-drop, no edit actions in menus/popups, subtle visual indicator |
| canView: true, canEdit: true | Full access — drag, edit, delete, mark complete, etc. |
Example: Role-Based Permissions
const users = [
{ id: 'pierre', name: 'Pierre', role: 'manager' },
{ id: 'sarah', name: 'Sarah', role: 'member' },
{ id: 'mike', name: 'Mike', role: 'viewer' },
];
TacelCalendar.initialize(container, {
users,
currentUserId: 'sarah',
permissions: (currentUserId, entryOwnerName) => {
const currentUser = users.find(u => u.id === currentUserId);
const isOwn = currentUser.name === entryOwnerName;
if (isOwn) return { canView: true, canEdit: true };
switch (currentUser.role) {
case 'manager':
return { canView: true, canEdit: true }; // Full access to everyone
case 'member':
return { canView: true, canEdit: false }; // Can see coworkers, can't edit
case 'viewer':
return { canView: false, canEdit: false }; // Own entries only
default:
return { canView: false, canEdit: false };
}
}
});Where Permissions Are Enforced
- Data layer: Entries with
canView: falseare filtered out afteronFetchEntries - Drag-and-drop: Disabled for entries with
canEdit: false(not draggable) - Detail popup: Default "Mark Complete" / "Undo Complete" buttons hidden for read-only entries
- Context menu: Default edit actions hidden for read-only entries
- Visual: Read-only entries get a
.tc-read-onlyCSS class (reduced opacity, no drag cursor)
No Permissions Callback = Full Access
If you don't set permissions, every user can view and edit every entry. This is the default behavior and is fine for single-user apps or apps that handle permissions server-side.
Detail Actions
The detail popup (shown on left-click) has customizable action buttons. There are three ways to configure them:
1. Use Defaults (null)
If detailActions is null (default), the module shows built-in buttons based on available callbacks:
- Mark Complete — shown if
onMarkCompleteis set and entry is not completed - Undo Complete — shown if
onMarkUncompleteis set and entry is completed
These defaults also respect permissions — they're hidden if the user can't edit the entry.
2. Static Array
detailActions: [
{
label: 'Go to Task',
icon: '<svg>...</svg>',
onClick: (entry, popup, cal) => { window.location.hash = `/tasks/${entry.id}`; }
},
{
label: 'Edit',
className: 'custom',
onClick: (entry, popup, cal) => myApp.openEditModal(entry)
}
]3. Dynamic Function (Recommended)
A function that returns actions per-entry — lets you show different buttons based on entry state and permissions:
detailActions: (entry, cal) => {
const perms = cal._getPermissions(entry);
const actions = [];
if (perms.canEdit && entry.status !== 'completed') {
actions.push({
label: 'Mark Complete',
className: 'complete',
loadingText: 'Completing...',
icon: '<svg>...</svg>',
onClick: async (entry, popup, cal) => {
await myApp.completeTask(entry.id);
return { success: true }; // Closes popup and refreshes
}
});
}
actions.push({
label: 'Go to Task',
onClick: (entry) => myApp.navigateToTask(entry.id)
});
return actions;
}Action Object Properties
| Property | Type | Description |
|----------|------|-------------|
| label | string | Button text |
| icon | string | Optional inline SVG/HTML prepended to label |
| className | string | Optional CSS class ('complete', 'uncomplete', 'custom') |
| loadingText | string | If set, button shows this text while onClick is running |
| hidden | bool\|fn(entry) | Hide this action entirely |
| onClick | fn(entry, popup, cal) | Click handler. Return { success: true } to auto-close popup and refresh. |
Context Menu
Right-clicking opens a context menu everywhere in the calendar — not just on entries. The module provides built-in menus for every area:
| Right-click Target | Menu Contents |
|-------------------|---------------|
| Entry | View Details, Edit, Mark Complete/Undo (customizable via contextMenuItems) |
| Empty cell (month view) | Date header, Add Entry, Go to Day View, Go to Week View, Refresh |
| Empty time slot (week/day view) | Date+time header, Add Entry Here, Refresh |
| Header title | Info explanation, Go to Today |
| View switcher | Info explanation, shortcut hints |
| Navigation buttons | Info explanation, shortcut hints |
| Background (any other area) | Add Entry (Today), Go to Today, Switch views, Refresh, Keyboard Shortcuts |
Header context menus show info/explanation items (ℹ icon, dimmed) that describe what the element does — useful for discoverability.
Entry Context Menu
Right-clicking an entry opens a context menu. Fully customizable — same three patterns as detail actions:
1. Use Defaults (null)
If contextMenuItems is null, the module shows:
- View Details — opens the detail popup
- Mark Complete / Undo Complete — if callbacks are set (gated by permissions)
- Any items from
detailActions(if it's an array)
2. Static Array
contextMenuItems: [
{ label: 'View Details', onClick: (entry, cal) => cal._showEntryDetail(entry, null) },
{ label: 'Copy Title', onClick: (entry) => navigator.clipboard.writeText(entry.title) },
{ separator: true },
{ label: 'Delete', danger: true, onClick: (entry, cal) => myApp.deleteEntry(entry.id) }
]3. Dynamic Function (Recommended)
contextMenuItems: (entry, cal) => {
const perms = cal._getPermissions(entry);
const items = [
{ label: 'View Details', onClick: (entry, cal) => cal._showEntryDetail(entry, null) },
];
if (perms.canEdit) {
items.push({ label: 'Mark Complete', onClick: async (entry, cal) => { /* ... */ } });
items.push({ separator: true });
items.push({ label: 'Delete', danger: true, onClick: (entry, cal) => { /* ... */ } });
}
return items;
}Menu Item Properties
| Property | Type | Description |
|----------|------|-------------|
| label | string | Menu item text |
| icon | string | Optional inline SVG/HTML |
| shortcut | string | Optional keyboard shortcut hint (display only) |
| onClick | fn(entry, cal) | Click handler |
| disabled | bool\|fn(entry) | Gray out the item |
| hidden | bool\|fn(entry) | Hide the item entirely |
| danger | boolean | Red styling for destructive actions |
| separator | boolean | Render a horizontal line instead of an item |
Steps / Subtasks
Entries can include steps (subtasks) via the meta.steps array. The detail popup renders them with a progress bar, checkmarks, and full inline CRUD when step callbacks are provided.
Step Completion Markers on Calendar (v1.5.0+)
When a step has a completed_at date, the calendar displays a visual marker on the day that step was completed — across all views (month, week, day, upcoming, and day popup). This lets you see at a glance which steps were completed on which days, even if the parent task is scheduled for a different date.
- Month view — when step markers toggle is enabled, all steps from entries on that date are shown as small dashed-border pills with a ✓ (completed) or ○ (pending) indicator and step text
- Week view — inline markers in the all-day section
- Day view — a "Steps Completed" section in the sidebar
- Upcoming view — step items appear after entries for each day group
- Day popup — step completions listed below entries with a divider
Clicking any step marker opens the parent task's detail popup, so you can quickly navigate from a completed step to its full task context.
When the step markers toggle is enabled, markers show all steps from entries on that date — matching the step count shown in the cell counter badge.
Cell Task/Step Counters (v1.8.0+)
Each day cell in month view shows clickable task and step count badges in the top-right corner:
- "5 tasks" — muted text showing the total number of entries on that date
- "7 steps" — accent-colored text showing the total steps across all entries (only shown if steps exist)
Clicking either counter opens a full centered modal listing all tasks or steps with:
- Color dots matching the entry color
- Status indicators (✓ for completed, ○ for pending)
- Click any row to open the entry's detail popup
- Close via × button, backdrop click, or Escape key
Counters are enabled by default (features.cellCounters: true). Apps can disable with features.cellCounters: false.
Entry Sorting by Status (v1.8.0+)
Entries within each month cell are automatically sorted so uncompleted tasks appear at the top and completed tasks sink to the bottom. This applies to both multi-day spanning entries and single-day entries.
The completed_at field is automatically set by the calendar when toggling a step's completion status via the detail popup. Apps using their own UI to toggle steps should set completed_at to an ISO date string when marking complete, or null when uncompleting.
Controlling Step Marker Visibility
The features.stepMarkers config controls whether step markers are shown:
| Value | Behavior |
|-------|----------|
| true (default) | Always show step markers. No toggle button. |
| false | Never show step markers. No toggle button. |
| 'user' | Show a toggle button in the header. User can turn markers on/off. Starts visible. |
TacelCalendar.initialize(container, {
features: {
stepMarkers: 'user', // Let the user toggle step markers on/off
}
});Apps can also control visibility programmatically regardless of the config mode:
calendar.setStepMarkers(false); // Hide step markers
calendar.setStepMarkers(true); // Show step markers
calendar.getStepMarkers(); // Returns current visibility (boolean)The toggle button is available as a header part (parts.stepMarkersToggle) for custom header layouts.
Entry Format
{
id: 1,
title: 'Deploy Hotfix',
start: new Date(),
// ... other fields ...
meta: {
steps: [
{ id: 1, text: 'Merge PR to main', completed: true, completed_at: '2025-02-09T14:00:00Z' },
{ id: 2, text: 'Run staging tests', completed: true, completed_at: '2025-02-10T10:30:00Z' },
{ id: 3, text: 'Deploy to production', completed: false },
]
}
}What Renders
When an entry with meta.steps is clicked, the detail popup shows:
- Steps header with "Steps" label and a
done/totalcounter - Progress bar — fills proportionally with the accent color
- Step list — each step shows a circle check (green when done) and text (strikethrough when done)
Interactive Step CRUD (v1.5.0+)
When the step CRUD callbacks (onAddStep, onUpdateStep, onDeleteStep) are provided, steps become fully interactive in the detail popup and View All popup:
- Add Step — inline input appears below the step list, type and press Enter or click "Add"
- Edit Step — click the step text to enter inline edit mode, press Enter to save or Escape to cancel
- Toggle Complete — click the checkbox circle to mark a step complete or uncomplete
- Delete Step — hover over a step to reveal the delete button (appears on the right)
- Per-Step File Attachments — each step can have its own file upload dropzone with drag-drop, browse, and scoped Ctrl+V paste
All operations call the corresponding callback and refresh the UI on success.
Step CRUD Callbacks
| Callback | Signature | Description |
|----------|-----------|-------------|
| onAddStep | async (entry, stepText) => { success, step } | Add a new step. Return the created step object. |
| onUpdateStep | async (entry, step, updates) => { success } | Update a step. updates = { text?, completed?, completed_at? } |
| onDeleteStep | async (entry, step) => { success } | Delete a step. |
Example
TacelCalendar.initialize(container, {
onAddStep: async (entry, stepText) => {
const newStep = await ipcRenderer.invoke('calendar:add-step', entry.id, stepText);
return { success: true, step: newStep };
},
onUpdateStep: async (entry, step, updates) => {
await ipcRenderer.invoke('calendar:update-step', entry.id, step.id, updates);
return { success: true };
},
onDeleteStep: async (entry, step) => {
await ipcRenderer.invoke('calendar:delete-step', entry.id, step.id);
return { success: true };
},
});If these callbacks are not provided, steps display as read-only (the previous behavior).
Add / Edit Entry Modals (Test App)
The test app demonstrates a full steps editor in the Add Entry and Edit Entry modals:
- Auto-resizing textarea — step text wraps to multiple lines instead of horizontal scroll
- Per-step file attachments — click the paperclip icon on any step to expand a file dropzone
- Task-level file attachments — a dropzone at the bottom of the modal for files that belong to the task itself
- Scoped clipboard paste — hover/click a dropzone, then Ctrl+V to paste a screenshot only to that specific dropzone
- File count badges — each step shows a badge with the number of attached files
Auto-Refresh
The calendar silently polls for data changes at a configurable interval. If the data hasn't changed, nothing happens. If it has, the view re-renders smoothly with no loading indicator or flicker.
Configuration
TacelCalendar.initialize(container, {
autoRefresh: 5000, // Poll every 5 seconds (default)
// autoRefresh: 10000, // Poll every 10 seconds
// autoRefresh: false, // Disable auto-refresh entirely
});| Option | Type | Default | Description |
|--------|------|---------|-------------|
| autoRefresh | number\|false | 5000 | Milliseconds between silent refreshes. Set to false or 0 to disable. |
How It Works
- Every
autoRefreshms, the calendar calls youronFetchEntriescallback with the current view's date range - The returned entries are hashed (id, title, status, start, end, color, priority, isAllDay, step completions) and compared to the previous hash
- If identical → nothing happens, zero DOM work
- If different →
_state.entriesis updated and only the view content re-renders (no header rebuild, no loading spinner) - Skips the tick if a user-initiated load is already in progress
- Errors are silently swallowed — the user is never disrupted
Runtime Control
// Change interval at runtime
calendar.setAutoRefresh(10000); // Switch to 10s
// Disable
calendar.setAutoRefresh(false);
// Re-enable
calendar.setAutoRefresh(3000);Cleanup
Auto-refresh is automatically stopped when calendar.destroy() is called.
Keyboard Shortcuts
The calendar includes a full set of keyboard shortcuts enabled by default. Shortcuts are ignored when the user is typing in an input, textarea, or select.
Configuration
TacelCalendar.initialize(container, {
features: {
keyboardShortcuts: true, // Enable shortcuts (default: true)
shortcutHelp: false, // Show "?" button in header (default: false)
}
});| Feature | Default | Description |
|---------|---------|-------------|
| keyboardShortcuts | true | Enable all keyboard shortcuts |
| shortcutHelp | false | Show a ? button in the header that opens the shortcut help popup |
All Shortcuts
Navigation
| Key | Action |
|-----|--------|
| ← | Previous period (month/week/day) |
| → | Next period |
| T | Go to today |
Views
| Key | Action |
|-----|--------|
| M | Switch to Month view |
| W | Switch to Week view |
| D | Switch to Day view |
| U | Switch to Upcoming view (if enabled) |
Actions
| Key | Action |
|-----|--------|
| N | New entry (opens add entry for today) |
| R | Refresh data |
| / | Focus search bar (if enabled) |
| Escape | Close any open popup, detail panel, or overlay |
| Ctrl+Z | Undo last drag-and-drop move (⌘+Z on Mac) |
| Ctrl+Y | Redo last undone move (⌘+Y on Mac) |
Quick Access
| Key | Action |
|-----|--------|
| ? | Show keyboard shortcuts help popup |
| 1 | Jump to Monday (current week) |
| 2 | Jump to Tuesday |
| 3 | Jump to Wednesday |
| 4 | Jump to Thursday |
| 5 | Jump to Friday |
| 6 | Jump to Saturday |
| 7 | Jump to Sunday |
Shortcut Help Popup
When features.shortcutHelp is true, a small ? button appears in the header. Clicking it (or pressing ?) opens a themed overlay listing all shortcuts grouped by category.
Programmatic Access
// Show the shortcut help popup programmatically
calendar.showShortcutHelp();
// Get the full shortcut list as structured data
const shortcuts = calendar.getShortcuts();
// Returns: [{ category: 'Navigation', shortcuts: [{ keys: ['←'], description: '...' }, ...] }, ...]Responsive Design
The calendar adapts to any screen size — from tiny containers (~300px) to large displays (1400px+). No configuration needed; it's all handled via CSS media queries.
Breakpoints
| Breakpoint | What Changes | |-----------|-------------| | ≥1400px | Larger cells, bigger text, wider sidebar (300px), wider detail popup (380px) | | ≤900px | Sidebar narrows to 200px, user picker role text hidden | | ≤768px | Header wraps (title + nav on top, view toggle full-width below), day sidebar hidden, narrower time gutters (42px), smaller fonts, search bar shrinks, mini calendar/legend narrower | | ≤560px | Body stacks vertically (mini cal on top, legend on bottom), search bar full-width row, entry detail centered as modal with sticky header, day/shortcut popups centered, context menu larger touch targets, ultra-narrow gutters (32px), user picker icon-only | | ≤400px | Mini calendar hidden, legend minimal horizontal, detail popup full-width with stacked action buttons, undo toast edge-to-edge (detail text hidden), minimal gutters (24px), tiny fonts |
What Adapts
- Header: Title shrinks, view toggle goes full-width, search bar becomes full-width row, user picker collapses to avatar-only, nav buttons shrink
- Month View: Cell min-height, day number size, pill font/padding, checkmarks hidden on tiny screens
- Week View: Time gutter width (60px → 42px → 32px → 24px), day header sizes, entry block font sizes, section text hidden
- Day View: Same gutter scaling as week, sidebar hidden at ≤768px
- Upcoming View: Padding, item spacing, badge sizes, title font sizes
- Mini Calendar: Narrows at 768px → horizontal strip at 560px → hidden at 400px
- Color Legend: Narrows at 768px → horizontal strip with wrapping items at 560px → minimal at 400px
- Detail Popup: Width scales from 380px → 320px → centered modal (360px) → full width; sticky header, scrollable body; action buttons stack vertically on tiny screens
- Day Popup / Shortcut Help: Centered modal on small screens, full-width on tiny screens
- Undo Toast: Narrows → full viewport width → edge-to-edge (detail text hidden on tiny)
- Context Menu: Larger touch targets on small screens (10px padding)
- Attachments: Compact padding, smaller icons and fonts
- FAB: Shrinks on tiny screens (44px → 40px)
- Search Bar: Shrinks at 768px → full-width row at 560px → tighter at 400px
Popup Positioning
On large screens (>560px), the detail popup positions itself near the mouse click (not relative to the element), so it always appears where you expect — even on wide day-view blocks or full-width upcoming items. Falls back to element-relative positioning if no click event is available.
On small screens (≤560px), all popups automatically center on screen as a modal overlay. The positioning functions detect the viewport size and switch to translate(-50%, -50%) centering, preventing popups from going off-screen.
All popups include a fade-in/fade-out animation (150ms scale + opacity transition) for a smooth, polished feel across all views.
Public API
Static Methods
| Method | Description |
|--------|-------------|
| TacelCalendar.initialize(container, config) | Mount a calendar. Returns a TacelCalendarInstance. |
| TacelCalendar.loadCSS(path?) | Dynamically inject the stylesheet. Auto-detects path if omitted. |
| TacelCalendar.themes | Object containing all 34 built-in theme presets. |
Instance Methods
| Method | Description |
|--------|-------------|
| instance.refresh() | Re-fetch entries and re-render the current view. |
| instance.switchView(name) | Switch to 'month', 'week', 'day', or 'upcoming'. |
| instance.navigate(dir) | Navigate 'prev', 'next', or 'today'. |
| instance.setUsers(users, selectedIds) | Update user list and selection at runtime. |
| instance.setAutoRefresh(ms\|false) | Change auto-refresh interval at runtime, or disable with false. |
| instance.showShortcutHelp() | Open the keyboard shortcuts help popup. |
| instance.getShortcuts() | Get structured shortcut data: [{ category, shortcuts: [{ keys, description }] }]. |
| instance.setTheme(theme) | Change theme at runtime. Persists to localStorage if persistTheme is enabled. Accepts preset name, CSS var object, or null. |
| instance.getTheme() | Get the current theme value (preset name, object, or null). |
| instance.getThemeVars() | Get the resolved theme CSS variables as an object: { '--cal-bg': '#1a1a2e', ... }. |
| instance.applyTheme(el) | Apply the calendar's theme CSS variables to any DOM element (e.g. app-owned modals). |
| instance.destroy() | Clean up everything — DOM, intervals, keyboard listeners, inline styles. |
File Structure
calendar-module/
├── index.js # Entry point — exports { TacelCalendar, THEMES }
├── package.json # npm package: tacel-calendar
├── README.md # This file
├── calendar.js # Core module: init, state, rendering, navigation
├── calendar.css # Full stylesheet with CSS variable theming
├── themes.js # 34 built-in theme presets
├── components/
│ ├── header.js # Header bar: title, view toggle, nav, user picker
│ ├── month-view.js # Month grid with day cells, pills, overflow
│ ├── week-view.js # Week time-grid with hourly slots, all-day banner
│ ├── day-view.js # Day time-grid with sidebar
│ ├── upcoming-view.js # Coming Up sorted list
│ ├── day-popup.js # Day overflow popup (month view "+N more")
│ ├── entry-detail.js # Entry detail popup with customizable fields/actions
│ ├── context-menu.js # Right-click context menus (entries, cells, headers, background)
│ ├── fab.js # Floating action button
│ ├── mini-calendar.js # Mini calendar navigator sidebar
│ ├── search-bar.js # Search / filter bar component
│ ├── undo-toast.js # Undo toast for drag-and-drop moves
│ └── color-legend.js # Color coding legend panel
└── utils/
├── date-utils.js # Date math: ranges, formatting, week start, spans
└── dom-utils.js # DOM helpers: createElement, escape, positionAdapter Examples
Wire-Scheduler
function toCalendarEntry(task) {
return {
id: task.id,
title: task.title,
description: task.description || '',
start: new Date(task.due_date),
end: null,
isAllDay: false,
color: getPriorityColor(task.priority),
type: 'task',
status: task.status,
priority: task.priority,
section: task.section || 'General',
user: task.user,
completed_at: task.completed_at || null,
meta: { ticket_category_id: task.ticket_category_id, ticket_number: task.ticket_number }
};
}Office-HQ / ShipWorks
function toCalendarEntry(item) {
return {
id: item.id,
title: item.subject || item.title,
description: item.notes || '',
start: new Date(item.scheduled_date),
end: item.end_date ? new Date(item.end_date) : null,
isAllDay: !!item.all_day,
color: item.category_color || 'blue',
type: item.type || 'event',
status: item.status,
priority: item.priority,
section: item.department || null,
user: item.assigned_to || null,
completed_at: item.completed_at || null,
meta: { rma_id: item.rma_id, order_number: item.order_number }
};
}Test App
A standalone Electron test app is included at:
Random (rma,ticketing,more)/calendar-test-app/Run it:
cd "Random (rma,ticketing,more)/calendar-test-app"
npm install
npm startThe test app includes:
- All 34 themes in a dropdown (organized by category)
- 3 mock data sets (Full 30+ entries, Sparse 5 entries, Empty)
- Event log panel showing all callback invocations
- Add/Edit entry modals with task-level and per-step file uploads, auto-resizing step textareas, and scoped clipboard paste
- Toast notifications for user actions
- Full drag-and-drop with undo toast, mark complete/uncomplete, and entry detail testing
- Mini calendar navigator sidebar with click-to-jump and entry dot indicators
- Search/filter bar with real-time entry filtering
- Color coding legend with click-to-filter by color
- File attachments demo with mock file store (task-level and per-step upload, open, delete)
- Interactive step CRUD in detail popup (add, edit, toggle complete, delete steps inline)
- Per-step file attachments with scoped Ctrl+V paste in detail and View All popups
- Custom header toggle — demonstrates
headerRendererwith rearranged layout and app-specific entry count badge - Multi-user permissions demo (Manager/Member roles)
- Universal right-click context menus on entries, cells, headers, and background
- Category View with tab switching and inline filter bar
- Theme persistence across sessions
Version History
| Version | Date | Changes |
|---------|------|---------|
| 1.8.0 | Feb 2026 | Dashboard header layout — new 'dashboard' header layout preset with page title top-left, month/date centered, user picker top-right, and a bottom row with view toggle, centered nav, and action buttons. Cell task/step counters — clickable "X tasks" / "X steps" badges in each month cell that open a full modal listing all items. Theme dropdown — built-in theme selector in the header (features.themeDropdown). Pill text readability — --cal-pill-text CSS variable ensures entry text is always black in light themes and white in dark themes. Entry sorting — uncompleted tasks sort to the top of each cell, completed to the bottom. Header action buttons — Export, Stats, Refresh, Popout buttons as reusable module-level header parts. Uniform header height — all dashboard bottom row controls share 32px height. Step CRUD fix — in-memory entry updates before UI refresh so steps appear/disappear instantly without needing to reopen the popup. Mini calendar default — starts collapsed by default. |
| 1.7.0 | Feb 2026 | Section filter improvements — onGetSections callback for color legend to show full section list (default + custom) even when no entries exist. Theme variable additions — --cal-bg-secondary and --cal-bg-hover CSS variables for proper theming of secondary backgrounds and hover states across all UI elements (View All popup step files, file attachment rows, etc.). Fixed white background issue in View All popup step file areas when using dark themes. |
| 1.6.1 | Feb 2026 | Theme fix release — --cal-bg-secondary and --cal-bg-hover variables added to theme builder for proper dark-theme support in secondary UI areas. |
| 1.6.0 | Feb 2026 | Section filter fix — color legend now uses onGetSections callback to display all sections including custom ones. |
| 1.5.0 | Feb 2026 | Interactive Step CRUD — add, edit, toggle complete, and delete steps inline in detail popup and View All popup (via onAddStep, onUpdateStep, onDeleteStep callbacks). Per-step file attachments — each step can have its own files with upload, delete, and preview (onGetStepFiles, onUploadStepFiles, onDeleteStepFile). Scoped clipboard paste — Ctrl+V screenshots go only to the focused/active dropzone, not all dropzones. Scrollable detail popup — header stays pinned, body scrolls when content is tall. Add/Edit modal file uploads — task-level and per-step file dropzones with drag-drop, browse, and scoped paste. Auto-resizing step textareas — step text wraps to multiple lines instead of horizontal scroll. Modal click-outside protection — Add/Edit modals only close via X or Cancel, not by clicking the backdrop. Inline category creation — "Add New Category" uses an inline input instead of prompt() (Electron-compatible). |
| 1.4.0 | Feb 2026 | Category View (features.categoryView), 10 header layout presets (headerLayout config), themed custom scrollbars, resizable panels (mini calendar + color legend), entry.section in search. |
| 1.3.0 | Feb 2026 | Custom header support (headerRenderer config, cal.headerParts factory, individual header building blocks). Color legend multi-select filtering (toggle multiple colors, works on all views). Comprehensive responsive design overhaul — mini calendar/legend stack vertically on small screens, entry detail centers as modal, popups auto-center on small viewports, search bar full-width row, touch-friendly context menus, undo toast adapts, attachments compact. Fixed search bar and legend filter selectors for week/day/upcoming views. |
| 1.2.0 | Feb 2026 | File attachments in entry detail (onGetFiles, onUploadFiles, onOpenFile, onDeleteFile callbacks, dropzone, type-colored icons, permissions-gated) |
| 1.1.0 | Feb 2026 | Mini calendar navigator, search/filter bar, undo toast for drag-drop, color coding legend, universal context menus, theme persistence, / keyboard shortcut |
| 1.0.0 | Feb 2026 | Initial release — month/week/day/upcoming views, 34 themes, drag-and-drop, entry detail, FAB, multi-user picker |
