feat(docs): in-place help pattern — explain a surface where it happens
Some checks failed
Security / Security check (push) Failing after 2s

New ChainInlineHelp (intro strip: what this is + what will happen, with
an optional 'Learn more' into the doc sheet) and ChainFieldHelp /
ChainFieldLabel (a '?' affordance per field). First applied to the
add-satellite dialog, which asked for a bare 'name' with no hint of
what a satellite is or does (usertest): it now leads with a plain
explanation + a federation 'Learn more', and the name field carries a
'?'. Both widgets are quiet by design.

Verified: field_help_test covers the widgets + that the dialog explains
itself; dialog_shots_test.dart (a reusable headed dialog-capture
harness) proved the layout in light + dark. Studio 0.76.0.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-07-20 02:00:56 +02:00
parent 5a3f00bb2c
commit 87afa4dc05
8 changed files with 390 additions and 9 deletions

View file

@ -0,0 +1,169 @@
// In-place help the "explain it where it happens" pattern
// (usertest: the Add-satellite dialog asked for a name with no hint
// of what a satellite is or what the name does). Two pieces:
//
// ChainInlineHelp a calm intro strip at the top of a dialog
// or surface: one plain sentence saying what this is and what
// will happen, with an optional "Learn more" link into the
// full doc sheet.
// ChainFieldHelp a small "?" affordance to sit next to a
// single field's label; tap/hover reveals a one-line
// explanation. Use it only where a field genuinely needs it.
//
// Both are intentionally quiet: help should be present, not loud.
import 'package:flutter/material.dart';
import '../theme/tokens.dart';
/// A one-sentence intro strip for the top of a dialog or panel.
/// [onLearnMore] wires the "Learn more" link to a doc sheet
/// (`showFaiDoc`), shown only when provided.
class ChainInlineHelp extends StatelessWidget {
final String text;
final IconData icon;
final VoidCallback? onLearnMore;
final String? learnMoreLabel;
const ChainInlineHelp({
super.key,
required this.text,
this.icon = Icons.info_outline,
this.onLearnMore,
this.learnMoreLabel,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
padding: const EdgeInsets.all(ChainSpace.md),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(ChainRadius.sm),
border: Border.all(color: theme.colorScheme.outlineVariant),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 16, color: theme.colorScheme.primary),
const SizedBox(width: ChainSpace.sm),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
text,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface,
height: 1.4,
),
),
if (onLearnMore != null) ...[
const SizedBox(height: ChainSpace.xs),
InkWell(
onTap: onLearnMore,
borderRadius: BorderRadius.circular(ChainRadius.sm),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
learnMoreLabel ?? 'Learn more',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 2),
Icon(
Icons.arrow_forward,
size: 12,
color: theme.colorScheme.primary,
),
],
),
),
),
],
],
),
),
],
),
);
}
}
/// A "?" info affordance for a single field. Sit it next to the
/// field's label; hover shows the [message] as a tooltip, and a
/// tap reveals it too (touch / keyboard users who don't hover).
/// Semantics carry [message] for screen readers.
class ChainFieldHelp extends StatefulWidget {
final String message;
const ChainFieldHelp({super.key, required this.message});
@override
State<ChainFieldHelp> createState() => _ChainFieldHelpState();
}
class _ChainFieldHelpState extends State<ChainFieldHelp> {
final _tooltipKey = GlobalKey<TooltipState>();
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Tooltip(
key: _tooltipKey,
message: widget.message,
waitDuration: const Duration(milliseconds: 300),
triggerMode: TooltipTriggerMode.manual,
preferBelow: false,
child: Semantics(
button: true,
label: widget.message,
child: InkResponse(
radius: 14,
// Manual trigger so a tap (not just hover) reveals it.
onTap: () => _tooltipKey.currentState?.ensureTooltipVisible(),
child: Padding(
padding: const EdgeInsets.all(2),
child: Icon(
Icons.help_outline,
size: 14,
color: theme.colorScheme.onSurfaceVariant,
),
),
),
),
);
}
}
/// A field label with a trailing [ChainFieldHelp]. Convenience for
/// the common "label + ?" row above a TextField.
class ChainFieldLabel extends StatelessWidget {
final String label;
final String help;
const ChainFieldLabel({super.key, required this.label, required this.help});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Row(
children: [
Text(
label,
style: theme.textTheme.labelMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(width: 4),
ChainFieldHelp(message: help),
],
);
}
}