fix(ui): WCAG light-theme accent, overflow-safe audit page, formal address
Some checks are pending
Security / Security check (push) Waiting to run
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:
parent
efaa089454
commit
da58125f20
9 changed files with 339 additions and 95 deletions
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue