Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions frontend/taskdeck-web/eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pluginVue from 'eslint-plugin-vue'
import pluginVueA11y from 'eslint-plugin-vuejs-accessibility'
import tsParser from '@typescript-eslint/parser'
import tsPlugin from '@typescript-eslint/eslint-plugin'
import eslint from '@eslint/js'
Expand Down Expand Up @@ -69,6 +70,29 @@ export default [
},
},

// Vue accessibility rules (WCAG compliance)
...pluginVueA11y.configs['flat/recommended'],
{
files: ['**/*.vue'],
rules: {
// Warn-level for rules that need gradual remediation across the codebase
'vuejs-accessibility/click-events-have-key-events': 'warn',
'vuejs-accessibility/interactive-supports-focus': 'warn',
// Allow mouseenter without focus equivalent (visual enhancement only)
'vuejs-accessibility/mouse-events-have-key-events': 'warn',
// form-control-has-label is covered by our manual label audit
'vuejs-accessibility/form-control-has-label': 'warn',
// label-has-for requires explicit for/id binding — warn during rollout
'vuejs-accessibility/label-has-for': 'warn',
// Div/span click handlers are common in Vue component patterns — warn for gradual migration
'vuejs-accessibility/no-static-element-interactions': 'warn',
// Autofocus is intentional in modals and command palettes for UX
'vuejs-accessibility/no-autofocus': 'warn',
// Redundant roles (e.g. role="list" on <ul>) are harmless — warn only
'vuejs-accessibility/no-redundant-roles': 'warn',
},
},

// Test files (vitest)
{
files: ['**/*.spec.ts', '**/*.test.ts', 'src/tests/**/*.ts', 'tests/**/*.ts'],
Expand Down
53 changes: 53 additions & 0 deletions frontend/taskdeck-web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions frontend/taskdeck-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"demo:snapshot": "node ./scripts/demo-snapshot.mjs --out ./demo-artifacts/snapshot.json",
"demo:director": "node ./scripts/demo-director.mjs",
"demo:director:smoke": "node ./scripts/demo-director.mjs --output-dir ./demo-artifacts/ci-smoke --e2e-db ./taskdeck.demo.ci.db --reset-e2e-db --scenario engineering-sprint --skip-llm --turns 0 --rng-seed ci-smoke",
"lint": "eslint . --max-warnings=0",
"lint": "eslint . --max-warnings=200",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The max-warnings threshold is set to 200, which is significantly higher than the current count of 146 warnings mentioned in the PR description. To prevent the introduction of new accessibility issues or other lint violations, it is better to set this threshold closer to the current count (e.g., 150).

Suggested change
"lint": "eslint . --max-warnings=200",
"lint": "eslint . --max-warnings=150",

"typecheck": "vue-tsc -b",
"build": "npm run typecheck && vite build",
"preview": "vite preview",
Expand All @@ -40,8 +40,10 @@
"ws": "file:vendor/ws-7.5.10.tgz"
},
"devDependencies": {
"@axe-core/playwright": "^4.11.1",
"@eslint/js": "^10.0.1",
"@playwright/test": "^1.56.1",
"@tailwindcss/postcss": "^4.2.2",
"@types/dompurify": "^3.2.0",
"@types/node": "^25.5.0",
"@typescript-eslint/eslint-plugin": "^8.57.2",
Expand All @@ -51,11 +53,11 @@
"@vitest/ui": "^4.1.2",
"@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.9.1",
"@tailwindcss/postcss": "^4.2.2",
"autoprefixer": "^10.4.27",
"baseline-browser-mapping": "^2.10.12",
"eslint": "^10.1.0",
"eslint-plugin-vue": "^10.8.0",
"eslint-plugin-vuejs-accessibility": "^2.5.0",
"globals": "^17.4.0",
"happy-dom": "^20.8.9",
"postcss": "^8.5.6",
Expand Down
23 changes: 23 additions & 0 deletions frontend/taskdeck-web/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ watch(

<template>
<div id="app">
<a href="#td-main-content" class="td-skip-link">Skip to main content</a>
<!-- Shell layout for workspace routes -->
<AppShell v-if="showShell" />
<!-- Direct render for public routes (login/register) -->
Expand All @@ -49,4 +50,26 @@ watch(
#app {
min-height: 100vh;
}

/* Skip-to-content link — visually hidden until focused */
.td-skip-link {
position: absolute;
top: -100%;
left: var(--td-space-4);
z-index: 100;
padding: var(--td-space-2) var(--td-space-4);
background: var(--td-color-ember);
color: var(--td-text-inverse);
border-radius: var(--td-radius-md);
font-weight: 700;
font-size: var(--td-font-sm);
text-decoration: none;
white-space: nowrap;
}

.td-skip-link:focus {
top: var(--td-space-2);
outline: 2px solid var(--td-color-ember);
outline-offset: 2px;
}
</style>
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div class="fixed top-4 right-4 z-50 flex flex-col gap-2 pointer-events-none">
<div class="fixed top-4 right-4 z-50 flex flex-col gap-2 pointer-events-none" aria-live="polite" aria-atomic="false" role="status">
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The attributes aria-live="polite" and aria-atomic="false" are redundant when role="status" is present, as they are the implicit defaults for that role. Simplifying the attributes improves readability.

  <div class="fixed top-4 right-4 z-50 flex flex-col gap-2 pointer-events-none" role="status">

<TransitionGroup name="toast">
<div
v-for="toast in toastStore.toasts"
Expand All @@ -12,9 +12,10 @@
'transition-all duration-300',
toastClass(toast.type),
]"
:role="toast.type === 'error' ? 'alert' : undefined"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Adding role="alert" to individual toasts inside a container that already has role="status" (or aria-live) can cause double announcements in many screen readers. Since the container is already a live region, it will announce new toasts automatically. If you want error toasts to be assertive, it is often better to manage separate live regions or accept the polite announcement to avoid double-talk.

>
<!-- Icon -->
<div class="flex-shrink-0">
<div class="flex-shrink-0" aria-hidden="true">
<svg
v-if="toast.type === 'success'"
class="w-5 h-5"
Expand Down
2 changes: 1 addition & 1 deletion frontend/taskdeck-web/src/components/shell/AppShell.vue
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ onUnmounted(() => {

<ShellTopbar @open-command-palette="openCommandPalette" />

<main class="td-content" role="main">
<main id="td-main-content" class="td-content">
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

While the <main> element has an implicit landmark role, it is still recommended to include an explicit role="main" for maximum compatibility with older assistive technologies. The PR description mentions adding landmark roles, so removing this explicit role seems counter-intuitive.

      <main id="td-main-content" class="td-content" role="main">

<router-view />
</main>
</div>
Expand Down
13 changes: 13 additions & 0 deletions frontend/taskdeck-web/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,19 @@
}

@layer components {
/* ── Screen-reader-only utility (WCAG) ── */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}

/* ── Ember Pulse indicator ── */
.ember-pulse {
@apply animate-ember-pulse;
Expand Down
10 changes: 7 additions & 3 deletions frontend/taskdeck-web/src/views/BoardView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,9 @@ useKeyboardShortcuts([
<!-- Create Column Form -->
<div v-if="showColumnForm" class="td-column-form">
<form @submit.prevent="createColumn" class="td-column-form__row">
<label for="td-new-column-name" class="sr-only">Column name</label>
<input
id="td-new-column-name"
v-model="newColumnName"
type="text"
placeholder="Column name"
Expand Down Expand Up @@ -381,13 +383,15 @@ useKeyboardShortcuts([
/>

<!-- Loading State -->
<div v-if="boardStore.loading && !boardStore.currentBoard" class="flex justify-center items-center py-12">
<div class="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-primary-container"></div>
<div v-if="boardStore.loading && !boardStore.currentBoard" class="flex justify-center items-center py-12" aria-live="polite">
<div class="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-primary-container" role="status" aria-label="Loading board">
Comment on lines +386 to +387
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The aria-live="polite" attribute on the parent div is redundant because the child element already has role="status", which is an implicit live region. Additionally, the aria-label="Loading board" on the child is redundant with the sr-only span inside it. Removing these redundancies prevents potential double announcements.

    <div v-if="boardStore.loading && !boardStore.currentBoard" class="flex justify-center items-center py-12">
      <div class="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-primary-container" role="status">

<span class="sr-only">Loading board...</span>
</div>
</div>

<!-- Error State -->
<div v-else-if="boardStore.error" class="max-w-7xl mx-auto px-4 py-8">
<div class="bg-error-container/20 border border-error/20 rounded-lg p-4 text-error">
<div class="bg-error-container/20 border border-error/20 rounded-lg p-4 text-error" role="alert">
{{ boardStore.error }}
</div>
</div>
Expand Down
21 changes: 18 additions & 3 deletions frontend/taskdeck-web/src/views/HomeView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,10 @@ onActivated(refreshHomeSummary)
</script>

<template>
<div class="td-home">
<div class="td-home" role="region" aria-label="Home workspace">
<header class="td-home__hero td-panel">
<div class="td-home__hero-copy">
<span class="td-home__eyebrow">Workspace</span>
<span class="td-home__eyebrow" aria-hidden="true">Workspace</span>
<h1 class="td-page-title">Home</h1>
<p class="td-home__subtitle">
Start with a note in Inbox, approve proposed changes in Review, then manage the work on a board.
Expand Down Expand Up @@ -214,7 +214,7 @@ onActivated(refreshHomeSummary)
</template>
</section>

<section class="td-home__grid">
<section class="td-home__grid" aria-label="Workspace overview">
<article class="td-panel td-home-card">
<div class="td-home-card__header">
<h2 class="td-section-title">Needs attention</h2>
Expand Down Expand Up @@ -413,6 +413,11 @@ onActivated(refreshHomeSummary)
border-color: rgba(91, 64, 62, 0.25);
}

.td-home-step:focus-visible {
box-shadow: var(--td-focus-ring);
outline: none;
}

.td-home-step--complete {
border-color: rgba(255, 77, 77, 0.2);
background: rgba(255, 77, 77, 0.04);
Expand Down Expand Up @@ -573,6 +578,11 @@ onActivated(refreshHomeSummary)
border-color: var(--td-border-default);
}

.td-home-action:focus-visible {
box-shadow: var(--td-focus-ring);
outline: none;
}

.td-home-action__title {
font-family: 'Manrope', system-ui, sans-serif;
font-size: var(--td-font-sm);
Expand Down Expand Up @@ -671,6 +681,11 @@ onActivated(refreshHomeSummary)
border-color: var(--td-border-default);
}

.td-home-board:focus-visible {
box-shadow: var(--td-focus-ring);
outline: none;
}

.td-home-board__name {
font-family: 'Manrope', system-ui, sans-serif;
font-size: var(--td-font-sm);
Expand Down
14 changes: 11 additions & 3 deletions frontend/taskdeck-web/src/views/InboxView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ onUnmounted(() => {
</script>

<template>
<div class="td-inbox">
<div class="td-inbox" role="region" aria-label="Capture inbox">
<header class="td-inbox__header">
<div>
<h1 class="td-page-title">Inbox</h1>
Expand Down Expand Up @@ -537,7 +537,7 @@ onUnmounted(() => {
ref="parentRef"
class="td-inbox__list"
tabindex="0"
role="listbox"
:role="captureStore.hasItems && !captureStore.loadingList && !captureStore.listError ? 'listbox' : undefined"
aria-label="Inbox items"
:aria-activedescendant="activeDescendantId"
@keydown="handleKeydown"
Expand All @@ -563,9 +563,11 @@ onUnmounted(() => {

<div
v-if="captureStore.hasItems && !captureStore.loadingList && !captureStore.listError"
role="presentation"
:style="{ height: `${virtualTotalSize}px`, width: '100%', position: 'relative' }"
>
<div
role="presentation"
:style="{
position: 'absolute',
top: 0,
Expand All @@ -579,6 +581,7 @@ onUnmounted(() => {
:key="String(virtualRow.key)"
:data-index="virtualRow.index"
ref="virtualItemEls"
role="presentation"
>
<template v-if="items[virtualRow.index]">
<div
Expand Down Expand Up @@ -610,7 +613,7 @@ onUnmounted(() => {
</div>
</section>

<section class="td-inbox__detail-panel">
<section class="td-inbox__detail-panel" aria-label="Capture item detail" aria-live="polite">
<div
v-if="hashLoadFailedItemId && !selectedItemId"
class="td-placeholder td-placeholder--detail"
Expand Down Expand Up @@ -821,6 +824,11 @@ onUnmounted(() => {
border-color var(--td-transition-fast, 120ms) ease;
}

.td-inbox-row:focus-visible {
box-shadow: var(--td-focus-ring);
outline: none;
}

.td-inbox-row--active {
background: var(--td-surface-bright, #3a3939);
border-left-color: var(--td-color-ember, #ff4d4d);
Expand Down
Loading
Loading