diff --git a/frontend/css/app.css b/frontend/css/app.css index 59ea871..8fbcd34 100644 --- a/frontend/css/app.css +++ b/frontend/css/app.css @@ -193,6 +193,76 @@ a { color: var(--primary); text-decoration: none; } .color-swatch:hover { transform: scale(1.15); } .color-swatch.active { border-color: var(--text-1); } +.ev-color-row { display: flex; align-items: center; gap: 8px; } +.ev-color-hex { + flex: 1; height: 36px; padding: 0 10px; + background: var(--bg-hover); border: 1px solid var(--border); + border-radius: var(--radius-sm); color: var(--text-1); + font-family: 'Roboto Mono', monospace; font-size: 13px; +} +.ev-color-hex:focus { outline: none; border-color: var(--primary); } +.ev-color-preview { + width: 36px; height: 36px; border-radius: var(--radius-sm); + border: 1px solid var(--border); cursor: pointer; + flex-shrink: 0; transition: box-shadow var(--transition); +} +.ev-color-preview:hover { box-shadow: 0 0 0 2px var(--primary); } + +/* ── Gradient Color Picker (Dark) ──────────────────────── */ +.gcp { + position: fixed; z-index: 600; + width: 252px; padding: 16px; + background: var(--bg-surface); border: 1px solid var(--border); + border-radius: var(--radius); box-shadow: var(--shadow-lg); +} +.gcp-sv { + display: block; width: 100%; height: 160px; + border-radius: var(--radius-sm); cursor: crosshair; +} +.gcp-sv-cursor { + position: absolute; width: 14px; height: 14px; + border: 2px solid #fff; border-radius: 50%; + box-shadow: 0 0 0 1px rgba(0,0,0,.4), inset 0 0 0 1px rgba(0,0,0,.3); + transform: translate(-50%, -50%); pointer-events: none; +} +.gcp-hue-track { + position: relative; margin-top: 12px; height: 14px; cursor: pointer; +} +.gcp-hue { + display: block; width: 100%; height: 14px; + border-radius: 7px; cursor: pointer; +} +.gcp-hue-thumb { + position: absolute; top: 50%; width: 18px; height: 18px; + border: 2px solid #fff; border-radius: 50%; + box-shadow: 0 0 0 1px rgba(0,0,0,.3), 0 1px 4px rgba(0,0,0,.4); + transform: translate(-50%, -50%); pointer-events: none; + background: transparent; +} +.gcp-bottom { + display: flex; align-items: center; gap: 10px; margin-top: 12px; +} +.gcp-preview { + width: 36px; height: 36px; border-radius: var(--radius-sm); + border: 1px solid var(--border); flex-shrink: 0; +} +.gcp-hex { + flex: 1; height: 36px; padding: 0 10px; + background: var(--bg-hover); border: 1px solid var(--border); + border-radius: var(--radius-sm); color: var(--text-1); + font-family: 'Roboto Mono', monospace; font-size: 13px; + outline: none; +} +.gcp-hex:focus { border-color: var(--primary); } +.gcp-select { + display: block; width: 100%; margin-top: 12px; + padding: 8px 0; background: var(--primary); color: #fff; + border: none; border-radius: var(--radius-sm); + font-weight: 600; font-size: 13px; cursor: pointer; + transition: opacity var(--transition); +} +.gcp-select:hover { opacity: .85; } + /* ── Top Bar ────────────────────────────────────────────── */ .topbar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; @@ -321,21 +391,6 @@ a { color: var(--primary); text-decoration: none; } width: 14px; height: 14px; border-radius: 3px; flex-shrink: 0; cursor: pointer; } .cal-item-dot:hover { outline: 2px solid var(--text-2); outline-offset: 1px; } -.cal-color-picker { - position: fixed; z-index: 500; - display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; - padding: 10px; - background: var(--bg-surface); border: 1px solid var(--border); - border-radius: var(--radius); box-shadow: var(--shadow-lg); -} -.cal-cp-swatch { - width: 28px; height: 28px; border-radius: 50%; - cursor: pointer; transition: transform .1s, box-shadow .1s; -} -.cal-cp-swatch:hover { - transform: scale(1.2); - box-shadow: 0 0 0 2px var(--bg-surface), 0 0 0 4px currentColor; -} .cal-item input[type=checkbox] { accent-color: var(--primary); width: 14px; height: 14px; } .cal-item-name { font-size: 13px; flex: 1; color: var(--text-1); cursor: default; } .cal-rename-input { diff --git a/frontend/index.html b/frontend/index.html index 7d5845d..0a18c36 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -220,16 +220,9 @@
-
-
-
-
-
-
-
-
-
-
+
+ +
diff --git a/frontend/js/calendar.js b/frontend/js/calendar.js index 99e3c9c..cead487 100644 --- a/frontend/js/calendar.js +++ b/frontend/js/calendar.js @@ -3,6 +3,7 @@ import { applyTheme, isToday, isSameDay, toLocalDatetimeInput, toDateInput, date import { renderMonth } from './views/month.js'; import { renderWeek } from './views/week.js'; import { renderAgenda } from './views/agenda.js'; +import { openColorPicker } from './color-picker.js'; // Fetch avatar image as blob URL (with auth header) function fetchAvatarBlob() { @@ -516,9 +517,10 @@ function toggleAlldayFields(allDay) { function resetColorPicker(color) { state.selectedEventColor = color; - document.querySelectorAll('#ev-color-picker .color-swatch').forEach(sw => { - sw.classList.toggle('active', (sw.dataset.color || '') === color); - }); + const hex = document.getElementById('ev-color-hex'); + const preview = document.getElementById('ev-color-preview'); + hex.value = color ? color.toUpperCase() : ''; + preview.style.background = color || 'var(--primary)'; } function bindEventModal() { @@ -526,11 +528,22 @@ function bindEventModal() { toggleAlldayFields(e.target.checked); }); - document.querySelectorAll('#ev-color-picker .color-swatch').forEach(sw => { - sw.addEventListener('click', () => { - state.selectedEventColor = sw.dataset.color || ''; - resetColorPicker(state.selectedEventColor); - }); + // Color picker: click preview to open gradient picker + const evColorPreview = document.getElementById('ev-color-preview'); + const evColorHex = document.getElementById('ev-color-hex'); + + evColorPreview.addEventListener('click', async () => { + const current = state.selectedEventColor || '#4285f4'; + const picked = await openColorPicker(evColorPreview, current); + if (picked) resetColorPicker(picked); + }); + + evColorHex.addEventListener('change', () => { + let val = evColorHex.value.trim(); + if (!val.startsWith('#')) val = '#' + val; + if (/^#[0-9a-fA-F]{6}$/.test(val)) { + resetColorPicker(val); + } }); document.getElementById('ev-save').onclick = async () => { @@ -943,71 +956,26 @@ function updateTopbarAvatar(hasAvatar) { } // ── Calendar Color Picker ───────────────────────────────── -const CAL_COLORS = [ - '#4285f4', '#7986cb', '#8e24aa', '#e67c73', - '#f4511e', '#f6bf26', '#33b679', '#0b8043', - '#039be5', '#616161', '#3f51b5', '#d50000', - '#e4c441', '#009688', '#795548', '#ef6c00', -]; - -let activeColorPicker = null; - -function openCalColorPicker(anchor, calId) { - closeCalColorPicker(); - - const rect = anchor.getBoundingClientRect(); - const picker = document.createElement('div'); - picker.className = 'cal-color-picker'; - picker.innerHTML = CAL_COLORS.map(c => - `
` - ).join(''); - - picker.style.top = (rect.bottom + 6) + 'px'; - picker.style.left = rect.left + 'px'; - - // Ensure picker stays in viewport - document.body.appendChild(picker); - const pRect = picker.getBoundingClientRect(); - if (pRect.right > window.innerWidth - 8) { - picker.style.left = (window.innerWidth - pRect.width - 8) + 'px'; +async function openCalColorPicker(anchor, calId) { + // Find current color of the calendar + let currentColor = '#4285f4'; + for (const acc of state.accounts) { + for (const cal of acc.calendars) { + if (cal.id === calId && cal.color) currentColor = cal.color; + } } - picker.querySelectorAll('.cal-cp-swatch').forEach(sw => { - sw.addEventListener('click', async (e) => { - e.stopPropagation(); - const color = sw.dataset.color; - await api.put(`/caldav/calendars/${calId}`, { color }); - for (const acc of state.accounts) { - for (const cal of acc.calendars) { - if (cal.id === calId) cal.color = color; - } - } - closeCalColorPicker(); - renderCalendarList(); - fetchAndRender(); - }); - }); + const picked = await openColorPicker(anchor, currentColor); + if (!picked) return; - activeColorPicker = picker; - - // Close on outside click (next tick to avoid immediate close) - setTimeout(() => { - document.addEventListener('click', closeCalColorPickerOutside); - }, 0); -} - -function closeCalColorPicker() { - if (activeColorPicker) { - activeColorPicker.remove(); - activeColorPicker = null; - document.removeEventListener('click', closeCalColorPickerOutside); - } -} - -function closeCalColorPickerOutside(e) { - if (activeColorPicker && !activeColorPicker.contains(e.target)) { - closeCalColorPicker(); + await api.put(`/caldav/calendars/${calId}`, { color: picked }); + for (const acc of state.accounts) { + for (const cal of acc.calendars) { + if (cal.id === calId) cal.color = picked; + } } + renderCalendarList(); + fetchAndRender(); } // ── Avatar Crop ──────────────────────────────────────────