UNPKG

@taskgenius/calendar

Version:

A lightweight, configurable TypeScript calendar component with drag-and-drop support

734 lines (634 loc) 15.6 kB
/* === base.css === */ /** * Base styles - CSS variables, container, header, navigation */ .tg-calendar { /* CSS Variables */ --tg-primary-color: #3b82f6; --tg-primary-rgb: 59, 130, 246; --tg-cell-height: 60px; --tg-font-header: 14px; --tg-font-event: 12px; /* Base styles */ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; -webkit-font-smoothing: antialiased; user-select: none; overflow-x: hidden; /* Main container layout */ width: 100%; height: 100%; background: white; border-radius: 12px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); border: 1px solid #e5e7eb; display: flex; flex-direction: column; overflow: hidden; } /* Disable text selection during range selection */ .tg-calendar.tg-selecting { user-select: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; } /* View container */ .tg-view-container { min-height: 0; background: white; flex: 1; overflow: hidden; position: relative; display: flex; flex-direction: column; } /* Header styles */ .tg-header { display: flex; justify-content: space-between; align-items: center; padding: 16px; border-bottom: 1px solid #e5e7eb; background: white; } .tg-title { font-size: 1.25rem; font-weight: bold; color: #1f2937; } /* View switch buttons */ .tg-view-switch { display: flex; background: #f3f4f6; padding: 4px; border-radius: 8px; } .tg-view-btn { padding: 4px 12px; font-size: 14px; border-radius: 6px; border: none; background: transparent; cursor: pointer; transition: all 0.2s; color: #6b7280; } .tg-view-btn:hover { color: #374151; } .tg-view-btn.tg-active { background: white; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); color: var(--tg-primary-color); font-weight: 500; } /* Navigation buttons */ .tg-nav { display: flex; gap: 8px; } .tg-nav-btn { padding: 4px 12px; font-size: 14px; border-radius: 4px; border: 1px solid #e5e7eb; background: #f9fafb; cursor: pointer; transition: all 0.2s; } .tg-nav-btn:hover { background: #f3f4f6; } .tg-nav-btn.tg-today { background: rgba(var(--tg-primary-rgb), 0.1); color: var(--tg-primary-color); border-color: rgba(var(--tg-primary-rgb), 0.2); } .tg-nav-btn.tg-today:hover { background: rgba(var(--tg-primary-rgb), 0.2); } /* Grid layout helpers */ .tg-grid-7 { display: grid; grid-template-columns: repeat(7, 1fr); } /* Disabled state */ .tg-disabled { opacity: 0.5; pointer-events: none; cursor: not-allowed; } /* === month.css === */ /** * Month view styles */ /* Month body container - grid layout to distribute row heights evenly */ .tg-month-body { overflow-y: auto; flex: 1; min-height: 0; /* Required for flex child to respect overflow */ display: grid; grid-auto-rows: 1fr; /* All rows get equal height */ } /* Month header */ .tg-month-header { display: grid; border-bottom: 1px solid #e5e7eb; background: #f9fafb; } .tg-month-header-cell { padding: 8px; text-align: center; font-size: 12px; font-weight: bold; color: #9ca3af; } /* Month row - height controlled by parent grid (grid-auto-rows: 1fr) */ .tg-month-row { display: grid; position: relative; min-height: var(--tg-row-min-height, 80px); /* Minimum height per row */ border-bottom: 1px solid #e5e7eb; } /* Month cell */ .tg-month-cell { height: 100%; padding: 4px; border-right: 1px solid #e5e7eb; position: relative; z-index: 1; } .tg-month-cell:last-child { border-right: none; } /* Date number in month cell */ .tg-date-number { text-align: right; font-size: 12px; padding: 4px; } .tg-date-number.tg-current-month { color: #374151; } .tg-date-number.tg-other-month { color: #d1d5db; } .tg-date-number.tg-today { color: var(--tg-primary-color); font-weight: bold; } /* Event count badge */ .tg-event-count-badge { position: absolute; bottom: 2px; right: 2px; background: var(--tg-primary-color); color: white; border-radius: 10px; padding: 2px 6px; font-size: 10px; font-weight: bold; min-width: 18px; text-align: center; } /* Disabled month cell */ .tg-month-cell.tg-disabled { opacity: 0.5; pointer-events: none; background-color: #f9fafb; } .tg-month-cell.tg-disabled .tg-date-number { color: #9ca3af; text-decoration: line-through; } /* Range preview for month view */ .tg-month-cell.tg-range-preview { background-color: rgba(var(--tg-primary-rgb), 0.15); position: relative; } .tg-month-cell.tg-range-preview::before { content: ""; position: absolute; inset: 0; border: 2px solid rgba(var(--tg-primary-rgb), 0.4); pointer-events: none; box-sizing: border-box; } /* "+N more" indicator */ .tg-more-indicator { position: absolute; font-size: 11px; color: var(--tg-primary-color, #3b82f6); padding: 2px 6px; cursor: pointer; border-radius: 4px; transition: background-color 0.15s; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; z-index: 5; } .tg-more-indicator:hover { background-color: rgba(var(--tg-primary-rgb, 59, 130, 246), 0.1); } /* More events popover */ .tg-more-popover { position: fixed; z-index: 1000; background: white; border-radius: 8px; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15), 0 2px 8px rgba(0, 0, 0, 0.1); border: 1px solid #e5e7eb; min-width: 200px; max-width: 280px; max-height: 300px; overflow: hidden; display: flex; flex-direction: column; } .tg-more-popover-header { padding: 10px 12px; font-weight: 600; font-size: 13px; color: #374151; border-bottom: 1px solid #e5e7eb; background: #f9fafb; } .tg-more-popover-list { overflow-y: auto; padding: 4px 0; flex: 1; } .tg-more-popover-item { display: flex; align-items: center; gap: 8px; padding: 8px 12px; font-size: 12px; color: #374151; transition: background-color 0.15s; } .tg-more-popover-item:hover { background-color: #f3f4f6; } .tg-more-popover-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; } .tg-more-popover-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* === time.css === */ /** * Time view (week/day) styles */ /* Time grid container - scrollable wrapper */ .tg-time-grid-container { flex: 1 1 0; min-height: 0; overflow-y: auto; overflow-x: hidden; position: relative; } /* Time header */ .tg-time-header { display: flex; padding-left: 60px; border-bottom: 1px solid #e5e7eb; position: sticky; top: 0; background: white; z-index: 30; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.02); } .tg-time-header-cell { flex: 1; text-align: center; padding: 8px; border-right: 1px solid #f3f4f6; font-size: var(--tg-font-header); } .tg-time-header-cell .tg-header-date { font-weight: bold; font-size: 18px; } .tg-time-header-cell.tg-today { background-color: rgba(var(--tg-primary-rgb), 0.1); color: var(--tg-primary-color); } .tg-time-header-cell.tg-disabled { opacity: 0.5; pointer-events: none; background-color: #f9fafb; } /* Time body */ .tg-time-body { display: flex; position: relative; min-height: calc(var(--tg-cell-height) * 24); } /* Time axis */ .tg-time-axis { width: 60px; flex-shrink: 0; background: #fafafa; border-right: 1px solid #e5e7eb; position: sticky; left: 0; z-index: 20; height: 100%; } .tg-time-axis-label { position: absolute; width: 100%; height: var(--tg-cell-height); text-align: right; padding-right: 8px; color: #9ca3af; font-size: 11px; pointer-events: none; /* Use transform for GPU-accelerated positioning */ will-change: transform; } .tg-time-axis-label.custom { font-weight: bold; } /* Day columns */ .tg-day-column { flex: 1; position: relative; border-right: 1px solid #e5e7eb; background: repeating-linear-gradient( to bottom, transparent 0, transparent calc(var(--tg-cell-height) - 1px), #f3f4f6 var(--tg-cell-height) ); } /* Range preview for time view */ .tg-time-range-preview { position: absolute; background-color: rgba(var(--tg-primary-rgb), 0.15); border: 2px dashed rgba(var(--tg-primary-rgb), 0.8); pointer-events: none; z-index: 15; border-radius: 4px; box-sizing: border-box; } /* Column-level range preview for multi-day selection */ .tg-day-column.tg-range-preview { background-color: rgba(var(--tg-primary-rgb), 0.08); position: relative; } .tg-day-column.tg-range-preview::before { content: ""; position: absolute; inset: 0; border-left: 3px solid rgba(var(--tg-primary-rgb), 0.5); border-right: 3px solid rgba(var(--tg-primary-rgb), 0.5); pointer-events: none; box-sizing: border-box; } /* === events.css === */ /** * Event styles - event blocks, bars, drag/resize handles * * Performance note: Events use CSS custom properties with transform * for GPU-accelerated positioning instead of top/left properties. */ /* Event base styles */ .tg-event-base { position: absolute; border-radius: 4px; padding: 2px 6px; font-size: var(--tg-font-event); color: white; cursor: grab; z-index: 10; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); transition: opacity 0.2s, box-shadow 0.1s, filter 0.1s; /* GPU acceleration for smooth transforms */ will-change: transform; /* Base position for transform - JS sets translate() */ top: 0; left: 0; } .tg-event-base:hover { box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); filter: brightness(1.05); } .tg-event-base:active { cursor: grabbing; } .tg-event-base.tg-is-dragging-source { opacity: 0.3; filter: grayscale(0.3); } /* Month view event bar */ .tg-event-bar { height: 26px; line-height: 26px; } /* Time view event block */ .tg-event-block { display: flex; flex-direction: column; justify-content: flex-start; line-height: 1.3; padding-top: 4px; border: 1px solid rgba(255, 255, 255, 0.3); /* Height set via CSS custom property */ white-space: normal; } .tg-event-block .tg-time-text { font-size: 10px; opacity: 0.9; margin-bottom: 0; } /* Event title */ .tg-event-title { font-weight: bold; overflow: hidden; text-overflow: ellipsis; } /* Resize handles */ .tg-resize-handle { position: absolute; z-index: 20; opacity: 0; transition: opacity 0.2s; } .tg-event-base:hover .tg-resize-handle { opacity: 1; } .tg-resize-handle:hover { background-color: rgba(255, 255, 255, 0.4); } /* Horizontal resize handles (month view) */ .tg-resize-h { top: 0; bottom: 0; width: 12px; cursor: col-resize; } .tg-resize-h.tg-left { left: 0; } .tg-resize-h.tg-right { right: 0; } /* Vertical resize handle (time view) */ .tg-resize-v { left: 0; right: 0; height: 8px; cursor: row-resize; } .tg-resize-v.tg-top { top: 0; bottom: auto; } .tg-resize-v.tg-bottom { bottom: 0; top: auto; } /* Ghost element for drag preview */ .tg-ghost-event { position: absolute; background-color: rgba(var(--tg-primary-rgb), 0.15); border: 2px dashed rgba(var(--tg-primary-rgb), 0.8); border-radius: 4px; z-index: 5; pointer-events: none; box-sizing: border-box; } /* Drag proxy */ #tg-drag-proxy { position: fixed; pointer-events: none; z-index: 9999; box-shadow: 0 12px 24px rgba(0, 0, 0, 0.2); border-radius: 4px; visibility: hidden; opacity: 0.9; transform-origin: top left; } /* ============================================================================= All-day Events Section (Week/Day views) ============================================================================= */ .tg-allday-section { display: flex; flex-direction: row; border-bottom: 1px solid var(--tg-border-color, #e5e7eb); background-color: var(--tg-bg-color, #fff); min-height: 28px; } .tg-allday-spacer { flex-shrink: 0; width: 60px; /* Match time axis width */ border-right: 1px solid var(--tg-border-color, #e5e7eb); } .tg-allday-events-container { flex: 1; position: relative; min-height: 28px; } /* All-day event bar (spanning style like month view) */ .tg-allday-event { position: absolute; height: 22px; line-height: 22px; border-radius: 3px; padding: 0 6px; font-size: 12px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; cursor: grab; box-sizing: border-box; } .tg-allday-event:hover { filter: brightness(1.05); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); } .tg-allday-event:active { cursor: grabbing; } /* ============================================================================= Cross-Midnight Event Styles (Time view) Events that span multiple days show as segmented blocks ============================================================================= */ /* Base styling for segmented events */ .tg-event-block.tg-event-segmented { /* Visual indicator that this is part of a multi-day event */ } /* First segment: event start - has rounded top corners */ .tg-event-block.tg-event-segment-first { border-bottom-left-radius: 0; border-bottom-right-radius: 0; border-bottom: 2px dashed rgba(255, 255, 255, 0.5); } /* Last segment: event end - has rounded bottom corners */ .tg-event-block.tg-event-segment-last { border-top-left-radius: 0; border-top-right-radius: 0; border-top: 2px dashed rgba(255, 255, 255, 0.5); } /* Middle segments: no rounded corners */ .tg-event-block.tg-event-segment-middle { border-radius: 0; border-top: 2px dashed rgba(255, 255, 255, 0.5); border-bottom: 2px dashed rgba(255, 255, 255, 0.5); } /* Continuation indicator: event continues from previous day */ .tg-event-block.tg-event-continuation { border-top-left-radius: 0; border-top-right-radius: 0; } /* Continued indicator: event continues to next day */ .tg-event-block.tg-event-continued { border-bottom-left-radius: 0; border-bottom-right-radius: 0; } /* Combined: middle segment (continuation + continued) */ .tg-event-block.tg-event-continuation.tg-event-continued { border-radius: 0; } /* Visual enhancement: subtle indicator for cross-midnight events */ .tg-event-block.tg-event-continuation::before, .tg-event-block.tg-event-continued::after { content: ""; position: absolute; left: 50%; transform: translateX(-50%); width: 0; height: 0; opacity: 0.7; } /* Top arrow indicator for continuation from previous day */ .tg-event-block.tg-event-continuation::before { top: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; border-top: 6px solid rgba(255, 255, 255, 0.6); } /* Bottom arrow indicator for continuation to next day */ .tg-event-block.tg-event-continued::after { bottom: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; border-bottom: 6px solid rgba(255, 255, 255, 0.6); }