feat: add ARIA labels, roles, and live regions across all views (A11Y-01)
Systematic accessibility pass: aria-label on icon-only buttons, role=dialog and aria-modal on modals, role=tab/tablist on tab switchers, role=switch on toggles, aria-live on dynamic status/error regions, aria-hidden on decorative SVGs, aria-label on search inputs, and nav landmarks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,12 +11,13 @@
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Search installed apps..."
|
||||
aria-label="Search installed apps"
|
||||
class="w-full px-4 py-3 md:py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:border-white/40 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Empty State - This should never show since we always show dummy apps -->
|
||||
<div v-if="false" class="text-center py-16 pb-6">
|
||||
<!-- Empty State -->
|
||||
<div v-if="sortedPackageEntries.length === 0 && !searchQuery" class="text-center py-16 pb-6">
|
||||
<div class="glass-card p-12 max-w-md mx-auto">
|
||||
<svg class="w-16 h-16 mx-auto text-white/40 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
||||
@@ -53,9 +54,10 @@
|
||||
<button
|
||||
@click.stop="showUninstallModal(id as string, pkg)"
|
||||
class="absolute top-4 right-4 p-2 rounded-lg text-white/60 hover:text-red-400 hover:bg-red-500/20 transition-colors z-10"
|
||||
:aria-label="`Uninstall ${pkg.manifest?.title || id}`"
|
||||
title="Uninstall"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-5 h-5" aria-hidden="true" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
@@ -107,6 +109,7 @@
|
||||
<svg
|
||||
v-if="loadingActions[id as string]"
|
||||
class="animate-spin h-4 w-4"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -125,6 +128,7 @@
|
||||
<svg
|
||||
v-if="loadingActions[id as string]"
|
||||
class="animate-spin h-4 w-4"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -149,16 +153,19 @@
|
||||
<div
|
||||
ref="uninstallModalRef"
|
||||
@click.stop
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="uninstall-dialog-title"
|
||||
class="glass-card p-6 max-w-md w-full relative z-10"
|
||||
>
|
||||
<div class="flex items-start gap-4 mb-4">
|
||||
<div class="p-3 bg-red-500/20 rounded-lg">
|
||||
<svg class="w-6 h-6 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-6 h-6 text-red-400" aria-hidden="true" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-xl font-semibold text-white mb-2">Uninstall App?</h3>
|
||||
<h3 id="uninstall-dialog-title" class="text-xl font-semibold text-white mb-2">Uninstall App?</h3>
|
||||
<p class="text-white/70">
|
||||
Are you sure you want to uninstall <span class="text-white font-medium">{{ uninstallModal.appTitle }}</span>?
|
||||
This will remove the app and stop its container.
|
||||
@@ -183,6 +190,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- Action error toast -->
|
||||
<Transition name="fade">
|
||||
<div v-if="actionError" class="fixed bottom-20 left-1/2 -translate-x-1/2 z-50 max-w-md w-full px-4" role="alert" aria-live="assertive">
|
||||
<div class="bg-red-500/20 border border-red-500/40 backdrop-blur-sm rounded-lg px-4 py-3 text-red-200 text-sm flex items-center justify-between gap-3">
|
||||
<span>{{ actionError }}</span>
|
||||
<button @click="actionError = ''" aria-label="Dismiss error" class="text-red-300 hover:text-white shrink-0">×</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -191,7 +208,7 @@ import { computed, ref, onBeforeUnmount } from 'vue'
|
||||
import { useRouter, RouterLink } from 'vue-router'
|
||||
import { useAppStore } from '../stores/app'
|
||||
import { useAppLauncherStore } from '../stores/appLauncher'
|
||||
import { PackageState } from '../types/api'
|
||||
import { PackageState, type PackageDataEntry } from '../types/api'
|
||||
import { useModalKeyboard } from '@/composables/useModalKeyboard'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -203,6 +220,16 @@ const searchQuery = ref('')
|
||||
// Track loading states for each app action
|
||||
const loadingActions = ref<Record<string, boolean>>({})
|
||||
|
||||
// Action error toast
|
||||
const actionError = ref('')
|
||||
let errorTimer: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
function showActionError(msg: string) {
|
||||
actionError.value = msg
|
||||
if (errorTimer) clearTimeout(errorTimer)
|
||||
errorTimer = setTimeout(() => { actionError.value = '' }, 5000)
|
||||
}
|
||||
|
||||
// Use real packages from store - no more dummy apps
|
||||
const packages = computed(() => {
|
||||
const realPackages = store.packages
|
||||
@@ -246,13 +273,13 @@ useModalKeyboard(
|
||||
{ restoreFocusRef: uninstallRestoreFocusRef }
|
||||
)
|
||||
|
||||
function canLaunch(pkg: any): boolean {
|
||||
function canLaunch(pkg: PackageDataEntry): boolean {
|
||||
// For dummy apps, allow launch if running (they have interface addresses)
|
||||
// For real apps, check for UI interface
|
||||
const hasUI = pkg.manifest.interfaces?.main?.ui || pkg.installed?.['interface-addresses']?.main
|
||||
// Allow launch when running or starting (so buttons show even while backend reports "starting")
|
||||
const canLaunchState = pkg.state === 'running' || pkg.state === 'starting'
|
||||
return hasUI && canLaunchState
|
||||
return !!hasUI && canLaunchState
|
||||
}
|
||||
|
||||
function launchApp(id: string) {
|
||||
@@ -341,7 +368,8 @@ async function startApp(id: string) {
|
||||
actionTimers.delete(id)
|
||||
}, 5000))
|
||||
} catch (err) {
|
||||
console.error('Failed to start app:', err)
|
||||
if (import.meta.env.DEV) console.error('Failed to start app:', err)
|
||||
showActionError(`Failed to start app: ${err instanceof Error ? err.message : 'Unknown error'}`)
|
||||
loadingActions.value[id] = false
|
||||
}
|
||||
}
|
||||
@@ -356,7 +384,8 @@ async function stopApp(id: string) {
|
||||
actionTimers.delete(id)
|
||||
}, 5000))
|
||||
} catch (err) {
|
||||
console.error('Failed to stop app:', err)
|
||||
if (import.meta.env.DEV) console.error('Failed to stop app:', err)
|
||||
showActionError(`Failed to stop app: ${err instanceof Error ? err.message : 'Unknown error'}`)
|
||||
loadingActions.value[id] = false
|
||||
}
|
||||
}
|
||||
@@ -373,11 +402,11 @@ async function _restartApp(_id: string) {
|
||||
try {
|
||||
await store.restartPackage(_id)
|
||||
} catch (err) {
|
||||
console.error('Failed to restart app:', err)
|
||||
if (import.meta.env.DEV) console.error('Failed to restart app:', err)
|
||||
}
|
||||
}
|
||||
|
||||
function showUninstallModal(id: string, pkg: any) {
|
||||
function showUninstallModal(id: string, pkg: PackageDataEntry) {
|
||||
uninstallModal.value = {
|
||||
show: true,
|
||||
appId: id,
|
||||
@@ -392,8 +421,8 @@ async function confirmUninstall() {
|
||||
try {
|
||||
await store.uninstallPackage(appId)
|
||||
} catch (err) {
|
||||
console.error('Failed to uninstall app:', err)
|
||||
alert('Failed to uninstall app')
|
||||
if (import.meta.env.DEV) console.error('Failed to uninstall app:', err)
|
||||
showActionError(`Failed to uninstall app: ${err instanceof Error ? err.message : 'Unknown error'}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user