fix(ui): WCAG light-theme accent, overflow-safe audit page, formal address
Some checks are pending
Security / Security check (push) Waiting to run

Accessibility/responsive audit pass with two new permanent test
gates (test/a11y_test.dart: WCAG text contrast + labeled tap
targets on every page in both themes; test/responsive_test.dart:
no layout overflow at 800/960/1280/1920 px). Findings fixed:

- Light theme primary/tertiary sky-500 → sky-700: white text on
  the lighter accent only reached 2.8:1 (welcome CTA, active
  sidebar label); sky-700 clears WCAG AA at ~5.9:1. Dark theme
  unchanged (already compliant). FABs now follow the same accent
  instead of Material 3's washed-out tonal default.
- Audit page: filter chips collapse into a checkmark popup menu
  below 900 px window width (app bar overflowed); the live-status
  bar's left text is now Expanded with ellipsis so the row can
  shrink, and the disconnected state's copyable error gets the
  full remaining width.
- German strings now use formal address consistently (~20 strings
  still used du-forms next to Sie-forms on welcome/setup), the
  audit event-type chip "Step" is "Schritt", and the doctor
  page's event count pluralises correctly in both languages.

flutter analyze clean, 64 tests green. Screenshot pass light+dark
via the guide-shots harness (verified parity, no overflows).

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-07-15 04:34:19 +02:00
parent efaa089454
commit da58125f20
9 changed files with 339 additions and 95 deletions

View file

@ -155,12 +155,20 @@ class _AuditPageState extends State<AuditPage> {
actions: [
const ChainWorkspaceSwitcher(),
const SizedBox(width: ChainSpace.md),
// Narrow windows can't fit the inline chip row next to the
// workspace switcher collapse to a checkmark menu so the
// app bar never overflows (responsive_test.dart pins this).
Padding(
padding: const EdgeInsets.only(right: ChainSpace.lg),
child: _FilterChips(
value: _typeFilter,
onChanged: (v) => setState(() => _typeFilter = v),
),
child: MediaQuery.sizeOf(context).width < 900
? _FilterMenu(
value: _typeFilter,
onChanged: (v) => setState(() => _typeFilter = v),
)
: _FilterChips(
value: _typeFilter,
onChanged: (v) => setState(() => _typeFilter = v),
),
),
IconButton(
icon: const Icon(Icons.help_outline, size: 18),
@ -368,6 +376,49 @@ class _EventItem extends _ListItem {
const _EventItem(this.event);
}
List<(String, String)> _filterItems(AppLocalizations l) => [
('all', l.auditFilterAll),
('flow.', l.auditFilterFlow),
('step.', l.auditFilterStep),
('module.', l.auditFilterModule),
];
/// Narrow-window replacement for [_FilterChips]: one icon button
/// with a checkmark menu. The icon takes the accent colour while a
/// filter other than "all" is active, so a narrowed window never
/// hides that the list is filtered.
class _FilterMenu extends StatelessWidget {
final String value;
final ValueChanged<String> onChanged;
const _FilterMenu({required this.value, required this.onChanged});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final l = AppLocalizations.of(context)!;
return PopupMenuButton<String>(
tooltip: l.auditFilterTooltip,
icon: Icon(
Icons.filter_list,
size: 18,
color: value == 'all'
? theme.colorScheme.onSurfaceVariant
: theme.colorScheme.primary,
),
onSelected: onChanged,
itemBuilder: (_) => [
for (final (v, label) in _filterItems(l))
CheckedPopupMenuItem(
value: v,
checked: value == v,
child: Text(label),
),
],
);
}
}
class _FilterChips extends StatelessWidget {
final String value;
final ValueChanged<String> onChanged;
@ -377,12 +428,7 @@ class _FilterChips extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
final items = [
('all', l.auditFilterAll),
('flow.', l.auditFilterFlow),
('step.', l.auditFilterStep),
('module.', l.auditFilterModule),
];
final items = _filterItems(l);
return Row(
children: [
for (final (v, label) in items)
@ -498,42 +544,44 @@ class _LiveStatusBar extends StatelessWidget {
// Disconnected SelectableText so the raw error is copyable
// (it's the only place the underlying failure is surfaced;
// the empty-state below shows only a generic hint).
if (live)
Text(
l.auditLiveStatus(eventCount),
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
)
else
Flexible(
child: SelectableText(
l.auditDisconnected(error!),
maxLines: 2,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
),
const Spacer(),
if (live)
Row(
children: [
Icon(
Icons.shield_outlined,
size: 12,
color: theme.colorScheme.primary,
),
const SizedBox(width: 4),
Text(
l.auditHashChainVerified,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w500,
//
// The left text is Expanded (not natural-width + Spacer):
// it must yield when the window narrows otherwise this
// row overflows (responsive_test.dart pins it) and its
// tight fill keeps the hash-chain trailing right-aligned.
Expanded(
child: live
? Text(
l.auditLiveStatus(eventCount),
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
)
: SelectableText(
l.auditDisconnected(error!),
maxLines: 2,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
),
],
),
if (live) ...[
const SizedBox(width: ChainSpace.md),
Icon(
Icons.shield_outlined,
size: 12,
color: theme.colorScheme.primary,
),
const SizedBox(width: 4),
Text(
l.auditHashChainVerified,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w500,
),
),
],
],
),
);