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:
1 parent
95756778d8
commit
8a0f903a40
14 files changed
+1160
-190
No files matched your search
+13
-16
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user