fix(a11y): skip-link target, priority labels, greeting tokens

- Rename #page-content to #main-content so skip-to-content link
  targets the semantic <main> landmark
- Add sr-only priority labels to dashboard task items for screen
  readers (WCAG 1.4.1 color-not-only)
- Replace hardcoded hex in greeting gradient with accent tokens
  so dark mode themes the banner correctly
- Replace hardcoded gap: 2px with --space-0h token
- Bump version to 0.7.2
This commit is contained in:
Ulas
2026-04-04 06:31:21 +02:00
parent 6bc4c46f03
commit 70c1291ae7
5 changed files with 29 additions and 9 deletions
+10
View File
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.7.2] - 2026-04-04
### Accessibility
- Rename `#page-content` to `#main-content` so the existing skip-to-content link targets the semantic `<main>` landmark correctly
- Add `sr-only` priority labels to dashboard task items - screen readers now announce priority level instead of relying on color alone (WCAG 1.4.1)
### Fixed
- Replace hardcoded hex values in greeting widget gradient with `--color-accent-active` / `--color-accent` tokens - dark mode now correctly themes the greeting banner
- Replace hardcoded `gap: 2px` with `--space-0h` token in greeting widget
## [0.7.1] - 2026-04-04 ## [0.7.1] - 2026-04-04
### Security ### Security
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "oikos", "name": "oikos",
"version": "0.7.1", "version": "0.7.2",
"description": "Self-hosted family planner - calendar, tasks, shopping, meal planning, budget and more. Private, open-source, no subscription.", "description": "Self-hosted family planner - calendar, tasks, shopping, meal planning, budget and more. Private, open-source, no subscription.",
"main": "server/index.js", "main": "server/index.js",
"type": "module", "type": "module",
+9 -1
View File
@@ -56,6 +56,13 @@ function formatDueDate(dateStr) {
}; };
} }
const PRIORITY_LABELS = () => ({
urgent: t('tasks.priorityUrgent'),
high: t('tasks.priorityHigh'),
medium: t('tasks.priorityMedium'),
low: t('tasks.priorityLow'),
});
const MEAL_LABELS = () => ({ const MEAL_LABELS = () => ({
breakfast: t('meals.typeBreakfast'), breakfast: t('meals.typeBreakfast'),
lunch: t('meals.typeLunch'), lunch: t('meals.typeLunch'),
@@ -160,7 +167,8 @@ function renderUrgentTasks(tasks) {
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">
<div class="task-item__priority task-item__priority--${t.priority}"></div> <div class="task-item__priority task-item__priority--${t.priority}" aria-hidden="true"></div>
<span class="sr-only">${PRIORITY_LABELS()[t.priority] ?? t.priority}</span>
<div class="task-item__content"> <div class="task-item__content">
<div class="task-item__title">${esc(t.title)}</div> <div class="task-item__title">${esc(t.title)}</div>
${due ? `<div class="task-item__meta ${due.overdue ? 'task-item__meta--overdue' : ''}">${due.text}</div>` : ''} ${due ? `<div class="task-item__meta ${due.overdue ? 'task-item__meta--overdue' : ''}">${due.text}</div>` : ''}
+5 -5
View File
@@ -177,13 +177,13 @@ async function renderPage(route, previousPath = null) {
} }
// App-Shell einmalig aufbauen BEVOR render() aufgerufen wird - // App-Shell einmalig aufbauen BEVOR render() aufgerufen wird -
// page-content muss im DOM existieren damit document.getElementById() // main-content muss im DOM existieren damit document.getElementById()
// in Seiten-Modulen funktioniert. // in Seiten-Modulen funktioniert.
if (!document.querySelector('.nav-bottom') && currentUser) { if (!document.querySelector('.nav-bottom') && currentUser) {
renderAppShell(app); renderAppShell(app);
} }
const content = document.getElementById('page-content') || app; const content = document.getElementById('main-content') || app;
// Richtung bestimmen (previousPath ist der alte Pfad vor der Navigation) // Richtung bestimmen (previousPath ist der alte Pfad vor der Navigation)
const direction = getDirection(previousPath, route.path); const direction = getDirection(previousPath, route.path);
@@ -216,7 +216,7 @@ async function renderPage(route, previousPath = null) {
*/ */
function renderAppShell(container) { function renderAppShell(container) {
container.innerHTML = ` container.innerHTML = `
<a href="#page-content" class="sr-only">${t('common.skipToContent')}</a> <a href="#main-content" class="sr-only">${t('common.skipToContent')}</a>
<nav class="nav-sidebar" aria-label="${t('nav.main')}"> <nav class="nav-sidebar" aria-label="${t('nav.main')}">
<div class="nav-sidebar__logo"><span>Oikos</span></div> <div class="nav-sidebar__logo"><span>Oikos</span></div>
<div class="nav-sidebar__items" role="list"> <div class="nav-sidebar__items" role="list">
@@ -224,7 +224,7 @@ function renderAppShell(container) {
</div> </div>
</nav> </nav>
<main class="app-content" id="page-content" aria-live="polite"> <main class="app-content" id="main-content" aria-live="polite">
</main> </main>
<nav class="nav-bottom" aria-label="${t('nav.navigation')}"> <nav class="nav-bottom" aria-label="${t('nav.navigation')}">
@@ -416,7 +416,7 @@ window.addEventListener('auth:expired', () => {
window.addEventListener('locale-changed', () => { window.addEventListener('locale-changed', () => {
const navSidebarItems = document.querySelector('.nav-sidebar__items'); const navSidebarItems = document.querySelector('.nav-sidebar__items');
const navBottomPages = document.querySelectorAll('.nav-bottom__page'); const navBottomPages = document.querySelectorAll('.nav-bottom__page');
const skipLink = document.querySelector('.sr-only[href="#page-content"]'); const skipLink = document.querySelector('.sr-only[href="#main-content"]');
const navSidebar = document.querySelector('.nav-sidebar'); const navSidebar = document.querySelector('.nav-sidebar');
const navBottom = document.querySelector('.nav-bottom'); const navBottom = document.querySelector('.nav-bottom');
+4 -2
View File
@@ -89,7 +89,9 @@
* Begrüßungs-Widget * Begrüßungs-Widget
* -------------------------------------------------------- */ * -------------------------------------------------------- */
.widget-greeting { .widget-greeting {
background: linear-gradient(135deg, #1D4ED8, #2563EB); --greeting-from: var(--color-accent-active);
--greeting-to: var(--color-accent);
background: linear-gradient(135deg, var(--greeting-from), var(--greeting-to));
border-radius: var(--radius-md); border-radius: var(--radius-md);
padding: var(--space-4) var(--space-5); padding: var(--space-4) var(--space-5);
color: #ffffff; color: #ffffff;
@@ -99,7 +101,7 @@
.widget-greeting__content { .widget-greeting__content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 2px; gap: var(--space-0h);
} }
.widget-greeting__title { .widget-greeting__title {