fix: a11y — tablist, health dots, table headers, switch roles, nav landmark
This commit is contained in:
parent
b7e71b0e78
commit
29fb31d76c
4 changed files with 63 additions and 27 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-surface font-mono text-text-primary">
|
<div class="min-h-screen bg-surface font-mono text-text-primary">
|
||||||
<nav class="border-b border-surface-border px-6 py-3 flex items-center gap-6">
|
<nav aria-label="Main navigation" class="border-b border-surface-border px-6 py-3 flex items-center gap-6">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-accent font-semibold tracking-wide">TURNSTONE</span>
|
<span class="text-accent font-semibold tracking-wide">TURNSTONE</span>
|
||||||
<span class="text-text-dim text-xs">diagnostic intelligence</span>
|
<span class="text-text-dim text-xs">diagnostic intelligence</span>
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
:key="link.to"
|
:key="link.to"
|
||||||
:to="link.to"
|
:to="link.to"
|
||||||
class="px-3 py-1 rounded text-sm text-text-muted hover:text-text-primary hover:bg-surface-raised transition-colors"
|
class="px-3 py-1 rounded text-sm text-text-muted hover:text-text-primary hover:bg-surface-raised transition-colors"
|
||||||
active-class="text-accent bg-accent-muted"
|
active-class="text-accent bg-accent-muted font-semibold"
|
||||||
>{{ link.label }}</RouterLink>
|
>{{ link.label }}</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-auto flex items-center gap-3">
|
<div class="ml-auto flex items-center gap-3">
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
@click="toggleTheme"
|
@click="toggleTheme"
|
||||||
:title="isDark ? 'Switch to light mode' : 'Switch to dark mode'"
|
:title="isDark ? 'Switch to light mode' : 'Switch to dark mode'"
|
||||||
class="text-text-dim hover:text-text-primary transition-colors text-base leading-none px-1"
|
class="text-text-dim hover:text-text-primary transition-colors text-base leading-none px-1"
|
||||||
aria-label="Toggle theme"
|
:aria-label="isDark ? 'Switch to light mode' : 'Switch to dark mode'"
|
||||||
>{{ isDark ? '☀' : '☾' }}</button>
|
>{{ isDark ? '☀' : '☾' }}</button>
|
||||||
<StatusDot />
|
<StatusDot />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -42,16 +42,16 @@
|
||||||
{{ loading ? '…' : (stats?.errors_24h ?? 0) }}
|
{{ loading ? '…' : (stats?.errors_24h ?? 0) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<RouterLink
|
||||||
class="rounded border bg-surface-raised p-5 cursor-pointer hover:bg-surface transition-colors"
|
to="/incidents"
|
||||||
|
class="rounded border bg-surface-raised p-5 block hover:bg-surface transition-colors"
|
||||||
:class="activeIncidents > 0 ? 'border-sev-warn' : 'border-surface-border'"
|
:class="activeIncidents > 0 ? 'border-sev-warn' : 'border-surface-border'"
|
||||||
@click="$router.push('/incidents')"
|
|
||||||
>
|
>
|
||||||
<p class="text-text-dim text-xs uppercase tracking-widest mb-2">Active Incidents</p>
|
<p class="text-text-dim text-xs uppercase tracking-widest mb-2">Active Incidents</p>
|
||||||
<p class="text-3xl font-semibold tabular-nums" :class="activeIncidents > 0 ? 'text-sev-warn' : 'text-text-muted'">
|
<p class="text-3xl font-semibold tabular-nums" :class="activeIncidents > 0 ? 'text-sev-warn' : 'text-text-muted'">
|
||||||
{{ incidentsLoading ? '…' : activeIncidents }}
|
{{ incidentsLoading ? '…' : activeIncidents }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Source health (24h) -->
|
<!-- Source health (24h) -->
|
||||||
|
|
@ -67,14 +67,15 @@
|
||||||
|
|
||||||
<div v-else class="rounded border border-surface-border overflow-hidden">
|
<div v-else class="rounded border border-surface-border overflow-hidden">
|
||||||
<table class="w-full text-sm">
|
<table class="w-full text-sm">
|
||||||
|
<caption class="sr-only">Source health — last 24 hours</caption>
|
||||||
<thead class="bg-surface-raised border-b border-surface-border">
|
<thead class="bg-surface-raised border-b border-surface-border">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-left px-4 py-2.5 text-text-dim font-medium text-xs uppercase tracking-wider w-4"></th>
|
<th scope="col" class="text-left px-4 py-2.5 text-text-dim font-medium text-xs uppercase tracking-wider w-4"></th>
|
||||||
<th class="text-left px-4 py-2.5 text-text-dim font-medium text-xs uppercase tracking-wider">Source</th>
|
<th scope="col" class="text-left px-4 py-2.5 text-text-dim font-medium text-xs uppercase tracking-wider">Source</th>
|
||||||
<th class="text-right px-4 py-2.5 text-text-dim font-medium text-xs uppercase tracking-wider">Events</th>
|
<th scope="col" class="text-right px-4 py-2.5 text-text-dim font-medium text-xs uppercase tracking-wider">Events</th>
|
||||||
<th class="text-right px-4 py-2.5 text-text-dim font-medium text-xs uppercase tracking-wider">Errors</th>
|
<th scope="col" class="text-right px-4 py-2.5 text-text-dim font-medium text-xs uppercase tracking-wider">Errors</th>
|
||||||
<th class="text-left px-4 py-2.5 text-text-dim font-medium text-xs uppercase tracking-wider">Latest</th>
|
<th scope="col" class="text-left px-4 py-2.5 text-text-dim font-medium text-xs uppercase tracking-wider">Latest</th>
|
||||||
<th class="px-4 py-2.5"></th>
|
<th scope="col" class="px-4 py-2.5"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
@ -87,6 +88,7 @@
|
||||||
<span
|
<span
|
||||||
class="inline-block w-2 h-2 rounded-full"
|
class="inline-block w-2 h-2 rounded-full"
|
||||||
:class="healthDot(src.error_count, src.entry_count)"
|
:class="healthDot(src.error_count, src.entry_count)"
|
||||||
|
aria-hidden="true"
|
||||||
></span>
|
></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-2.5 text-accent font-mono text-xs">{{ src.source_id }}</td>
|
<td class="px-4 py-2.5 text-accent font-mono text-xs">{{ src.source_id }}</td>
|
||||||
|
|
@ -101,6 +103,7 @@
|
||||||
<button
|
<button
|
||||||
class="text-text-dim hover:text-accent text-xs px-2 py-1 rounded hover:bg-surface transition-colors"
|
class="text-text-dim hover:text-accent text-xs px-2 py-1 rounded hover:bg-surface transition-colors"
|
||||||
@click="diagnoseSource(src.source_id)"
|
@click="diagnoseSource(src.source_id)"
|
||||||
|
:aria-label="`Diagnose ${src.source_id}`"
|
||||||
>diagnose</button>
|
>diagnose</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,14 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab toggle -->
|
<!-- Tab toggle -->
|
||||||
<div class="flex gap-1 mb-6 border-b border-surface-border">
|
<div role="tablist" aria-label="Diagnose mode" class="flex gap-1 mb-6 border-b border-surface-border">
|
||||||
<button
|
<button
|
||||||
v-for="t in tabs"
|
v-for="t in tabs"
|
||||||
:key="t.key"
|
:key="t.key"
|
||||||
|
role="tab"
|
||||||
|
:aria-selected="activeTab === t.key"
|
||||||
|
:id="`tab-${t.key}`"
|
||||||
|
:aria-controls="`tabpanel-${t.key}`"
|
||||||
@click="activeTab = t.key"
|
@click="activeTab = t.key"
|
||||||
:class="[
|
:class="[
|
||||||
'px-4 py-2 text-sm transition-colors border-b-2 -mb-px',
|
'px-4 py-2 text-sm transition-colors border-b-2 -mb-px',
|
||||||
|
|
@ -23,11 +27,23 @@
|
||||||
>{{ t.label }}</button>
|
>{{ t.label }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Quick tab -->
|
<!-- Quick tab panel -->
|
||||||
<QuickCapture v-if="activeTab === 'quick'" />
|
<div
|
||||||
|
v-if="activeTab === 'quick'"
|
||||||
|
role="tabpanel"
|
||||||
|
id="tabpanel-quick"
|
||||||
|
aria-labelledby="tab-quick"
|
||||||
|
>
|
||||||
|
<QuickCapture />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Structured tab -->
|
<!-- Structured tab panel -->
|
||||||
<template v-else>
|
<div
|
||||||
|
v-else
|
||||||
|
role="tabpanel"
|
||||||
|
id="tabpanel-structured"
|
||||||
|
aria-labelledby="tab-structured"
|
||||||
|
>
|
||||||
<IncidentForm @created="onCreated" />
|
<IncidentForm @created="onCreated" />
|
||||||
<div
|
<div
|
||||||
v-if="createdLabel"
|
v-if="createdLabel"
|
||||||
|
|
@ -36,7 +52,7 @@
|
||||||
Incident "{{ createdLabel }}" saved —
|
Incident "{{ createdLabel }}" saved —
|
||||||
<RouterLink to="/incidents" class="underline underline-offset-2">view in Incidents</RouterLink>
|
<RouterLink to="/incidents" class="underline underline-offset-2">view in Incidents</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,16 @@
|
||||||
<div class="rounded border border-surface-border bg-surface-raised p-5 space-y-6">
|
<div class="rounded border border-surface-border bg-surface-raised p-5 space-y-6">
|
||||||
<!-- Entry point -->
|
<!-- Entry point -->
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-text-primary text-sm font-semibold mb-1">Quick Capture Entry Point</h2>
|
<h2 id="entry-point-label" class="text-text-primary text-sm font-semibold mb-1">Quick Capture Entry Point</h2>
|
||||||
<p class="text-text-dim text-xs mb-3">
|
<p class="text-text-dim text-xs mb-3">
|
||||||
Where the "describe it and search" input appears on every page.
|
Where the "describe it and search" input appears on every page.
|
||||||
</p>
|
</p>
|
||||||
<div class="flex gap-3">
|
<div role="radiogroup" aria-labelledby="entry-point-label" class="flex gap-3">
|
||||||
<button
|
<button
|
||||||
v-for="opt in entryPointOptions"
|
v-for="opt in entryPointOptions"
|
||||||
:key="opt.value"
|
:key="opt.value"
|
||||||
|
role="radio"
|
||||||
|
:aria-checked="prefs.entry_point_style === opt.value"
|
||||||
@click="setEntryPoint(opt.value as 'topbar' | 'fab')"
|
@click="setEntryPoint(opt.value as 'topbar' | 'fab')"
|
||||||
:class="[
|
:class="[
|
||||||
'flex-1 px-4 py-3 rounded border text-sm transition-colors text-left',
|
'flex-1 px-4 py-3 rounded border text-sm transition-colors text-left',
|
||||||
|
|
@ -64,12 +66,20 @@
|
||||||
API Key
|
API Key
|
||||||
<span class="text-text-dim font-normal">(optional — required for cf-orch remote inference)</span>
|
<span class="text-text-dim font-normal">(optional — required for cf-orch remote inference)</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<div class="relative">
|
||||||
v-model="prefs.llm_api_key"
|
<input
|
||||||
type="password"
|
v-model="prefs.llm_api_key"
|
||||||
placeholder="Leave blank for local Ollama"
|
:type="showApiKey ? 'text' : 'password'"
|
||||||
class="w-full bg-surface border border-surface-border rounded px-3 py-2 text-sm text-text-primary placeholder-text-dim focus:outline-none focus:border-accent transition-colors"
|
placeholder="Leave blank for local Ollama"
|
||||||
/>
|
class="w-full bg-surface border border-surface-border rounded px-3 py-2 text-sm text-text-primary placeholder-text-dim focus:outline-none focus:border-accent transition-colors"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="showApiKey = !showApiKey"
|
||||||
|
:aria-label="showApiKey ? 'Hide API key' : 'Show API key'"
|
||||||
|
class="absolute right-3 top-1/2 -translate-y-1/2 text-text-dim hover:text-text-primary text-xs"
|
||||||
|
>{{ showApiKey ? 'Hide' : 'Show' }}</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="saveLlm"
|
@click="saveLlm"
|
||||||
|
|
@ -94,6 +104,9 @@
|
||||||
class="flex items-start gap-3 rounded border border-surface-border bg-surface p-3"
|
class="flex items-start gap-3 rounded border border-surface-border bg-surface p-3"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
role="switch"
|
||||||
|
:aria-checked="rule.enabled"
|
||||||
|
:aria-label="`${rule.name || 'Override'} rule enabled`"
|
||||||
@click="toggleOverride(i)"
|
@click="toggleOverride(i)"
|
||||||
:class="[
|
:class="[
|
||||||
'mt-0.5 w-9 h-5 rounded-full flex-shrink-0 transition-colors relative',
|
'mt-0.5 w-9 h-5 rounded-full flex-shrink-0 transition-colors relative',
|
||||||
|
|
@ -112,6 +125,7 @@
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="deleteOverride(i)"
|
@click="deleteOverride(i)"
|
||||||
|
:aria-label="`Delete override rule: ${rule.name || 'unnamed'}`"
|
||||||
class="text-text-dim hover:text-sev-error text-xs flex-shrink-0 mt-0.5"
|
class="text-text-dim hover:text-sev-error text-xs flex-shrink-0 mt-0.5"
|
||||||
title="Delete rule"
|
title="Delete rule"
|
||||||
>✕</button>
|
>✕</button>
|
||||||
|
|
@ -160,6 +174,8 @@
|
||||||
|
|
||||||
<p
|
<p
|
||||||
v-if="saveStatus"
|
v-if="saveStatus"
|
||||||
|
role="status"
|
||||||
|
aria-live="polite"
|
||||||
class="text-xs"
|
class="text-xs"
|
||||||
:class="saveStatus.ok ? 'text-green-400' : 'text-sev-error'"
|
:class="saveStatus.ok ? 'text-green-400' : 'text-sev-error'"
|
||||||
>
|
>
|
||||||
|
|
@ -192,6 +208,7 @@ interface Prefs {
|
||||||
const prefs = ref<Prefs>({ entry_point_style: 'topbar', llm_url: '', llm_model: '', llm_api_key: '', severity_overrides: [] })
|
const prefs = ref<Prefs>({ entry_point_style: 'topbar', llm_url: '', llm_model: '', llm_api_key: '', severity_overrides: [] })
|
||||||
const saveStatus = ref<{ ok: boolean; msg: string } | null>(null)
|
const saveStatus = ref<{ ok: boolean; msg: string } | null>(null)
|
||||||
const showAddOverride = ref(false)
|
const showAddOverride = ref(false)
|
||||||
|
const showApiKey = ref(false)
|
||||||
const newRule = ref<SeverityOverride>({ name: '', pattern: '', override_severity: 'WARN', enabled: true })
|
const newRule = ref<SeverityOverride>({ name: '', pattern: '', override_severity: 'WARN', enabled: true })
|
||||||
|
|
||||||
const entryPointOptions = [
|
const entryPointOptions = [
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue