From c8e20b22c846aad0c317bf9df3ea6fed387c9ced Mon Sep 17 00:00:00 2001 From: Ulas Kalayci Date: Mon, 20 Apr 2026 07:36:07 +0200 Subject: [PATCH] chore: release v0.20.21 Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 16 ++++++ package.json | 2 +- public/index.html | 14 ++++-- public/pages/dashboard.js | 54 ++++++++------------ public/pages/tasks.js | 44 ++++++++++------- public/styles/layout.css | 30 +++++++++++ public/styles/tokens.css | 101 ++------------------------------------ server/routes/weather.js | 3 +- 8 files changed, 109 insertions(+), 155 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83aaa67..c383365 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.20.21] - 2026-04-20 + +### Changed +- Dashboard: eliminated double-render flicker — initial paint uses skeleton widgets and a stat-less greeting; real widgets replace skeletons in-place without resetting `container.innerHTML` +- Dashboard: weather widget now derives temperature unit symbol (°C / °F / K) from the `units` field returned by the weather API instead of always showing °C +- Dark mode: removed duplicate `@media (prefers-color-scheme: dark)` block from `tokens.css`; system-preference detection moved to a `matchMedia` listener in `index.html` for flash-free sync +- Tasks: view-toggle (list / Kanban) fades out at 40% opacity during re-render and fades back in, giving visible feedback of the switch + +### Fixed +- Tasks: inline `style="width/height"` on all Lucide icon instances replaced with utility CSS classes (`icon-xs` … `icon-2xl`, `icon-11`) defined in `layout.css` +- Tasks: edit-button inline size overrides removed; replaced with new `.btn--icon-sm` utility class +- Tasks: `textarea` `resize: vertical` and select `min-height: 44px` moved from inline styles to `layout.css` +- Dashboard: `chipIcon` inline style variable eliminated; chip icons now use `class="icon-sm"` +- Dashboard: settings, refresh, chevron, and other action icons converted from inline styles to CSS classes +- Weather API: server now forwards the configured `units` value in the response payload so the frontend can render the correct unit symbol + ## [0.20.20] - 2026-04-20 ### Fixed diff --git a/package.json b/package.json index 9bdab5e..e3ae63f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oikos", - "version": "0.20.20", + "version": "0.20.21", "description": "Self-hosted family planner - calendar, tasks, shopping, meal planning, budget and more. Private, open-source, no subscription.", "main": "server/index.js", "type": "module", diff --git a/public/index.html b/public/index.html index 53025c7..1765f32 100644 --- a/public/index.html +++ b/public/index.html @@ -40,11 +40,19 @@ - + diff --git a/public/pages/dashboard.js b/public/pages/dashboard.js index 2de3708..3af080d 100644 --- a/public/pages/dashboard.js +++ b/public/pages/dashboard.js @@ -149,21 +149,20 @@ function skeletonWidget(lines = 3) { function renderGreeting(user, stats = {}) { const { urgentCount = 0, todayEventCount = 0, todayMealTitle = null } = stats; - const chipIcon = 'width:12px;height:12px;flex-shrink:0;'; const statChips = []; if (urgentCount > 0) statChips.push(` - + ${urgentCount > 1 ? t('dashboard.urgentTasksChipPlural', { count: urgentCount }) : t('dashboard.urgentTasksChip', { count: urgentCount })} `); if (todayEventCount > 0) statChips.push(` - + ${todayEventCount > 1 ? t('dashboard.eventsChipPlural', { count: todayEventCount }) : t('dashboard.eventsChip', { count: todayEventCount })} `); if (todayMealTitle) statChips.push(` - + ${t('dashboard.todayMealChip', { title: esc(todayMealTitle) })} `); @@ -177,7 +176,7 @@ function renderGreeting(user, stats = {}) { @@ -358,7 +357,8 @@ const WEATHER_ICON_BASE = '/api/v1/weather/icon/'; function renderWeatherWidget(weather) { if (!weather) return ''; - const { city, current, forecast } = weather; + const { city, current, forecast, units } = weather; + const unitSymbol = units === 'imperial' ? '°F' : units === 'standard' ? 'K' : '°C'; const forecastHtml = forecast.map((d, i) => { const date = new Date(d.date + 'T12:00:00'); @@ -379,12 +379,12 @@ function renderWeatherWidget(weather) { return `
-
${esc(current.temp)}°C
+
${esc(current.temp)}${unitSymbol}
${esc(current.desc)}
${esc(city)}
@@ -510,11 +510,11 @@ function openCustomizeModal(currentConfig, onSave) {
@@ -624,15 +624,9 @@ export async function render(container, { user }) { container.innerHTML = `
+

${t('dashboard.title')}

-
-
-
-
${greeting(user.display_name)}
-
${formatDate(new Date())}
-
-
-
+ ${renderGreeting(user, {})} ${skeletonWidget(3)} ${skeletonWidget(3)} ${skeletonWidget(2)} @@ -671,9 +665,9 @@ export async function render(container, { user }) { }; function rebuildGrid(cfg) { - const grid = container.querySelector('.dashboard__grid'); - const greeting = grid?.querySelector('.widget-greeting'); + const grid = container.querySelector('.dashboard__grid'); if (!grid) return; + const greeting = grid.querySelector('.widget-greeting'); grid.replaceChildren(...(greeting ? [greeting] : [])); grid.insertAdjacentHTML('beforeend', renderWidgets(cfg, data, weather)); wireLinks(container); @@ -681,20 +675,14 @@ export async function render(container, { user }) { wireWeatherRefresh(container); } - container.innerHTML = ` -
-

${t('dashboard.title')}

-
- ${renderGreeting(user, stats)} - ${renderWidgets(widgetConfig, data, weather)} -
-
- ${renderFab()} - `; + // Greeting in-place aktualisieren (Stats-Chips hinzufügen), kein Gesamt-Reset + const greetingEl = container.querySelector('.widget-greeting'); + if (greetingEl) greetingEl.outerHTML = renderGreeting(user, stats); + + // Skeletons durch echte Widgets ersetzen + rebuildGrid(widgetConfig); - wireLinks(container); initFab(container, _fabController.signal); - if (window.lucide) window.lucide.createIcons(); container.querySelector('#dashboard-customize-btn')?.addEventListener( 'click', @@ -705,8 +693,6 @@ export async function render(container, { user }) { { signal: _fabController.signal }, ); - wireWeatherRefresh(container); - // 30-Minuten Auto-Refresh für Wetter const refreshBtn = container.querySelector('#weather-refresh-btn'); if (refreshBtn) { diff --git a/public/pages/tasks.js b/public/pages/tasks.js index 1f1e0ab..81e5e1d 100644 --- a/public/pages/tasks.js +++ b/public/pages/tasks.js @@ -123,7 +123,7 @@ function renderDueDate(dateStr) { const d = formatDueDate(dateStr); if (!d) return ''; return ` - ${d.label} + ${d.label} `; } @@ -132,11 +132,11 @@ function renderSwipeRow(task, innerHtml) { return `
${innerHtml} @@ -179,7 +179,7 @@ function renderTaskCard(task, opts = {}) {
${renderPriorityBadge(task.priority)} ${renderDueDate(task.due_date)} - ${task.is_recurring ? `` : ''} + ${task.is_recurring ? `` : ''} ${task.category !== 'Sonstiges' ? `${CATEGORY_LABELS()[task.category] ?? task.category}` : ''}
@@ -190,9 +190,9 @@ function renderTaskCard(task, opts = {}) { ${esc(initials(task.assigned_name ?? ''))}
` : ''} -
@@ -284,19 +284,19 @@ function renderModalContent({ task = null, users = [], reminder = null } = {}) { + >${esc(task?.description)}
`; diff --git a/public/styles/layout.css b/public/styles/layout.css index 64f6f2d..ad95331 100755 --- a/public/styles/layout.css +++ b/public/styles/layout.css @@ -1779,6 +1779,36 @@ border-radius: 0 0 var(--radius-sm) 0; } +/* -------------------------------------------------------- + * Icon-Größen-Utilities + * Größen für Lucide-Icons (). Klassen werden + * vom Lucide-Renderer auf das erzeugte übertragen. + * -------------------------------------------------------- */ +.icon-xs { width: 10px; height: 10px; flex-shrink: 0; } +.icon-11 { width: 11px; height: 11px; flex-shrink: 0; } +.icon-sm { width: 12px; height: 12px; flex-shrink: 0; } +.icon-md { width: 14px; height: 14px; flex-shrink: 0; } +.icon-base { width: 16px; height: 16px; flex-shrink: 0; } +.icon-lg { width: 18px; height: 18px; flex-shrink: 0; } +.icon-xl { width: 22px; height: 22px; flex-shrink: 0; } +.icon-2xl { width: 24px; height: 24px; flex-shrink: 0; } + +/* Icons innerhalb von Buttons sollen keine Pointer-Events fangen */ +button i[data-lucide], +button svg { pointer-events: none; } + +/* Kompakter Icon-Button (36×36) für Icons in engen Listenkontexten */ +.btn--icon-sm { + padding: var(--space-1); + min-height: unset; + min-width: unset; + width: 36px; + height: 36px; +} + +/* Textarea: vertikale Größenänderung ist nutzbar */ +textarea.input { resize: vertical; } + /* -------------------------------------------------------- * Windows High Contrast / Forced Colors * -------------------------------------------------------- */ diff --git a/public/styles/tokens.css b/public/styles/tokens.css index 8c617ee..46fba92 100644 --- a/public/styles/tokens.css +++ b/public/styles/tokens.css @@ -415,15 +415,11 @@ /* ================================================================ * Dark Mode — private Tokens überschreiben, öffentliche API bleibt stabil. * - * Beide Selektoren überschreiben nur --_private Tokens. Die öffentlichen - * --color-* / --module-* / --glass-* Tokens müssen nie angefasst werden. - * - * (1) System-Preference — greift, wenn kein data-theme gesetzt ist. - * (2) Manueller Override — greift, wenn JS data-theme="dark" setzt - * (auch bei System in Light-Mode). + * data-theme="dark" wird durch das Head-Script in index.html gesetzt — + * sowohl für manuelle Overrides als auch für System-Präferenz (via + * matchMedia-Listener). Ein einziger Selektor reicht, kein @media-Duplikat. * ================================================================ */ -@media (prefers-color-scheme: dark) { - :root:not([data-theme="light"]) { +[data-theme="dark"] { /* Neutral-Skala invertiert (warm-dunkel) */ --_neutral-50: #1A1A18; --_neutral-100: #222220; @@ -519,95 +515,6 @@ --_glass-bg-input: rgba(34, 34, 32, 0.45); --_glass-bg-toolbar: rgba(40, 40, 38, 0.55); --_glass-tint-strength: 8%; - } -} - -/* Manueller Dark-Mode-Override: data-theme="dark" auf (auch bei System in Light-Mode) */ -:root[data-theme="dark"] { - --_neutral-50: #1A1A18; - --_neutral-100: #222220; - --_neutral-150: #2A2A28; - --_neutral-200: #333331; - --_neutral-250: #3D3D3A; - --_neutral-300: #48484A; - --_neutral-400: #636360; - --_neutral-500: #8E8D89; - --_neutral-600: #AEADB0; - --_neutral-700: #C8C7C3; - --_neutral-800: #E2E1DC; - --_neutral-900: #F5F4F1; - --_neutral-950: #FAFAF8; - - --_color-surface: #2A2A28; - --_color-surface-3: #333331; - - --_sidebar-bg: #1A1A18; - --_sidebar-shadow-light: rgba(255, 255, 255, 0.04); - --_sidebar-shadow-dark: rgba(0, 0, 0, 0.4); - - --_color-accent: #818CF8; - --_color-accent-hover: #6366F1; - --_color-accent-active: #4F46E5; - --_color-accent-light: #2E2D5B; - --_color-accent-subtle: #252255; - --_color-btn-primary: #6366F1; - --_color-btn-primary-hover: #4F46E5; - --_color-accent-secondary: #A78BFA; - - --_color-success: #4ADE80; - --_color-warning: #F59E0B; - --_color-danger: #FCA5A5; - --_color-text-tertiary: #A3A3A0; - --_color-success-light: #1A3325; - --_color-warning-light: #332400; - --_color-danger-light: #3D1C1A; - --_color-info-light: #1A2D40; - - --_module-dashboard: #818CF8; - --_module-tasks: #4ADE80; - --_module-calendar: #A78BFA; - --_module-meals: #FB923C; - --_module-shopping: #F472B6; - --_module-notes: #FCD34D; - --_module-contacts: #60A5FA; - --_module-budget: #2DD4BF; - --_module-settings: #94A3B8; - - --_meal-breakfast: #F59E0B; - --_meal-breakfast-light: #332400; - --_meal-lunch-light: #1A3325; - --_meal-dinner: #818CF8; - --_meal-dinner-light: #2E2D5B; - --_meal-snack-light: #3D2010; - - --_color-priority-none-bg: rgba(142, 141, 137, 0.12); - --_color-priority-low-bg: rgba(142, 141, 137, 0.18); - --_color-priority-medium-bg: rgba(230, 147, 10, 0.18); - --_color-priority-high-bg: rgba(212, 81, 30, 0.18); - --_color-priority-urgent-bg: rgba(229, 83, 75, 0.18); - - --_color-overlay: rgba(0, 0, 0, 0.6); - --_color-overlay-light: rgba(0, 0, 0, 0.35); - - --_shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.25); - --_shadow-md: 0 4px 12px rgba(0, 0, 0, 0.35); - --_shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.45); - - --_glass-bg: rgba(40, 40, 38, 0.75); - --_glass-bg-hover: rgba(50, 50, 48, 0.82); - --_glass-bg-elevated: rgba(58, 58, 55, 0.90); - --_glass-border: rgba(255, 255, 255, 0.12); - --_glass-border-subtle: rgba(255, 255, 255, 0.07); - --_glass-highlight: rgba(255, 255, 255, 0.10); - --_glass-highlight-subtle: rgba(255, 255, 255, 0.06); - --_glass-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.30), 0 0 0 1px rgba(255, 255, 255, 0.08); - --_glass-shadow-md: 0 4px 20px rgba(0, 0, 0, 0.40), 0 0 0 1px rgba(255, 255, 255, 0.07); - --_glass-shadow-lg: 0 8px 40px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(255, 255, 255, 0.06); - --_glass-bg-card: rgba(38, 38, 36, 0.50); - --_glass-bg-card-hover: rgba(48, 48, 46, 0.62); - --_glass-bg-input: rgba(34, 34, 32, 0.45); - --_glass-bg-toolbar: rgba(40, 40, 38, 0.55); - --_glass-tint-strength: 8%; } /* ================================================================ diff --git a/server/routes/weather.js b/server/routes/weather.js index b1b3aa1..bb73e02 100644 --- a/server/routes/weather.js +++ b/server/routes/weather.js @@ -74,7 +74,8 @@ router.get('/', async (req, res) => { } const data = { - city: currentJson.name, + city: currentJson.name, + units, current: { temp: Math.round(currentJson.main.temp), feels_like: Math.round(currentJson.main.feels_like),