feat: Dashboard-Widgets mit dynamischen Daten und neuem Design
- Begrüßungs-Widget mit Stats-Chips (dringende Aufgaben, heutige Termine, Mittagessen) - Aufgaben- und Termine-Widgets mit Count-Badge im Header - Essen-Widget als 4-Slot-Raster (Frühstück/Mittagessen/Abendessen/Snack) mit Lucide-Icons - Notizen als Kachel-Grid statt Liste - event-time-badge, widget__badge, greeting-chip, meal-slots, notes-grid-widget CSS - Hover-Lift-Effekt auf Widgets (Desktop) - Widget-Empty-States mit zentrierten Icons Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+131
-94
@@ -62,10 +62,35 @@ const MEAL_LABELS = {
|
|||||||
snack: 'Snack',
|
snack: 'Snack',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MEAL_ICONS = {
|
||||||
|
breakfast: 'sunrise',
|
||||||
|
lunch: 'sun',
|
||||||
|
dinner: 'moon',
|
||||||
|
snack: 'apple',
|
||||||
|
};
|
||||||
|
|
||||||
function initials(name = '') {
|
function initials(name = '') {
|
||||||
return name.split(' ').map((w) => w[0]).join('').slice(0, 2).toUpperCase();
|
return name.split(' ').map((w) => w[0]).join('').slice(0, 2).toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function widgetHeader(icon, title, count, linkHref, linkLabel = 'Alle') {
|
||||||
|
const badge = count != null
|
||||||
|
? `<span class="widget__badge">${count}</span>`
|
||||||
|
: '';
|
||||||
|
return `
|
||||||
|
<div class="widget__header">
|
||||||
|
<span class="widget__title">
|
||||||
|
<i data-lucide="${icon}" class="widget__title-icon" aria-hidden="true"></i>
|
||||||
|
${title}
|
||||||
|
${badge}
|
||||||
|
</span>
|
||||||
|
<a href="${linkHref}" data-route="${linkHref}" class="widget__link">
|
||||||
|
${linkLabel} <i data-lucide="chevron-right" style="width:12px;height:12px;vertical-align:-2px;"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// Skeleton
|
// Skeleton
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -88,37 +113,52 @@ function skeletonWidget(lines = 3) {
|
|||||||
// Widget-Renderer
|
// Widget-Renderer
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
function renderGreeting(user) {
|
function renderGreeting(user, stats = {}) {
|
||||||
|
const { urgentCount = 0, todayEventCount = 0, todayMealTitle = null } = stats;
|
||||||
|
|
||||||
|
const statChips = [];
|
||||||
|
if (urgentCount > 0)
|
||||||
|
statChips.push(`<span class="greeting-chip greeting-chip--warn">
|
||||||
|
<i data-lucide="alert-circle" style="width:12px;height:12px;"></i>
|
||||||
|
${urgentCount} dring. Aufgabe${urgentCount > 1 ? 'n' : ''}
|
||||||
|
</span>`);
|
||||||
|
if (todayEventCount > 0)
|
||||||
|
statChips.push(`<span class="greeting-chip">
|
||||||
|
<i data-lucide="calendar" style="width:12px;height:12px;"></i>
|
||||||
|
${todayEventCount} Termin${todayEventCount > 1 ? 'e' : ''} heute
|
||||||
|
</span>`);
|
||||||
|
if (todayMealTitle)
|
||||||
|
statChips.push(`<span class="greeting-chip">
|
||||||
|
<i data-lucide="utensils" style="width:12px;height:12px;"></i>
|
||||||
|
Heute: ${todayMealTitle}
|
||||||
|
</span>`);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="widget-greeting">
|
<div class="widget-greeting">
|
||||||
|
<div class="widget-greeting__content">
|
||||||
<div class="widget-greeting__title">${greeting(user.display_name)}</div>
|
<div class="widget-greeting__title">${greeting(user.display_name)}</div>
|
||||||
<div class="widget-greeting__date">${formatDate()}</div>
|
<div class="widget-greeting__date">${formatDate()}</div>
|
||||||
|
${statChips.length ? `<div class="widget-greeting__chips">${statChips.join('')}</div>` : ''}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderUrgentTasks(tasks) {
|
function renderUrgentTasks(tasks) {
|
||||||
const header = `
|
|
||||||
<div class="widget__header">
|
|
||||||
<span class="widget__title">
|
|
||||||
<i data-lucide="alert-circle" class="widget__title-icon" aria-hidden="true"></i>
|
|
||||||
Dringende Aufgaben
|
|
||||||
</span>
|
|
||||||
<a href="/tasks" data-route="/tasks" class="widget__link">Alle</a>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (!tasks.length) {
|
if (!tasks.length) {
|
||||||
return `<div class="widget">${header}
|
return `<div class="widget">
|
||||||
<div class="widget__empty">Keine dringenden Aufgaben. ✓</div>
|
${widgetHeader('check-square', 'Aufgaben', 0, '/tasks')}
|
||||||
|
<div class="widget__empty">
|
||||||
|
<i data-lucide="check-circle" style="width:32px;height:32px;color:var(--color-success);margin-bottom:var(--space-2);"></i>
|
||||||
|
<div>Alles erledigt</div>
|
||||||
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = tasks.map((t) => {
|
const items = tasks.map((t) => {
|
||||||
const due = formatDueDate(t.due_date);
|
const due = formatDueDate(t.due_date);
|
||||||
return `
|
return `
|
||||||
<div class="task-item" data-route="/tasks" role="button" tabindex="0"
|
<div class="task-item" data-route="/tasks" role="button" tabindex="0">
|
||||||
aria-label="Aufgabe: ${t.title}">
|
|
||||||
<div class="task-item__priority task-item__priority--${t.priority}"></div>
|
<div class="task-item__priority task-item__priority--${t.priority}"></div>
|
||||||
<div class="task-item__content">
|
<div class="task-item__content">
|
||||||
<div class="task-item__title">${t.title}</div>
|
<div class="task-item__title">${t.title}</div>
|
||||||
@@ -126,106 +166,97 @@ function renderUrgentTasks(tasks) {
|
|||||||
</div>
|
</div>
|
||||||
${t.assigned_color ? `
|
${t.assigned_color ? `
|
||||||
<div class="task-item__avatar" style="background-color:${t.assigned_color}"
|
<div class="task-item__avatar" style="background-color:${t.assigned_color}"
|
||||||
title="${t.assigned_name || ''}">
|
title="${t.assigned_name || ''}">${initials(t.assigned_name || '')}</div>` : ''}
|
||||||
${initials(t.assigned_name || '')}
|
|
||||||
</div>` : ''}
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
return `<div class="widget">${header}<div class="widget__body">${items}</div></div>`;
|
return `<div class="widget">
|
||||||
}
|
${widgetHeader('check-square', 'Aufgaben', tasks.length, '/tasks')}
|
||||||
|
<div class="widget__body">${items}</div>
|
||||||
function renderUpcomingEvents(events) {
|
|
||||||
const header = `
|
|
||||||
<div class="widget__header">
|
|
||||||
<span class="widget__title">
|
|
||||||
<i data-lucide="calendar" class="widget__title-icon" aria-hidden="true"></i>
|
|
||||||
Anstehende Termine
|
|
||||||
</span>
|
|
||||||
<a href="/calendar" data-route="/calendar" class="widget__link">Alle</a>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (!events.length) {
|
|
||||||
return `<div class="widget">${header}
|
|
||||||
<div class="widget__empty">Keine anstehenden Termine.</div>
|
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = events.map((e) => `
|
function renderUpcomingEvents(events) {
|
||||||
<div class="event-item" data-route="/calendar" role="button" tabindex="0"
|
if (!events.length) {
|
||||||
aria-label="Termin: ${e.title}">
|
return `<div class="widget">
|
||||||
<div class="event-item__bar"
|
${widgetHeader('calendar', 'Termine', 0, '/calendar')}
|
||||||
style="background-color:${e.assigned_color || e.color || 'var(--color-accent)'}"></div>
|
<div class="widget__empty">
|
||||||
|
<i data-lucide="calendar-check" style="width:32px;height:32px;color:var(--color-text-disabled);margin-bottom:var(--space-2);"></i>
|
||||||
|
<div>Keine Termine</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const today = new Date().toDateString();
|
||||||
|
const items = events.map((e) => {
|
||||||
|
const d = new Date(e.start_datetime);
|
||||||
|
const isToday = d.toDateString() === today;
|
||||||
|
const timeStr = e.all_day ? 'Ganztägig' : d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }) + ' Uhr';
|
||||||
|
return `
|
||||||
|
<div class="event-item" data-route="/calendar" role="button" tabindex="0">
|
||||||
|
<div class="event-item__bar" style="background-color:${e.color || 'var(--color-accent)'}"></div>
|
||||||
<div class="event-item__content">
|
<div class="event-item__content">
|
||||||
<div class="event-item__title">${e.title}</div>
|
<div class="event-item__title">${e.title}</div>
|
||||||
<div class="event-item__time">
|
<div class="event-item__time">
|
||||||
${e.all_day ? formatDate(new Date(e.start_datetime)) : formatDateTime(e.start_datetime)}
|
<span class="event-time-badge ${isToday ? 'event-time-badge--today' : ''}">${isToday ? 'Heute' : formatDateTime(e.start_datetime).split(',')[0]}</span>
|
||||||
|
${timeStr}
|
||||||
${e.location ? ` · ${e.location}` : ''}
|
${e.location ? ` · ${e.location}` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
|
||||||
|
|
||||||
return `<div class="widget">${header}<div class="widget__body">${items}</div></div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTodayMeals(meals) {
|
|
||||||
const header = `
|
|
||||||
<div class="widget__header">
|
|
||||||
<span class="widget__title">
|
|
||||||
<i data-lucide="utensils" class="widget__title-icon" aria-hidden="true"></i>
|
|
||||||
Heute essen
|
|
||||||
</span>
|
|
||||||
<a href="/meals" data-route="/meals" class="widget__link">Alle</a>
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
if (!meals.length) {
|
return `<div class="widget">
|
||||||
return `<div class="widget">${header}
|
${widgetHeader('calendar', 'Termine', events.length, '/calendar')}
|
||||||
<div class="widget__empty">Kein Essensplan für heute.</div>
|
<div class="widget__body">${items}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = meals.map((m) => `
|
function renderTodayMeals(meals) {
|
||||||
<div class="meal-item" data-route="/meals" role="button" tabindex="0"
|
const MEAL_ORDER = ['breakfast', 'lunch', 'dinner', 'snack'];
|
||||||
aria-label="${MEAL_LABELS[m.meal_type]}: ${m.title}">
|
|
||||||
<span class="meal-item__type-badge">${MEAL_LABELS[m.meal_type]}</span>
|
|
||||||
<span class="meal-item__title">${m.title}</span>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
|
|
||||||
return `<div class="widget">${header}<div class="widget__body">${items}</div></div>`;
|
const slots = MEAL_ORDER.map((type) => {
|
||||||
|
const meal = meals.find((m) => m.meal_type === type);
|
||||||
|
return `
|
||||||
|
<div class="meal-slot ${meal ? 'meal-slot--filled' : ''}" data-route="/meals" role="button" tabindex="0">
|
||||||
|
<i data-lucide="${MEAL_ICONS[type]}" class="meal-slot__icon" aria-hidden="true"></i>
|
||||||
|
<div class="meal-slot__type">${MEAL_LABELS[type]}</div>
|
||||||
|
<div class="meal-slot__title">${meal ? meal.title : '—'}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
return `<div class="widget widget--meals">
|
||||||
|
${widgetHeader('utensils', 'Heute essen', null, '/meals', 'Woche')}
|
||||||
|
<div class="meal-slots">${slots}</div>
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPinnedNotes(notes) {
|
function renderPinnedNotes(notes) {
|
||||||
const header = `
|
|
||||||
<div class="widget__header">
|
|
||||||
<span class="widget__title">
|
|
||||||
<i data-lucide="pin" class="widget__title-icon" aria-hidden="true"></i>
|
|
||||||
Pinnwand
|
|
||||||
</span>
|
|
||||||
<a href="/notes" data-route="/notes" class="widget__link">Alle</a>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (!notes.length) {
|
if (!notes.length) {
|
||||||
return `<div class="widget">${header}
|
return `<div class="widget">
|
||||||
<div class="widget__empty">Keine angepinnten Notizen.</div>
|
${widgetHeader('pin', 'Pinnwand', 0, '/notes')}
|
||||||
|
<div class="widget__empty">
|
||||||
|
<i data-lucide="sticky-note" style="width:32px;height:32px;color:var(--color-text-disabled);margin-bottom:var(--space-2);"></i>
|
||||||
|
<div>Keine angepinnten Notizen</div>
|
||||||
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = notes.map((n) => `
|
const items = notes.map((n) => `
|
||||||
<div class="note-item" data-route="/notes" role="button" tabindex="0"
|
<div class="note-item" data-route="/notes" role="button" tabindex="0"
|
||||||
style="background-color:${n.color}22; border-left-color:${n.color};"
|
style="--note-color:${n.color};">
|
||||||
aria-label="Notiz${n.title ? ': ' + n.title : ''}">
|
|
||||||
${n.title ? `<div class="note-item__title">${n.title}</div>` : ''}
|
${n.title ? `<div class="note-item__title">${n.title}</div>` : ''}
|
||||||
<div class="note-item__content">${n.content}</div>
|
<div class="note-item__content">${n.content}</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).join('');
|
||||||
|
|
||||||
return `<div class="widget">${header}<div class="widget__body">${items}</div></div>`;
|
return `<div class="widget widget--wide">
|
||||||
|
${widgetHeader('pin', 'Pinnwand', notes.length, '/notes')}
|
||||||
|
<div class="notes-grid-widget">${items}</div>
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -234,12 +265,8 @@ function renderPinnedNotes(notes) {
|
|||||||
|
|
||||||
const WEATHER_ICON_BASE = 'https://openweathermap.org/img/wn/';
|
const WEATHER_ICON_BASE = 'https://openweathermap.org/img/wn/';
|
||||||
|
|
||||||
function weatherIconUrl(icon) {
|
|
||||||
return `${WEATHER_ICON_BASE}${icon}@2x.png`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderWeatherWidget(weather) {
|
function renderWeatherWidget(weather) {
|
||||||
if (!weather) return ''; // Kein API-Key → Widget ausblenden
|
if (!weather) return '';
|
||||||
|
|
||||||
const { city, current, forecast } = weather;
|
const { city, current, forecast } = weather;
|
||||||
|
|
||||||
@@ -249,7 +276,7 @@ function renderWeatherWidget(weather) {
|
|||||||
return `
|
return `
|
||||||
<div class="weather-forecast__day">
|
<div class="weather-forecast__day">
|
||||||
<div class="weather-forecast__label">${label}</div>
|
<div class="weather-forecast__label">${label}</div>
|
||||||
<img class="weather-forecast__icon" src="${weatherIconUrl(d.icon)}"
|
<img class="weather-forecast__icon" src="${WEATHER_ICON_BASE}${d.icon}@2x.png"
|
||||||
alt="${d.desc}" width="32" height="32" loading="lazy">
|
alt="${d.desc}" width="32" height="32" loading="lazy">
|
||||||
<div class="weather-forecast__temps">
|
<div class="weather-forecast__temps">
|
||||||
<span class="weather-forecast__high">${d.temp_max}°</span>
|
<span class="weather-forecast__high">${d.temp_max}°</span>
|
||||||
@@ -266,10 +293,10 @@ function renderWeatherWidget(weather) {
|
|||||||
<div class="weather-widget__desc">${current.desc}</div>
|
<div class="weather-widget__desc">${current.desc}</div>
|
||||||
<div class="weather-widget__city">${city}</div>
|
<div class="weather-widget__city">${city}</div>
|
||||||
<div class="weather-widget__meta">
|
<div class="weather-widget__meta">
|
||||||
Gefühlt ${current.feels_like}° · ${current.humidity}% Luftfeuchtigkeit · Wind ${current.wind_speed} km/h
|
Gefühlt ${current.feels_like}° · ${current.humidity}% · Wind ${current.wind_speed} km/h
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<img class="weather-widget__icon" src="${weatherIconUrl(current.icon)}"
|
<img class="weather-widget__icon" src="${WEATHER_ICON_BASE}${current.icon}@2x.png"
|
||||||
alt="${current.desc}" width="80" height="80" loading="lazy">
|
alt="${current.desc}" width="80" height="80" loading="lazy">
|
||||||
</div>
|
</div>
|
||||||
${forecast.length ? `<div class="weather-forecast">${forecastHtml}</div>` : ''}
|
${forecast.length ? `<div class="weather-forecast">${forecastHtml}</div>` : ''}
|
||||||
@@ -348,7 +375,7 @@ function initFab(container) {
|
|||||||
|
|
||||||
function wireLinks(container) {
|
function wireLinks(container) {
|
||||||
container.querySelectorAll('[data-route]').forEach((el) => {
|
container.querySelectorAll('[data-route]').forEach((el) => {
|
||||||
if (el.id === 'fab-main' || el.closest('#fab-actions')) return; // FAB separat
|
if (el.id === 'fab-main' || el.closest('#fab-actions')) return;
|
||||||
const go = () => window.oikos.navigate(el.dataset.route);
|
const go = () => window.oikos.navigate(el.dataset.route);
|
||||||
if (el.tagName === 'A') {
|
if (el.tagName === 'A') {
|
||||||
el.addEventListener('click', (e) => { e.preventDefault(); go(); });
|
el.addEventListener('click', (e) => { e.preventDefault(); go(); });
|
||||||
@@ -366,14 +393,15 @@ function wireLinks(container) {
|
|||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
export async function render(container, { user }) {
|
export async function render(container, { user }) {
|
||||||
// Sofort Skeleton
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="dashboard">
|
<div class="dashboard">
|
||||||
<div class="dashboard__grid">
|
<div class="dashboard__grid">
|
||||||
<div class="widget-greeting" style="grid-column:1/-1">
|
<div class="widget-greeting" style="grid-column:1/-1">
|
||||||
|
<div class="widget-greeting__content">
|
||||||
<div class="widget-greeting__title">${greeting(user.display_name)}</div>
|
<div class="widget-greeting__title">${greeting(user.display_name)}</div>
|
||||||
<div class="widget-greeting__date">${formatDate()}</div>
|
<div class="widget-greeting__date">${formatDate()}</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
${skeletonWidget(3)}
|
${skeletonWidget(3)}
|
||||||
${skeletonWidget(3)}
|
${skeletonWidget(3)}
|
||||||
${skeletonWidget(2)}
|
${skeletonWidget(2)}
|
||||||
@@ -384,7 +412,6 @@ export async function render(container, { user }) {
|
|||||||
`;
|
`;
|
||||||
initFab(container);
|
initFab(container);
|
||||||
|
|
||||||
// Daten laden (Dashboard + Wetter parallel)
|
|
||||||
let data = { upcomingEvents: [], urgentTasks: [], todayMeals: [], pinnedNotes: [] };
|
let data = { upcomingEvents: [], urgentTasks: [], todayMeals: [], pinnedNotes: [] };
|
||||||
let weather = null;
|
let weather = null;
|
||||||
try {
|
try {
|
||||||
@@ -399,11 +426,21 @@ export async function render(container, { user }) {
|
|||||||
window.oikos?.showToast('Dashboard konnte nicht vollständig geladen werden.', 'warning');
|
window.oikos?.showToast('Dashboard konnte nicht vollständig geladen werden.', 'warning');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Widgets rendern
|
const today = new Date().toDateString();
|
||||||
|
const stats = {
|
||||||
|
urgentCount: data.urgentTasks?.length ?? 0,
|
||||||
|
todayEventCount: (data.upcomingEvents ?? []).filter((e) =>
|
||||||
|
new Date(e.start_datetime).toDateString() === today
|
||||||
|
).length,
|
||||||
|
todayMealTitle: (data.todayMeals ?? []).find((m) => m.meal_type === 'lunch')?.title
|
||||||
|
?? (data.todayMeals ?? [])[0]?.title
|
||||||
|
?? null,
|
||||||
|
};
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="dashboard">
|
<div class="dashboard">
|
||||||
<div class="dashboard__grid">
|
<div class="dashboard__grid">
|
||||||
${renderGreeting(user)}
|
${renderGreeting(user, stats)}
|
||||||
${renderWeatherWidget(weather)}
|
${renderWeatherWidget(weather)}
|
||||||
${renderUrgentTasks(data.urgentTasks ?? [])}
|
${renderUrgentTasks(data.urgentTasks ?? [])}
|
||||||
${renderUpcomingEvents(data.upcomingEvents ?? [])}
|
${renderUpcomingEvents(data.upcomingEvents ?? [])}
|
||||||
|
|||||||
+190
-26
@@ -58,18 +58,18 @@
|
|||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
padding: var(--space-5) var(--space-6);
|
padding: var(--space-5) var(--space-6);
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.widget-greeting {
|
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.widget-greeting__content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-greeting__title {
|
.widget-greeting__title {
|
||||||
font-size: var(--text-2xl);
|
font-size: var(--text-2xl);
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
margin-bottom: var(--space-1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-greeting__date {
|
.widget-greeting__date {
|
||||||
@@ -77,6 +77,30 @@
|
|||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.widget-greeting__chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-2);
|
||||||
|
margin-top: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.greeting-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
padding: 3px var(--space-2);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.greeting-chip--warn {
|
||||||
|
background: rgba(255, 59, 48, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
/* --------------------------------------------------------
|
/* --------------------------------------------------------
|
||||||
* Basis-Widget (Card)
|
* Basis-Widget (Card)
|
||||||
* -------------------------------------------------------- */
|
* -------------------------------------------------------- */
|
||||||
@@ -116,6 +140,24 @@
|
|||||||
font-size: var(--text-sm);
|
font-size: var(--text-sm);
|
||||||
color: var(--color-accent);
|
color: var(--color-accent);
|
||||||
font-weight: var(--font-weight-medium);
|
font-weight: var(--font-weight-medium);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget__badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
min-width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
padding: 0 4px;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget__body {
|
.widget__body {
|
||||||
@@ -128,6 +170,21 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
font-size: var(--text-sm);
|
font-size: var(--text-sm);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Widget hover lift (desktop) */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.widget {
|
||||||
|
transition: transform var(--transition-fast), box-shadow var(--transition-fast);
|
||||||
|
}
|
||||||
|
.widget:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --------------------------------------------------------
|
/* --------------------------------------------------------
|
||||||
@@ -246,10 +303,137 @@
|
|||||||
font-size: var(--text-xs);
|
font-size: var(--text-xs);
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-time-badge {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
background-color: var(--color-surface-2);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-time-badge--today {
|
||||||
|
background-color: var(--color-accent-light);
|
||||||
|
color: var(--color-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --------------------------------------------------------
|
/* --------------------------------------------------------
|
||||||
* Essen-Widget
|
* Essen-Widget (Slot-Grid)
|
||||||
|
* -------------------------------------------------------- */
|
||||||
|
.widget--meals {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-slots {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 1px;
|
||||||
|
background-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-slot {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
padding: var(--space-3) var(--space-2);
|
||||||
|
background-color: var(--color-surface);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color var(--transition-fast);
|
||||||
|
text-align: center;
|
||||||
|
min-height: 88px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-slot:hover {
|
||||||
|
background-color: var(--color-surface-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-slot:first-child {
|
||||||
|
border-bottom-left-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-slot:last-child {
|
||||||
|
border-bottom-right-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-slot__icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
color: var(--color-text-disabled);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-slot--filled .meal-slot__icon {
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-slot__type {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
color: var(--color-text-disabled);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-slot--filled .meal-slot__type {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-slot__title {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 0 var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-slot--filled .meal-slot__title {
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------------------------------------------------------
|
||||||
|
* Notizen-Grid-Widget
|
||||||
|
* -------------------------------------------------------- */
|
||||||
|
.notes-grid-widget {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-3) var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.notes-grid-widget {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-item {
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: var(--space-3);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity var(--transition-fast), transform var(--transition-fast);
|
||||||
|
border-left: 3px solid var(--note-color, var(--color-accent));
|
||||||
|
background-color: var(--color-surface-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-item:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* legacy (old .note-item had margin-bottom, now grid handles gap) */
|
||||||
|
|
||||||
|
/* --------------------------------------------------------
|
||||||
|
* Alte Essen-Listen-Styles (Fallback, nicht mehr primär)
|
||||||
* -------------------------------------------------------- */
|
* -------------------------------------------------------- */
|
||||||
.meal-item {
|
.meal-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -290,26 +474,6 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --------------------------------------------------------
|
|
||||||
* Notizen-Widget
|
|
||||||
* -------------------------------------------------------- */
|
|
||||||
.note-item {
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
padding: var(--space-3);
|
|
||||||
margin-bottom: var(--space-2);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: opacity var(--transition-fast);
|
|
||||||
border-left: 3px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-item:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-item:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-item__title {
|
.note-item__title {
|
||||||
font-size: var(--text-sm);
|
font-size: var(--text-sm);
|
||||||
font-weight: var(--font-weight-semibold);
|
font-weight: var(--font-weight-semibold);
|
||||||
|
|||||||
Reference in New Issue
Block a user