Oberfläche auf das tradeo Corporate Design umstellen

Nur tradeo Blue und Weiß, Neutraltöne als Deckkraft-Abstufungen. Poppins für
Titel und Kennzeichen, Inter für Bedienelemente, beide selbst gehostet.
Kopfzeile als Markenfläche, Listen mit Trennlinien statt Kästen, Linien-SVGs
statt Emojis, Rot nur noch funktional. Design-System unter docs/DESIGN.md
abgelegt und in CLAUDE.md verankert.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ferdi2goandClaude Fable 5.1 committed 2026-09-16 09:33:42 +00:00
1 parent 95756778d8
commit 8a0f903a40
14 files changed
+1160 -190

No files matched your search

+13 -16
View File
@@ -23,6 +23,7 @@ function fmtDate(isoStr) {
function fmtLong(isoStr) {
return new Date(isoStr + 'T12:00:00').toLocaleDateString('de-DE', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
}
const LOCK_ICON = '<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><rect x="5" y="11" width="14" height="10" rx="1.5"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>';
const MONTHS = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'];
let toastTimer;
@@ -66,11 +67,6 @@ function personById(id) { return state.data.persons.find((p) => p.id === id); }
function fullName(p) { return p ? `${p.firstName} ${p.lastName}` : 'unbesetzt'; }
function shortName(p) { return p ? `${p.firstName} ${p.lastName.charAt(0)}.` : '?'; }
function initials(p) { return `${p.firstName.charAt(0)}${p.lastName.charAt(0)}`.toUpperCase(); }
function colorFor(id) {
let h = 0;
for (const c of id) h = (h * 31 + c.charCodeAt(0)) >>> 0;
return `hsl(${h % 360} 55% 45%)`;
}
function isAbsent(personId, date) {
return state.data.absences.some((a) => a.personId === personId && a.from <= date && a.to >= date);
}
@@ -94,7 +90,6 @@ function avatarEl(p, cls = '') {
}
const e = document.createElement('span');
e.className = `avatar ${cls}`;
e.style.background = colorFor(p.id);
e.textContent = initials(p);
e.setAttribute('role', 'img');
e.setAttribute('aria-label', fullName(p));
@@ -141,7 +136,9 @@ function renderCalendar() {
if (date < today) cell.classList.add('past');
const num = document.createElement('div');
num.className = 'num';
num.textContent = cur.getDate();
const numText = document.createElement('span');
numText.textContent = cur.getDate();
num.appendChild(numText);
cell.appendChild(num);
if (outside) { grid.appendChild(cell); continue; }
@@ -162,7 +159,7 @@ function renderCalendar() {
cell.setAttribute('aria-label', `${fmtLong(date)}, kein Meeting`);
} else if (a) {
planned += 1;
if (a.locked) { const lock = document.createElement('span'); lock.className = 'lock'; lock.textContent = '🔒'; lock.title = 'fixiert'; num.appendChild(lock); }
if (a.locked) { const lock = document.createElement('span'); lock.className = 'lock'; lock.innerHTML = LOCK_ICON; lock.title = 'fixiert'; num.appendChild(lock); }
const pair = document.createElement('div');
pair.className = 'pair';
const main = personById(a.main);
@@ -171,7 +168,7 @@ function renderCalendar() {
pair.appendChild(slotEl(main, 'main', date));
pair.appendChild(slotEl(co, 'co', date));
cell.appendChild(pair);
const absentNote = (p) => (p && isAbsent(p.id, date) ? ' (abwesend!)' : '');
const absentNote = (p) => (p && isAbsent(p.id, date) ? ' (abwesend)' : '');
cell.setAttribute('aria-label', `${fmtLong(date)}: Haupt ${fullName(main)}${absentNote(main)}, Neben ${fullName(co)}${absentNote(co)}${a.locked ? ', fixiert' : ''}`);
} else {
cell.setAttribute('aria-label', `${fmtLong(date)}, noch nicht ausgelost`);
@@ -206,7 +203,7 @@ function slotEl(p, role, date) {
name.className = 'name';
name.textContent = shortName(p);
name.title = fullName(p);
if (absent) { name.classList.add('absent'); name.title += ' – abwesend!'; }
if (absent) { name.classList.add('absent'); name.title += ' – abwesend'; }
slot.appendChild(name);
return slot;
}
@@ -734,16 +731,16 @@ function renderAbsences() {
const conflicts = Object.entries(state.data.assignments).filter(([d, x]) => d >= a.from && d <= a.to && !x.off && (x.main === a.personId || x.co === a.personId));
if (conflicts.length) {
const warn = document.createElement('span');
warn.className = 'tag off';
warn.textContent = `${conflicts.length}× eingeplant!`;
warn.className = 'tag conflict';
warn.textContent = `${conflicts.length}× eingeplant`;
warn.title = 'Diese Person ist an Tagen im Kalender eingeplant, an denen sie abwesend ist. Im Kalender neu auslosen.';
li.appendChild(warn);
}
const del = document.createElement('button');
del.type = 'button';
del.className = 'ghost danger';
del.textContent = 'Löschen';
del.addEventListener('click', () => run(() => api(`api/absences/${a.id}`, { method: 'DELETE' }), 'Abwesenheit gelöscht.'));
del.className = 'ghost';
del.textContent = 'Entfernen';
del.addEventListener('click', () => run(() => api(`api/absences/${a.id}`, { method: 'DELETE' }), 'Abwesenheit entfernt.'));
li.appendChild(del);
list.appendChild(li);
}
@@ -797,7 +794,7 @@ function renderSettings() {
if (!h.auto) li.appendChild(tag('manuell'));
const del = document.createElement('button');
del.type = 'button';
del.className = 'ghost danger';
del.className = 'ghost';
del.textContent = 'Entfernen';
del.addEventListener('click', () => run(() => api(`api/holidays/${h.date}`, { method: 'DELETE' }), 'Eintrag entfernt.'));
li.appendChild(del);