1// static/js/calendar/utils.js 2// 3// Pure constants + zero-state helpers for the calendar UI. 4// No DOM, no fetch, no global mutable state â safe to import anywhere. 5 6export const WEEKDAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; 7 8export const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 9 'July', 'August', 'September', 'October', 'November', 'December']; 10 11export const MON_SHORT = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; 12 13export const CAL_PALETTE = [ 14 'var(--accent)', '#5b8abf', '#bf6b5b', '#5bbf7a', '#bf9a5b', 15 '#9a5bbf', '#5bbfb8', '#bf8a5b', '#7070c0', '#bf5b8a', 16]; 17 18export const CAL_COLORS = [ 19 { name: 'default', hex: '' }, 20 // Pale/pastel palette â softer event tints. 21 { name: 'red', hex: '#f0b5ba' }, 22 { name: 'orange', hex: '#e8ccb2' }, 23 { name: 'yellow', hex: '#f2dfbd' }, 24 { name: 'green', hex: '#cce0bc' }, 25 { name: 'blue', hex: '#b0d7f7' }, 26 { name: 'purple', hex: '#e2bcee' }, 27 { name: 'teal', hex: '#abdbe0' }, 28 { name: 'pink', hex: '#f0b5cc' }, 29 // Custom â mirrors the notes color picker. Clicking opens a file picker 30 // and the chosen image URL is stored as a `bg:<url>` sentinel. 31 { name: 'custom', hex: 'custom' }, 32]; 33 34export const _CAL_CUSTOM_GRADIENT = 'conic-gradient(from 0deg, #e06c75, #d19a66, #e5c07b, #98c379, #61afef, #c678dd, #e06c75)'; 35 36// Per-event-type accent palette. Used by the colored dots in month/year 37// grids and the chip stripe behind agenda rows. 38export const _TYPE_PALETTE = { 39 '!': '#e5a33a', // important â amber, less harsh than red 40 work: '#5b8abf', 41 personal: '#a07ae0', 42 health: '#e06c75', 43 travel: '#e5a33a', 44 meal: '#d8b974', 45 social: '#82c882', 46 admin: '#888888', 47 other: '#6b9cb5', 48 untagged: '#555', 49}; 50 51// SVG icon literals reused across the calendar UI. 52export const _trashIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/></svg>'; 53export const _moreIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="12" cy="19" r="2"/></svg>'; 54export const _bellIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>'; 55 56// ââ background CSS helpers ââ 57 58export function _isCalBgImage(c) { 59 return typeof c === 'string' && c.startsWith('bg:'); 60} 61 62export function _calBgImageUrl(c) { 63 return _isCalBgImage(c) ? c.slice(3) : ''; 64} 65 66// Returns a value safe to drop into `style="background:..."`. Falls back to 67// the calendar default for bg-image events in spots where an image would be 68// too small to render usefully (small grid dots, multi-day bars). 69export function _calBgCss(c, fallback) { 70 if (_isCalBgImage(c)) { 71 const u = _calBgImageUrl(c); 72 return u ? `center/cover no-repeat url('${u.replace(/'/g, "\\'")}')` : (fallback || 'var(--accent)'); 73 } 74 return c || fallback || 'var(--accent)'; 75} 76 77function _hexToRgb(c) { 78 if (typeof c !== 'string') return null; 79 const m = c.trim().match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); 80 if (!m) return null; 81 const hex = m[1].length === 3 82 ? m[1].split('').map(ch => ch + ch).join('') 83 : m[1]; 84 return { 85 r: parseInt(hex.slice(0, 2), 16), 86 g: parseInt(hex.slice(2, 4), 16), 87 b: parseInt(hex.slice(4, 6), 16), 88 }; 89} 90 91function _relativeLuminance({ r, g, b }) { 92 return [r, g, b].map(v => { 93 const c = v / 255; 94 return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); 95 }).reduce((sum, c, i) => sum + c * [0.2126, 0.7152, 0.0722][i], 0); 96} 97 98function _contrastRatio(a, b) { 99 const light = Math.max(a, b); 100 const dark = Math.min(a, b); 101 return (light + 0.05) / (dark + 0.05); 102} 103 104export function _calReadableTextColor(bg) { 105 const rgb = _hexToRgb(bg); 106 if (!rgb) return 'var(--fg)'; 107 const lum = _relativeLuminance(rgb); 108 const white = _contrastRatio(lum, 1); 109 const ink = _contrastRatio(lum, 0.006); 110 return ink >= white ? '#111820' : '#ffffff'; 111} 112 113// ââ date helpers ââ 114 115// `YYYY-MM-DD` string from a Date. 116export function _ds(d) { 117 return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); 118} 119 120export function _addDays(dateStr, n) { 121 if (typeof dateStr !== 'string' || !dateStr) return ''; 122 const d = new Date(dateStr + 'T00:00:00'); 123 if (isNaN(d)) return ''; 124 d.setDate(d.getDate() + n); 125 return _ds(d); 126} 127 128export function _shiftDT(iso, days) { 129 if (typeof iso !== 'string' || !iso) return ''; 130 const d = new Date(iso); 131 if (isNaN(d)) return ''; 132 d.setDate(d.getDate() + days); 133 return _ds(d) + (iso.length > 10 ? 'T' + iso.slice(11) : ''); 134} 135 136// Current user's UTC offset as `±HH:MM`. Used to stamp event payloads so 137// the backend can interpret naive datetimes in the user's tz. 138export function _tzOffset() { 139 const o = -new Date().getTimezoneOffset(); 140 const sign = o >= 0 ? '+' : '-'; 141 const h = String(Math.floor(Math.abs(o) / 60)).padStart(2, '0');
142 const m = String(Math.abs(o) % 60).padStart(2, '0'); 143 return `${sign}${h}:${m}`; 144} 145 146// For naive datetimes (no tz suffix), display the date portion as written â 147// TimeTree and many sync tools store "local time" without an offset, so 148// re-interpreting them via the user's tz would shift days. 149// 150// For tz-aware ISO (`Z` or `±HH:MM`), parse as an absolute instant and 151// bucket by the USER's local date. Without this an event at 152// "2026-05-13T22:00:00Z" (07:00 May 14 JST) would render on May 13. 153export function _localDateOf(isoStr) { 154 if (typeof isoStr !== 'string' || !isoStr) return ''; 155 if (isoStr.length === 10) return isoStr; 156 if (/[Zz]$|[+\-]\d{2}:?\d{2}$/.test(isoStr)) { 157 const d = new Date(isoStr); 158 if (!isNaN(d)) { 159 const y = d.getFullYear(); 160 const m = String(d.getMonth() + 1).padStart(2, '0'); 161 const dd = String(d.getDate()).padStart(2, '0'); 162 return `${y}-${m}-${dd}`; 163 } 164 } 165 return isoStr.slice(0, 10); 166}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.