feat(web): group members in the sidebar + per-user member colours

Moved the group member filter from a top bar into the left sidebar (a
"Mitglieder" section that replaces the personal calendar list while a group is
active). Each member row has a checkbox (show/hide their events) and a colour
dot that opens the colour picker to recolour that member just for this viewer —
stored per-device in localStorage and applied over the server colour. Bumped v44.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-06-01 20:32:45 +02:00
parent 817ce075d4
commit fc00bf9114
4 changed files with 73 additions and 56 deletions

View File

@@ -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);

View File

@@ -179,6 +179,14 @@
<div class="mini-cal-days" id="mini-days"></div>
</div>
<!-- Group members (shown only in the group overlay; hide/show people) -->
<div class="cal-list hidden" id="group-members">
<div class="cal-list-header">
<span data-i18n="group_members">Mitglieder</span>
</div>
<div id="group-members-items"></div>
</div>
<!-- Calendar List -->
<div class="cal-list" id="cal-list">
<div class="cal-list-header">

View File

@@ -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 ("<userId>" 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 `<button class="gmf-chip${off ? ' gmf-off' : ''}" data-gmf="${key}">
<span class="gmf-dot" style="background:${color || '#4285f4'}"></span>${escHtml(name)}</button>`;
const gid = state.activeGroupId;
const row = (numKey, colorKey, name, baseColor) => {
const hidden = state.hiddenGroupMembers.has(numKey);
const color = groupColorOverride(gid, colorKey) || baseColor || '#4285f4';
return `<div class="cal-item gm-row">
<input type="checkbox" class="gm-vis" ${hidden ? '' : 'checked'} data-k="${colorKey}" />
<div class="cal-item-dot gm-dot" style="background:${color}" data-k="${colorKey}" data-color="${color}" title="${t('change_color')}"></div>
<span class="cal-item-name">${escHtml(name)}</span>
</div>`;
};
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).

View File

@@ -1,2 +1,2 @@
// Increment APP_VERSION with every code change
export const APP_VERSION = 'v43';
export const APP_VERSION = 'v44';