diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c9a7b5..3f3c785 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,24 +6,6 @@ lockstep. ## Unreleased -### Added — explain it where it happens (0.76.0) - -- **In-place help pattern.** New `ChainInlineHelp` (a one-sentence - intro strip that says what a surface is and what will happen, with - an optional "Learn more" link into the doc sheet) and - `ChainFieldHelp` / `ChainFieldLabel` (a "?" affordance for a single - field). First applied to the add-satellite dialog, which used to - ask for a "name" with no hint of what a satellite even is - (usertest): it now leads with a plain explanation + a federation - "Learn more", and the name field carries a "?". -- **Page help opens the RIGHT topic.** The Federation and Runs help - buttons silently fell back to the architecture doc — `federation` - and `runs` had markdown assets but no catalog entry, so - `showFaiDoc` resolved them to the first doc. Both are registered - now; a new `doc_help_wiring_test.dart` asserts every - `showFaiDoc('slug')` call has a registered entry AND both locale - assets, so a help button can never open the wrong topic again. - ### Security (0.75.0) - **Sealed-area names are confidential by default.** The workspace diff --git a/integration_test/dialog_shots_test.dart b/integration_test/dialog_shots_test.dart deleted file mode 100644 index 7d70768..0000000 --- a/integration_test/dialog_shots_test.dart +++ /dev/null @@ -1,79 +0,0 @@ -// Dialog capture harness — visual proof for dialogs the page-level -// guide harness doesn't reach. Renders the target dialog against -// the scriptable fake hub and writes a PNG per theme via a -// RepaintBoundary (driverless, headed macOS `flutter test`). -// -// flutter test integration_test/dialog_shots_test.dart -d macos -// -// Output: build/dialog-shots/-.png (or $DIALOG_SHOTS_OUT). - -import 'dart:io'; -import 'dart:ui' as ui; - -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -import 'package:chain_studio/l10n/app_localizations.dart'; -import 'package:chain_studio/pages/federation.dart'; - -import '../test/support/fake_hub.dart'; - -final GlobalKey _shotKey = GlobalKey(); - -String get _outDir => - Platform.environment['DIALOG_SHOTS_OUT'] ?? 'build/dialog-shots'; - -Future _shot(WidgetTester tester, String name) async { - await tester.pump(const Duration(milliseconds: 150)); - await tester.pump(const Duration(milliseconds: 150)); - final boundary = - _shotKey.currentContext!.findRenderObject() as RenderRepaintBoundary; - final image = await boundary.toImage(pixelRatio: 2.0); - final bytes = await image.toByteData(format: ui.ImageByteFormat.png); - image.dispose(); - final file = File('$_outDir/$name.png'); - file.parent.createSync(recursive: true); - file.writeAsBytesSync(bytes!.buffer.asUint8List()); - // ignore: avoid_print - print('dialog-shot: ${file.path}'); -} - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - for (final (themeName, mode) in [ - ('light', ThemeMode.light), - ('dark', ThemeMode.dark), - ]) { - testWidgets('add-satellite dialog — $themeName', (tester) async { - SharedPreferences.setMockInitialValues({}); - installFakeHub(); - await tester.pumpWidget( - MaterialApp( - debugShowCheckedModeBanner: false, - themeMode: mode, - theme: ThemeData.light(useMaterial3: true), - darkTheme: ThemeData.dark(useMaterial3: true), - localizationsDelegates: AppLocalizations.localizationsDelegates, - supportedLocales: AppLocalizations.supportedLocales, - locale: const Locale('de'), - // Wrap the whole navigator (incl. the overlay dialogs - // render into) so the capture catches the dialog, not - // just the page beneath it. - builder: (context, child) => - RepaintBoundary(key: _shotKey, child: child), - home: const FederationPage(), - ), - ); - await tester.pump(const Duration(milliseconds: 200)); - await tester.tap(find.text('Satellit hinzufügen').first); - await tester.pumpAndSettle(); - await _shot(tester, 'add-satellite-$themeName'); - expect(find.textContaining('Ein Satellit ist ein weiterer Hub'), - findsOneWidget); - }); - } -} diff --git a/lib/data/about_info.dart b/lib/data/about_info.dart index f867338..d618851 100644 --- a/lib/data/about_info.dart +++ b/lib/data/about_info.dart @@ -4,7 +4,7 @@ /// Studio's own build version. Bump on every UI release so the /// running app self-identifies. -const String kStudioVersion = '0.76.0'; +const String kStudioVersion = '0.75.0'; const String kProductName = 'Ch∆In Studio'; const String kVendorName = 'Flemming.AI (F∆I)'; diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 822947f..c35637c 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -50,10 +50,6 @@ "welcomeDocFlowsBlurb": "YAML-Grundlagen, Templating-Referenz, das Extract→Summarize-Beispiel.", "welcomeDocApprovalsTitle": "Freigaben", "welcomeDocApprovalsBlurb": "Human-in-the-Loop-Checkpoints — wann nutzen, wie das Audit-Log sie protokolliert.", - "welcomeDocFederationTitle": "Föderation", - "welcomeDocFederationBlurb": "Weitere Hubs (Satelliten) anbinden, damit ihre Fähigkeiten hier nutzbar werden — was ein Satellit ist und wie die Anbindung abläuft.", - "welcomeDocRunsTitle": "Hintergrund-Läufe", - "welcomeDocRunsBlurb": "Flows, die im Hintergrund weiterlaufen — wozu, wie man die Funktion einschaltet und die Läufe überwacht.", "helpTooltip": "Hilfe", "errApprovalRejected": "Freigabe vom Reviewer abgelehnt.", "errApprovalRejectedHint": "Der Reviewer hat den Step abgelehnt. Begründung steht im Audit-Log.", @@ -108,7 +104,6 @@ "sidebarSettingsTooltip": "Einstellungen (⌘;)", "sidebarChannelTooltip": "Aktiver Kanal — klicken zum Wechseln.\nproduction = stabil · beta = Vorabversion · dev = laufend · local = Arbeitsumgebung", "buttonCancel": "Abbrechen", - "buttonLearnMore": "Mehr erfahren", "buttonSave": "Speichern", "buttonClose": "Schließen", "buttonRetry": "Erneut versuchen", @@ -1769,8 +1764,6 @@ "federationCapabilities": "{count} angebotene Fähigkeiten", "federationAddDialogTitle": "Registrierungs-Token ausstellen", "federationNameLabel": "Satelliten-Name", - "federationAddIntro": "Ein Satellit ist ein weiterer Hub, den dieser Hub anbindet — dessen Fähigkeiten werden dann hier in Flows nutzbar. Sie vergeben einen Namen und erhalten ein einmaliges Token, mit dem sich der Satellit bei diesem Hub anmeldet.", - "federationNameHelp": "Frei wählbarer Name, um diesen Satelliten in der Liste wiederzuerkennen (z. B. Standort oder Zweck). Rein zur Anzeige — ändert nichts an der Technik.", "federationIssueButton": "Ausstellen", "federationIssueFailed": "Token konnte nicht ausgestellt werden: {error}", "federationEnrollmentTitle": "{name} einrichten", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 874e9ac..8a887a4 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -58,10 +58,6 @@ "welcomeDocFlowsBlurb": "YAML basics, templating reference, the extract→summarize example.", "welcomeDocApprovalsTitle": "Approvals", "welcomeDocApprovalsBlurb": "Human-in-the-loop checkpoints — when to use them, how the audit log records them.", - "welcomeDocFederationTitle": "Federation", - "welcomeDocFederationBlurb": "Connect other hubs (satellites) so their capabilities become usable here — what a satellite is and how enrolment works.", - "welcomeDocRunsTitle": "Background runs", - "welcomeDocRunsBlurb": "Flows that keep working in the background — what they are for, how to switch the feature on and monitor the runs.", "helpTooltip": "Help", "errApprovalRejected": "Approval rejected by reviewer.", "errApprovalRejectedHint": "The reviewer marked this step as rejected. Check the audit log for the reviewer's reason.", @@ -116,7 +112,6 @@ "sidebarSettingsTooltip": "Settings (⌘;)", "sidebarChannelTooltip": "Active channel — click to switch.\nproduction = stable · beta = pre-release · dev = rolling · local = workspace", "buttonCancel": "Cancel", - "buttonLearnMore": "Learn more", "buttonSave": "Save", "buttonClose": "Close", "buttonRetry": "Retry", @@ -1798,8 +1793,6 @@ }, "federationAddDialogTitle": "Issue an enrollment token", "federationNameLabel": "Satellite name", - "federationAddIntro": "A satellite is another hub this hub connects to — its capabilities then become usable here in flows. You give it a name and receive a one-time token the satellite uses to enrol with this hub.", - "federationNameHelp": "A free-form name to recognize this satellite in the list (e.g. its location or purpose). Display only — it changes nothing technical.", "federationIssueButton": "Issue", "federationIssueFailed": "Could not issue token: {error}", "@federationIssueFailed": { diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index c3c747f..65bd2a7 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -398,30 +398,6 @@ abstract class AppLocalizations { /// **'Human-in-the-loop checkpoints — when to use them, how the audit log records them.'** String get welcomeDocApprovalsBlurb; - /// No description provided for @welcomeDocFederationTitle. - /// - /// In en, this message translates to: - /// **'Federation'** - String get welcomeDocFederationTitle; - - /// No description provided for @welcomeDocFederationBlurb. - /// - /// In en, this message translates to: - /// **'Connect other hubs (satellites) so their capabilities become usable here — what a satellite is and how enrolment works.'** - String get welcomeDocFederationBlurb; - - /// No description provided for @welcomeDocRunsTitle. - /// - /// In en, this message translates to: - /// **'Background runs'** - String get welcomeDocRunsTitle; - - /// No description provided for @welcomeDocRunsBlurb. - /// - /// In en, this message translates to: - /// **'Flows that keep working in the background — what they are for, how to switch the feature on and monitor the runs.'** - String get welcomeDocRunsBlurb; - /// No description provided for @helpTooltip. /// /// In en, this message translates to: @@ -704,12 +680,6 @@ abstract class AppLocalizations { /// **'Cancel'** String get buttonCancel; - /// No description provided for @buttonLearnMore. - /// - /// In en, this message translates to: - /// **'Learn more'** - String get buttonLearnMore; - /// No description provided for @buttonSave. /// /// In en, this message translates to: @@ -5377,18 +5347,6 @@ abstract class AppLocalizations { /// **'Satellite name'** String get federationNameLabel; - /// No description provided for @federationAddIntro. - /// - /// In en, this message translates to: - /// **'A satellite is another hub this hub connects to — its capabilities then become usable here in flows. You give it a name and receive a one-time token the satellite uses to enrol with this hub.'** - String get federationAddIntro; - - /// No description provided for @federationNameHelp. - /// - /// In en, this message translates to: - /// **'A free-form name to recognize this satellite in the list (e.g. its location or purpose). Display only — it changes nothing technical.'** - String get federationNameHelp; - /// No description provided for @federationIssueButton. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index a72ae77..a2d5768 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -180,20 +180,6 @@ class AppLocalizationsDe extends AppLocalizations { String get welcomeDocApprovalsBlurb => 'Human-in-the-Loop-Checkpoints — wann nutzen, wie das Audit-Log sie protokolliert.'; - @override - String get welcomeDocFederationTitle => 'Föderation'; - - @override - String get welcomeDocFederationBlurb => - 'Weitere Hubs (Satelliten) anbinden, damit ihre Fähigkeiten hier nutzbar werden — was ein Satellit ist und wie die Anbindung abläuft.'; - - @override - String get welcomeDocRunsTitle => 'Hintergrund-Läufe'; - - @override - String get welcomeDocRunsBlurb => - 'Flows, die im Hintergrund weiterlaufen — wozu, wie man die Funktion einschaltet und die Läufe überwacht.'; - @override String get helpTooltip => 'Hilfe'; @@ -352,9 +338,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get buttonCancel => 'Abbrechen'; - @override - String get buttonLearnMore => 'Mehr erfahren'; - @override String get buttonSave => 'Speichern'; @@ -3181,14 +3164,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get federationNameLabel => 'Satelliten-Name'; - @override - String get federationAddIntro => - 'Ein Satellit ist ein weiterer Hub, den dieser Hub anbindet — dessen Fähigkeiten werden dann hier in Flows nutzbar. Sie vergeben einen Namen und erhalten ein einmaliges Token, mit dem sich der Satellit bei diesem Hub anmeldet.'; - - @override - String get federationNameHelp => - 'Frei wählbarer Name, um diesen Satelliten in der Liste wiederzuerkennen (z. B. Standort oder Zweck). Rein zur Anzeige — ändert nichts an der Technik.'; - @override String get federationIssueButton => 'Ausstellen'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 5e766b5..6b9c2b5 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -181,20 +181,6 @@ class AppLocalizationsEn extends AppLocalizations { String get welcomeDocApprovalsBlurb => 'Human-in-the-loop checkpoints — when to use them, how the audit log records them.'; - @override - String get welcomeDocFederationTitle => 'Federation'; - - @override - String get welcomeDocFederationBlurb => - 'Connect other hubs (satellites) so their capabilities become usable here — what a satellite is and how enrolment works.'; - - @override - String get welcomeDocRunsTitle => 'Background runs'; - - @override - String get welcomeDocRunsBlurb => - 'Flows that keep working in the background — what they are for, how to switch the feature on and monitor the runs.'; - @override String get helpTooltip => 'Help'; @@ -352,9 +338,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get buttonCancel => 'Cancel'; - @override - String get buttonLearnMore => 'Learn more'; - @override String get buttonSave => 'Save'; @@ -3176,14 +3159,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get federationNameLabel => 'Satellite name'; - @override - String get federationAddIntro => - 'A satellite is another hub this hub connects to — its capabilities then become usable here in flows. You give it a name and receive a one-time token the satellite uses to enrol with this hub.'; - - @override - String get federationNameHelp => - 'A free-form name to recognize this satellite in the list (e.g. its location or purpose). Display only — it changes nothing technical.'; - @override String get federationIssueButton => 'Issue'; diff --git a/lib/pages/federation.dart b/lib/pages/federation.dart index c4b0b15..d9cdaf7 100644 --- a/lib/pages/federation.dart +++ b/lib/pages/federation.dart @@ -61,38 +61,13 @@ class _FederationPageState extends State { context: context, builder: (ctx) => AlertDialog( title: Text(l.federationAddDialogTitle), - content: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 420), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Say what this is and what will happen before asking - // for input — a bare "name" field left the operator - // guessing what a satellite even is (usertest). - ChainInlineHelp( - text: l.federationAddIntro, - icon: Icons.hub_outlined, - onLearnMore: () => showFaiDoc(ctx, 'federation'), - learnMoreLabel: l.buttonLearnMore, - ), - const SizedBox(height: ChainSpace.lg), - ChainFieldLabel( - label: l.federationNameLabel, - help: l.federationNameHelp, - ), - const SizedBox(height: ChainSpace.xs), - TextField( - controller: controller, - autofocus: true, - decoration: const InputDecoration( - hintText: 'satellite-a', - border: OutlineInputBorder(), - isDense: true, - ), - onSubmitted: (v) => Navigator.pop(ctx, v.trim()), - ), - ], + content: TextField( + controller: controller, + autofocus: true, + decoration: InputDecoration( + labelText: l.federationNameLabel, + hintText: 'satellite-a', + border: const OutlineInputBorder(), ), ), actions: [ diff --git a/lib/pages/welcome.dart b/lib/pages/welcome.dart index 96a9fd7..09e975b 100644 --- a/lib/pages/welcome.dart +++ b/lib/pages/welcome.dart @@ -1048,28 +1048,14 @@ class _DocEntry { final IconData icon; final String Function(AppLocalizations) title; final String Function(AppLocalizations) blurb; - - /// Whether this entry is shown as a card in the Welcome page's - /// doc grid. Every entry is reachable via [showFaiDoc] regardless; - /// `onWelcome: false` keeps advanced topics (federation, runs) out - /// of the newcomer grid while still wiring their page help buttons. - final bool onWelcome; - const _DocEntry({ required this.slug, required this.icon, required this.title, required this.blurb, - this.onWelcome = true, }); } -/// Every doc topic Studio can open in-app. The slug maps to -/// `assets/docs/[_].md`. Adding a page help button -/// (`showFaiDoc(context, '')`) requires a matching entry here -/// AND the asset files — `doc_help_wiring_test.dart` enforces both, -/// so a help button can never silently fall back to the wrong topic -/// again (federation/runs used to resolve to architecture). final List<_DocEntry> _kDocs = <_DocEntry>[ _DocEntry( slug: 'architecture', @@ -1101,47 +1087,17 @@ final List<_DocEntry> _kDocs = <_DocEntry>[ title: (l) => l.welcomeDocApprovalsTitle, blurb: (l) => l.welcomeDocApprovalsBlurb, ), - _DocEntry( - slug: 'federation', - icon: Icons.hub_outlined, - title: (l) => l.welcomeDocFederationTitle, - blurb: (l) => l.welcomeDocFederationBlurb, - onWelcome: false, - ), - _DocEntry( - slug: 'runs', - icon: Icons.rocket_launch_outlined, - title: (l) => l.welcomeDocRunsTitle, - blurb: (l) => l.welcomeDocRunsBlurb, - onWelcome: false, - ), ]; -/// The doc cards shown on the Welcome page (curated newcomer set). -final List<_DocEntry> _kWelcomeDocs = - _kDocs.where((d) => d.onWelcome).toList(); - -/// Slugs Studio can resolve in-app — exposed for the wiring guard -/// test so it can assert every `showFaiDoc` call has a home. -final Set kKnownDocSlugs = _kDocs.map((d) => d.slug).toSet(); - /// Public entry-point for the doc-reader sheet. Pass a slug /// ("approvals", "audit", "security", "architecture", "flows") /// and the bottom sheet opens with the localized markdown. /// Returns null when the slug isn't registered — caller can /// log + show a SnackBar. Future? showFaiDoc(BuildContext context, String slug) { - // A slug with no entry is a wiring bug (doc_help_wiring_test.dart - // guards against it). The fallback keeps the UI alive in release, - // but we assert in debug so the mistake surfaces during - // development rather than silently opening the wrong topic. final entry = _kDocs.firstWhere( (d) => d.slug == slug, - orElse: () { - assert(false, 'showFaiDoc: unknown doc slug "$slug" — register it ' - 'in _kDocs and add assets/docs/$slug[_de].md'); - return _kDocs.first; - }, + orElse: () => _kDocs.first, ); return _DocReaderSheet.show(context, entry); } @@ -1174,9 +1130,9 @@ class _DocsRow extends StatelessWidget { if (!twoCols) { return Column( children: [ - for (var i = 0; i < _kWelcomeDocs.length; i++) ...[ + for (var i = 0; i < _kDocs.length; i++) ...[ if (i > 0) const SizedBox(height: ChainSpace.md), - _DocCard(entry: _kWelcomeDocs[i]), + _DocCard(entry: _kDocs[i]), ], ], ); @@ -1188,12 +1144,12 @@ class _DocsRow extends StatelessWidget { // card spans the full width so it reads as intentional // rather than a lonely half-box with dead space beside it. final rows = []; - for (var i = 0; i < _kWelcomeDocs.length; i += 2) { + for (var i = 0; i < _kDocs.length; i += 2) { if (rows.isNotEmpty) { rows.add(const SizedBox(height: ChainSpace.md)); } - final left = _kWelcomeDocs[i]; - final right = i + 1 < _kWelcomeDocs.length ? _kWelcomeDocs[i + 1] : null; + final left = _kDocs[i]; + final right = i + 1 < _kDocs.length ? _kDocs[i + 1] : null; if (right == null) { rows.add(_DocCard(entry: left)); } else { diff --git a/lib/widgets/chain_field_help.dart b/lib/widgets/chain_field_help.dart deleted file mode 100644 index 9ce8db9..0000000 --- a/lib/widgets/chain_field_help.dart +++ /dev/null @@ -1,169 +0,0 @@ -// 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 createState() => _ChainFieldHelpState(); -} - -class _ChainFieldHelpState extends State { - final _tooltipKey = GlobalKey(); - - @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), - ], - ); - } -} diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index 9870981..1de0fec 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -11,7 +11,6 @@ export 'chain_delta_mark.dart'; export 'chain_empty_state.dart'; export 'chain_en_badge.dart'; export 'chain_error_box.dart'; -export 'chain_field_help.dart'; export 'chain_segments.dart'; export 'hub_load_error_view.dart'; export 'chain_flow_output.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index c6dba7b..01aa360 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: chain_studio description: "Ch∆In Studio — desktop GUI for the Ch∆In hub" publish_to: 'none' -version: 0.76.0 +version: 0.75.0 environment: sdk: ^3.11.0-200.1.beta diff --git a/test/doc_help_wiring_test.dart b/test/doc_help_wiring_test.dart deleted file mode 100644 index 6e23251..0000000 --- a/test/doc_help_wiring_test.dart +++ /dev/null @@ -1,70 +0,0 @@ -// Doc-help wiring guard — a page help button must open the RIGHT -// topic. Every `showFaiDoc(context, '')` call in lib/ needs: -// -// 1. a registered _DocEntry (exposed as kKnownDocSlugs), and -// 2. the backing assets assets/docs/.md + _de.md. -// -// Without this, a slug with no entry silently fell back to the -// first doc ('architecture'): the Federation and Runs help buttons -// opened the architecture sheet even though federation.md/runs.md -// existed — the author wrote the docs, set the icon, but never -// wired the catalog, and nothing caught it. This test catches that -// whole class (per the no-bugfix-without-a-guard rule). - -import 'dart:io'; - -import 'package:flutter_test/flutter_test.dart'; - -import 'package:chain_studio/pages/welcome.dart' show kKnownDocSlugs; - -void main() { - final callSlugs = _showFaiDocSlugsInLib(); - - test('lib/ actually calls showFaiDoc somewhere (sanity)', () { - expect( - callSlugs, - isNotEmpty, - reason: 'no showFaiDoc calls found — did the scan regex go stale?', - ); - }); - - test('every showFaiDoc slug is registered in the doc catalog', () { - final unregistered = callSlugs.difference(kKnownDocSlugs); - expect( - unregistered, - isEmpty, - reason: - 'These slugs are opened by a help button but have no _DocEntry, ' - 'so they silently fall back to the wrong topic. Register them in ' - '_kDocs (lib/pages/welcome.dart): $unregistered', - ); - }); - - test('every registered doc slug has both locale assets', () { - final missing = []; - for (final slug in kKnownDocSlugs) { - for (final path in ['assets/docs/$slug.md', 'assets/docs/${slug}_de.md']) { - if (!File(path).existsSync()) missing.add(path); - } - } - expect( - missing, - isEmpty, - reason: 'Registered doc slugs missing their markdown assets: $missing', - ); - }); -} - -/// Scan lib/ for `showFaiDoc(context, 'slug')` and collect the slugs. -Set _showFaiDocSlugsInLib() { - final re = RegExp(r'''showFaiDoc\(\s*context\s*,\s*['"]([a-z0-9_-]+)['"]'''); - final slugs = {}; - final dir = Directory('lib'); - for (final f in dir.listSync(recursive: true).whereType()) { - if (!f.path.endsWith('.dart')) continue; - for (final m in re.allMatches(f.readAsStringSync())) { - slugs.add(m.group(1)!); - } - } - return slugs; -} diff --git a/test/field_help_test.dart b/test/field_help_test.dart deleted file mode 100644 index 763e936..0000000 --- a/test/field_help_test.dart +++ /dev/null @@ -1,89 +0,0 @@ -// In-place help widgets + the satellite dialog that consumes them. -// The pattern (usertest): explain a surface where it happens — -// an intro strip saying what this is and what will happen, plus a -// per-field "?" for the details. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -import 'package:chain_studio/l10n/app_localizations.dart'; -import 'package:chain_studio/pages/federation.dart'; -import 'package:chain_studio/widgets/chain_field_help.dart'; - -import 'support/fake_hub.dart'; - -Widget _host(Widget child) => MaterialApp( - localizationsDelegates: AppLocalizations.localizationsDelegates, - supportedLocales: AppLocalizations.supportedLocales, - locale: const Locale('de'), - home: Scaffold(body: child), -); - -void main() { - testWidgets('ChainInlineHelp shows the text and fires Learn more', ( - tester, - ) async { - var learned = false; - await tester.pumpWidget( - _host( - ChainInlineHelp( - text: 'Ein Satellit ist ein weiterer Hub.', - onLearnMore: () => learned = true, - learnMoreLabel: 'Mehr erfahren', - ), - ), - ); - expect(find.text('Ein Satellit ist ein weiterer Hub.'), findsOneWidget); - await tester.tap(find.text('Mehr erfahren')); - expect(learned, isTrue); - }); - - testWidgets('ChainInlineHelp hides Learn more when no callback', ( - tester, - ) async { - await tester.pumpWidget( - _host(const ChainInlineHelp(text: 'Nur Text, kein Link.')), - ); - expect(find.text('Nur Text, kein Link.'), findsOneWidget); - expect(find.byIcon(Icons.arrow_forward), findsNothing); - }); - - testWidgets('ChainFieldHelp tap reveals the tooltip message', (tester) async { - await tester.pumpWidget( - _host( - const ChainFieldLabel( - label: 'Satelliten-Name', - help: 'Frei wählbarer Anzeigename.', - ), - ), - ); - // The help text is not shown until the affordance is used. - expect(find.text('Frei wählbarer Anzeigename.'), findsNothing); - await tester.tap(find.byIcon(Icons.help_outline)); - await tester.pump(const Duration(milliseconds: 400)); - expect(find.text('Frei wählbarer Anzeigename.'), findsOneWidget); - }); - - testWidgets('Add-satellite dialog explains what a satellite is', ( - tester, - ) async { - SharedPreferences.setMockInitialValues({}); - installFakeHub(); - await tester.pumpWidget(_host(const FederationPage())); - await tester.pump(const Duration(milliseconds: 200)); - - // Open the add flow via the AppBar action (the label also - // appears in the empty-state CTA; either opens the dialog). - await tester.tap(find.text('Satellit hinzufügen').first); - await tester.pumpAndSettle(); - - // The intro strip + its Learn-more link are present BEFORE any - // input is asked for — no more bare "name" field. - expect(find.textContaining('Ein Satellit ist ein weiterer Hub'), - findsOneWidget); - expect(find.text('Mehr erfahren'), findsOneWidget); - // Field-level "?" is wired too. - expect(find.byType(ChainFieldHelp), findsWidgets); - }); -}