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 ──────────────────────────────────────────