diff --git a/frontend/css/app.css b/frontend/css/app.css index 981a443..e3f6e06 100644 --- a/frontend/css/app.css +++ b/frontend/css/app.css @@ -1840,31 +1840,9 @@ a { color: var(--primary); text-decoration: none; } font-size: 14px; color: var(--text-1); } -/* Per-member filter bar under the group banner (hide individual people). */ -.group-members-filter { - display: flex; - flex-wrap: wrap; - gap: 6px; - padding: 8px 16px; - background: color-mix(in srgb, var(--accent) 7%, var(--bg-app)); - border-bottom: 1px solid var(--border); -} -.gmf-chip { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 4px 10px; - border-radius: 999px; - border: 1px solid var(--border); - background: var(--bg-surface); - color: var(--text-1); - font-size: 12.5px; - cursor: pointer; - transition: opacity var(--transition), background var(--transition); -} -.gmf-chip:hover { background: var(--bg-hover); } -.gmf-chip.gmf-off { opacity: 0.4; text-decoration: line-through; } -.gmf-dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; } +/* Group-member rows in the sidebar (colour dot = per-user colour, checkbox = + show/hide). Reuse the calendar-list item styling. */ +.gm-row .gm-dot { cursor: pointer; } .group-item-active { background: var(--bg-surface); diff --git a/frontend/index.html b/frontend/index.html index b9953f2..49f0b92 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -179,6 +179,14 @@
+ + +
diff --git a/frontend/js/calendar.js b/frontend/js/calendar.js index fb92bd3..2a40ee8 100644 --- a/frontend/js/calendar.js +++ b/frontend/js/calendar.js @@ -280,6 +280,22 @@ function ownerColor(ownerId) { return OWNER_PALETTE[(Number(ownerId) >>> 0) % OWNER_PALETTE.length]; } +// Per-user (per-device) member-colour overrides for the group overlay, so each +// viewer can recolour members just for their own view. Kept in localStorage, +// keyed by group id then member key ("" or "gc" for the group calendar). +function loadGroupColors() { + try { return JSON.parse(localStorage.getItem('groupMemberColors') || '{}'); } catch (e) { return {}; } +} +function groupColorOverride(groupId, key) { + return (loadGroupColors()[groupId] || {})[key] || null; +} +function setGroupColorOverride(groupId, key, hex) { + const all = loadGroupColors(); + all[groupId] = all[groupId] || {}; + if (hex) all[groupId][key] = hex; else delete all[groupId][key]; + try { localStorage.setItem('groupMemberColors', JSON.stringify(all)); } catch (e) { /* ignore */ } +} + async function fetchAndRender(force = false, silent = false) { const { start, end } = getViewRange(); @@ -312,8 +328,11 @@ async function fetchAndRender(force = false, silent = false) { title = ev.title; } } - // Server-defined colour (member colour / group colour) so web + apps match. - const color = ev.display_color || ownerColor(ownerId) || ev.color; + // Colour: this viewer's local override first, else the server-defined + // member/group colour, else a palette fallback. + const ownerKey = ev.is_group_event ? 'gc' : (ownerId != null ? String(ownerId) : null); + const override = ownerKey ? groupColorOverride(state.activeGroupId, ownerKey) : null; + const color = override || ev.display_color || ownerColor(ownerId) || ev.color; return { ...ev, title, color }; }); eventCache.start = null; eventCache.end = null; // invalidate normal cache @@ -2353,41 +2372,53 @@ function exitGroupView() { fetchAndRender(true); } -// Per-member filter bar shown under the group banner: tap a member (or the -// group calendar) to hide/show their events in the combined overlay. +// Group members live in the sidebar (like the calendar list): each row has a +// colour dot (tap to recolour that member just for your view) and a checkbox to +// hide/show them in the combined overlay. The personal calendar list is hidden +// while a group is active. function renderGroupMemberFilter() { - const banner = document.getElementById('group-view-banner'); - if (!banner) return; - let bar = document.getElementById('group-members-filter'); - if (!state.activeGroupId || !state.activeGroupDetail) { - if (bar) bar.remove(); - return; - } - if (!bar) { - bar = document.createElement('div'); - bar.id = 'group-members-filter'; - bar.className = 'group-members-filter'; - banner.insertAdjacentElement('afterend', bar); - } + const section = document.getElementById('group-members'); + const items = document.getElementById('group-members-items'); + const calList = document.getElementById('cal-list'); + const active = !!(state.activeGroupId && state.activeGroupDetail); + if (section) section.classList.toggle('hidden', !active); + if (calList) calList.classList.toggle('hidden', !!state.activeGroupId); + if (!active || !items) return; + const g = state.activeGroupDetail; - const chip = (key, name, color) => { - const off = state.hiddenGroupMembers.has(key); - return ``; + const gid = state.activeGroupId; + const row = (numKey, colorKey, name, baseColor) => { + const hidden = state.hiddenGroupMembers.has(numKey); + const color = groupColorOverride(gid, colorKey) || baseColor || '#4285f4'; + return `
+ +
+ ${escHtml(name)} +
`; }; - const parts = (g.members || []).map(m => chip(String(m.id), m.display_name || '—', m.color)); - parts.push(chip('gc', t('group_calendar'), g.group_calendar_color)); - bar.innerHTML = parts.join(''); - bar.querySelectorAll('[data-gmf]').forEach(b => { - b.addEventListener('click', () => { - const raw = b.dataset.gmf; - const key = raw === 'gc' ? 'gc' : parseInt(raw); - if (state.hiddenGroupMembers.has(key)) state.hiddenGroupMembers.delete(key); + const parts = (g.members || []).map(m => row(m.id, String(m.id), m.display_name || '—', m.color)); + parts.push(row('gc', 'gc', t('group_calendar'), g.group_calendar_color)); + items.innerHTML = parts.join(''); + + items.querySelectorAll('.gm-vis').forEach(cb => { + cb.addEventListener('change', () => { + const ck = cb.dataset.k; + const key = ck === 'gc' ? 'gc' : parseInt(ck); + if (cb.checked) state.hiddenGroupMembers.delete(key); else state.hiddenGroupMembers.add(key); - renderGroupMemberFilter(); renderView(); }); }); + items.querySelectorAll('.gm-dot').forEach(dot => { + dot.addEventListener('click', async () => { + const picked = await openColorPicker(dot, dot.dataset.color || '#4285f4'); + if (picked) { + setGroupColorOverride(gid, dot.dataset.k, picked); + renderGroupMemberFilter(); + fetchAndRender(true); + } + }); + }); } // Open the group modal in create mode (no id) or manage mode (existing group). diff --git a/frontend/js/version.js b/frontend/js/version.js index a139d4f..edad6d1 100644 --- a/frontend/js/version.js +++ b/frontend/js/version.js @@ -1,2 +1,2 @@ // Increment APP_VERSION with every code change -export const APP_VERSION = 'v43'; +export const APP_VERSION = 'v44';