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:
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [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
|
||||
|
||||
### Security
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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.",
|
||||
"main": "server/index.js",
|
||||
"type": "module",
|
||||
|
||||
@@ -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 = () => ({
|
||||
breakfast: t('meals.typeBreakfast'),
|
||||
lunch: t('meals.typeLunch'),
|
||||
@@ -160,7 +167,8 @@ function renderUrgentTasks(tasks) {
|
||||
const due = formatDueDate(t.due_date);
|
||||
return `
|
||||
<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__title">${esc(t.title)}</div>
|
||||
${due ? `<div class="task-item__meta ${due.overdue ? 'task-item__meta--overdue' : ''}">${due.text}</div>` : ''}
|
||||
|
||||
+5
-5
@@ -177,13 +177,13 @@ async function renderPage(route, previousPath = null) {
|
||||
}
|
||||
|
||||
// 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.
|
||||
if (!document.querySelector('.nav-bottom') && currentUser) {
|
||||
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)
|
||||
const direction = getDirection(previousPath, route.path);
|
||||
@@ -216,7 +216,7 @@ async function renderPage(route, previousPath = null) {
|
||||
*/
|
||||
function renderAppShell(container) {
|
||||
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')}">
|
||||
<div class="nav-sidebar__logo"><span>Oikos</span></div>
|
||||
<div class="nav-sidebar__items" role="list">
|
||||
@@ -224,7 +224,7 @@ function renderAppShell(container) {
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="app-content" id="page-content" aria-live="polite">
|
||||
<main class="app-content" id="main-content" aria-live="polite">
|
||||
</main>
|
||||
|
||||
<nav class="nav-bottom" aria-label="${t('nav.navigation')}">
|
||||
@@ -416,7 +416,7 @@ window.addEventListener('auth:expired', () => {
|
||||
window.addEventListener('locale-changed', () => {
|
||||
const navSidebarItems = document.querySelector('.nav-sidebar__items');
|
||||
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 navBottom = document.querySelector('.nav-bottom');
|
||||
|
||||
|
||||
@@ -89,7 +89,9 @@
|
||||
* Begrüßungs-Widget
|
||||
* -------------------------------------------------------- */
|
||||
.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);
|
||||
padding: var(--space-4) var(--space-5);
|
||||
color: #ffffff;
|
||||
@@ -99,7 +101,7 @@
|
||||
.widget-greeting__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
gap: var(--space-0h);
|
||||
}
|
||||
|
||||
.widget-greeting__title {
|
||||
|
||||
Reference in New Issue
Block a user