feat(studio): Welcome page Phase C — live getting-started checklist (v0.39.0)
Final slice of the Welcome surface. Four steps that prove
the operator has the basic loop wired up; each step is
a live probe against the running hub, no manual ticks.
The four checked signals:
1. System AI configured
→ `HubService.systemAiStatus().enabled`
2. Public capability source added
→ `listMcpClients()` returns at least one entry
3. Text module installed
→ `listModules()` contains a name starting with `text.`
4. Saved flow run
→ any `flow.completed` event in the last 100 audit
events
All four probes fire in parallel from `_OnboardingChecklist`'s
`_refresh()`. Failures stay false (catchError) so a hub that
is briefly unreachable doesn't blank the whole row — the
operator hits the explicit Refresh icon to retry.
UX:
- Renders between hero and pillars so the actionable path
beats the educational content for screen real estate.
- Each row: status icon (○ pending / ✓ done) + title +
one-line hint pointing the operator at the right Studio
surface + status pill.
- Done rows strike-through the title and dim it; pending
rows stay full-strength.
- When all four flip to done, an "All four steps complete"
footer appears with a `Hide checklist` button. Clicking
persists `welcome.checklist.dismissed = true` via
SharedPreferences; the section is gone for good (we don't
re-nag operators who chose a non-default path).
15 new ARB keys for the section header, body, four rows
plus done/pending pills, all-done footer, dismiss /
refresh / refreshing button labels.
Three-phase plan from `docs/landing-page-design.md` is now
fully shipped: Phase A scaffolding (v0.37.0), Phase B
embedded doc reader (v0.38.0), Phase C live checklist
(v0.39.0).
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
3b07d340d5
commit
930e246649
8 changed files with 496 additions and 2 deletions
|
|
@ -33,6 +33,22 @@
|
|||
"welcomeDocClose": "Schließen",
|
||||
"welcomeDocFailedToLoad": "Doku konnte nicht geladen werden: {error}",
|
||||
"@welcomeDocFailedToLoad": { "placeholders": { "error": { "type": "String" } } },
|
||||
"welcomeChecklistHeader": "ERSTE SCHRITTE",
|
||||
"welcomeChecklistBody": "Vier Schritte zu einem funktionierenden Hub. Live-Zustand — nach jeder Änderung aktualisieren.",
|
||||
"welcomeChecklistAi": "System-AI konfigurieren",
|
||||
"welcomeChecklistAiHint": "Einstellungen → System-AI. Wird für die KI-Suche und Fehlererklärungen gebraucht.",
|
||||
"welcomeChecklistMcp": "Öffentliche Capability-Quelle hinzufügen",
|
||||
"welcomeChecklistMcpHint": "Store → Today-Hero, oder Einstellungen → MCP-Clients. DeepWiki und Semgrep sind je ein Klick.",
|
||||
"welcomeChecklistModule": "Ein Text-Modul installieren",
|
||||
"welcomeChecklistModuleHint": "Store → nach „text\" suchen → Installieren. text.extract für PDFs und DOCX ist ein guter Start.",
|
||||
"welcomeChecklistFlow": "Einen gespeicherten Flow starten",
|
||||
"welcomeChecklistFlowHint": "Flows → einen wählen → Starten. Der mitgelieferte extract-summarize.yaml ist ein guter erster Lauf.",
|
||||
"welcomeChecklistAllDone": "Alle vier Schritte erledigt.",
|
||||
"welcomeChecklistDismiss": "Checkliste ausblenden",
|
||||
"welcomeChecklistRefresh": "Aktualisieren",
|
||||
"welcomeChecklistRefreshing": "Prüfe…",
|
||||
"welcomeChecklistDone": "Erledigt",
|
||||
"welcomeChecklistTodo": "Offen",
|
||||
"navDoctor": "Diagnose",
|
||||
"navModules": "Module",
|
||||
"navStore": "Store",
|
||||
|
|
|
|||
|
|
@ -34,6 +34,22 @@
|
|||
"welcomeDocClose": "Close",
|
||||
"welcomeDocFailedToLoad": "Could not load documentation: {error}",
|
||||
"@welcomeDocFailedToLoad": { "placeholders": { "error": { "type": "String" } } },
|
||||
"welcomeChecklistHeader": "GETTING STARTED",
|
||||
"welcomeChecklistBody": "Four steps to a working hub. Live state — refresh after changing anything.",
|
||||
"welcomeChecklistAi": "Configure System AI",
|
||||
"welcomeChecklistAiHint": "Settings → System AI. Required for the AI search and inline failure explanations.",
|
||||
"welcomeChecklistMcp": "Add a public capability source",
|
||||
"welcomeChecklistMcpHint": "Store → Today hero, or Settings → MCP Clients. DeepWiki and Semgrep are one click each.",
|
||||
"welcomeChecklistModule": "Install a text module",
|
||||
"welcomeChecklistModuleHint": "Store → search \"text\" → Install. Try text.extract for PDFs and DOCX.",
|
||||
"welcomeChecklistFlow": "Run a saved flow",
|
||||
"welcomeChecklistFlowHint": "Flows → pick one → Run. The bundled extract-summarize.yaml is a good first run.",
|
||||
"welcomeChecklistAllDone": "All four steps complete.",
|
||||
"welcomeChecklistDismiss": "Hide checklist",
|
||||
"welcomeChecklistRefresh": "Refresh",
|
||||
"welcomeChecklistRefreshing": "Checking…",
|
||||
"welcomeChecklistDone": "Done",
|
||||
"welcomeChecklistTodo": "Pending",
|
||||
"navDoctor": "Doctor",
|
||||
"navModules": "Modules",
|
||||
"navStore": "Store",
|
||||
|
|
|
|||
|
|
@ -278,6 +278,102 @@ abstract class AppLocalizations {
|
|||
/// **'Could not load documentation: {error}'**
|
||||
String welcomeDocFailedToLoad(String error);
|
||||
|
||||
/// No description provided for @welcomeChecklistHeader.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'GETTING STARTED'**
|
||||
String get welcomeChecklistHeader;
|
||||
|
||||
/// No description provided for @welcomeChecklistBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Four steps to a working hub. Live state — refresh after changing anything.'**
|
||||
String get welcomeChecklistBody;
|
||||
|
||||
/// No description provided for @welcomeChecklistAi.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Configure System AI'**
|
||||
String get welcomeChecklistAi;
|
||||
|
||||
/// No description provided for @welcomeChecklistAiHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Settings → System AI. Required for the AI search and inline failure explanations.'**
|
||||
String get welcomeChecklistAiHint;
|
||||
|
||||
/// No description provided for @welcomeChecklistMcp.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add a public capability source'**
|
||||
String get welcomeChecklistMcp;
|
||||
|
||||
/// No description provided for @welcomeChecklistMcpHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Store → Today hero, or Settings → MCP Clients. DeepWiki and Semgrep are one click each.'**
|
||||
String get welcomeChecklistMcpHint;
|
||||
|
||||
/// No description provided for @welcomeChecklistModule.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Install a text module'**
|
||||
String get welcomeChecklistModule;
|
||||
|
||||
/// No description provided for @welcomeChecklistModuleHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Store → search \"text\" → Install. Try text.extract for PDFs and DOCX.'**
|
||||
String get welcomeChecklistModuleHint;
|
||||
|
||||
/// No description provided for @welcomeChecklistFlow.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Run a saved flow'**
|
||||
String get welcomeChecklistFlow;
|
||||
|
||||
/// No description provided for @welcomeChecklistFlowHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Flows → pick one → Run. The bundled extract-summarize.yaml is a good first run.'**
|
||||
String get welcomeChecklistFlowHint;
|
||||
|
||||
/// No description provided for @welcomeChecklistAllDone.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'All four steps complete.'**
|
||||
String get welcomeChecklistAllDone;
|
||||
|
||||
/// No description provided for @welcomeChecklistDismiss.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Hide checklist'**
|
||||
String get welcomeChecklistDismiss;
|
||||
|
||||
/// No description provided for @welcomeChecklistRefresh.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Refresh'**
|
||||
String get welcomeChecklistRefresh;
|
||||
|
||||
/// No description provided for @welcomeChecklistRefreshing.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Checking…'**
|
||||
String get welcomeChecklistRefreshing;
|
||||
|
||||
/// No description provided for @welcomeChecklistDone.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Done'**
|
||||
String get welcomeChecklistDone;
|
||||
|
||||
/// No description provided for @welcomeChecklistTodo.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Pending'**
|
||||
String get welcomeChecklistTodo;
|
||||
|
||||
/// No description provided for @navDoctor.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -112,6 +112,59 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
return 'Doku konnte nicht geladen werden: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get welcomeChecklistHeader => 'ERSTE SCHRITTE';
|
||||
|
||||
@override
|
||||
String get welcomeChecklistBody =>
|
||||
'Vier Schritte zu einem funktionierenden Hub. Live-Zustand — nach jeder Änderung aktualisieren.';
|
||||
|
||||
@override
|
||||
String get welcomeChecklistAi => 'System-AI konfigurieren';
|
||||
|
||||
@override
|
||||
String get welcomeChecklistAiHint =>
|
||||
'Einstellungen → System-AI. Wird für die KI-Suche und Fehlererklärungen gebraucht.';
|
||||
|
||||
@override
|
||||
String get welcomeChecklistMcp => 'Öffentliche Capability-Quelle hinzufügen';
|
||||
|
||||
@override
|
||||
String get welcomeChecklistMcpHint =>
|
||||
'Store → Today-Hero, oder Einstellungen → MCP-Clients. DeepWiki und Semgrep sind je ein Klick.';
|
||||
|
||||
@override
|
||||
String get welcomeChecklistModule => 'Ein Text-Modul installieren';
|
||||
|
||||
@override
|
||||
String get welcomeChecklistModuleHint =>
|
||||
'Store → nach „text\" suchen → Installieren. text.extract für PDFs und DOCX ist ein guter Start.';
|
||||
|
||||
@override
|
||||
String get welcomeChecklistFlow => 'Einen gespeicherten Flow starten';
|
||||
|
||||
@override
|
||||
String get welcomeChecklistFlowHint =>
|
||||
'Flows → einen wählen → Starten. Der mitgelieferte extract-summarize.yaml ist ein guter erster Lauf.';
|
||||
|
||||
@override
|
||||
String get welcomeChecklistAllDone => 'Alle vier Schritte erledigt.';
|
||||
|
||||
@override
|
||||
String get welcomeChecklistDismiss => 'Checkliste ausblenden';
|
||||
|
||||
@override
|
||||
String get welcomeChecklistRefresh => 'Aktualisieren';
|
||||
|
||||
@override
|
||||
String get welcomeChecklistRefreshing => 'Prüfe…';
|
||||
|
||||
@override
|
||||
String get welcomeChecklistDone => 'Erledigt';
|
||||
|
||||
@override
|
||||
String get welcomeChecklistTodo => 'Offen';
|
||||
|
||||
@override
|
||||
String get navDoctor => 'Diagnose';
|
||||
|
||||
|
|
|
|||
|
|
@ -112,6 +112,59 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
return 'Could not load documentation: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get welcomeChecklistHeader => 'GETTING STARTED';
|
||||
|
||||
@override
|
||||
String get welcomeChecklistBody =>
|
||||
'Four steps to a working hub. Live state — refresh after changing anything.';
|
||||
|
||||
@override
|
||||
String get welcomeChecklistAi => 'Configure System AI';
|
||||
|
||||
@override
|
||||
String get welcomeChecklistAiHint =>
|
||||
'Settings → System AI. Required for the AI search and inline failure explanations.';
|
||||
|
||||
@override
|
||||
String get welcomeChecklistMcp => 'Add a public capability source';
|
||||
|
||||
@override
|
||||
String get welcomeChecklistMcpHint =>
|
||||
'Store → Today hero, or Settings → MCP Clients. DeepWiki and Semgrep are one click each.';
|
||||
|
||||
@override
|
||||
String get welcomeChecklistModule => 'Install a text module';
|
||||
|
||||
@override
|
||||
String get welcomeChecklistModuleHint =>
|
||||
'Store → search \"text\" → Install. Try text.extract for PDFs and DOCX.';
|
||||
|
||||
@override
|
||||
String get welcomeChecklistFlow => 'Run a saved flow';
|
||||
|
||||
@override
|
||||
String get welcomeChecklistFlowHint =>
|
||||
'Flows → pick one → Run. The bundled extract-summarize.yaml is a good first run.';
|
||||
|
||||
@override
|
||||
String get welcomeChecklistAllDone => 'All four steps complete.';
|
||||
|
||||
@override
|
||||
String get welcomeChecklistDismiss => 'Hide checklist';
|
||||
|
||||
@override
|
||||
String get welcomeChecklistRefresh => 'Refresh';
|
||||
|
||||
@override
|
||||
String get welcomeChecklistRefreshing => 'Checking…';
|
||||
|
||||
@override
|
||||
String get welcomeChecklistDone => 'Done';
|
||||
|
||||
@override
|
||||
String get welcomeChecklistTodo => 'Pending';
|
||||
|
||||
@override
|
||||
String get navDoctor => 'Doctor';
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import 'widgets/widgets.dart';
|
|||
/// Studio's own build version. Bump on every UI commit so the
|
||||
/// running app self-identifies — visible in the sidebar header
|
||||
/// and quick-glance proof that you're seeing the current build.
|
||||
const String kStudioVersion = '0.38.0';
|
||||
const String kStudioVersion = '0.39.0';
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
|
|
|||
|
|
@ -11,11 +11,14 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../data/hub.dart';
|
||||
import '../data/system_actions.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../theme/theme.dart';
|
||||
import '../theme/tokens.dart';
|
||||
import '../widgets/widgets.dart';
|
||||
|
||||
class WelcomePage extends StatelessWidget {
|
||||
const WelcomePage({super.key});
|
||||
|
|
@ -45,6 +48,10 @@ class WelcomePage extends StatelessWidget {
|
|||
children: const [
|
||||
_Hero(),
|
||||
SizedBox(height: FaiSpace.xxl),
|
||||
// Checklist comes second — operator-actionable
|
||||
// path beats educational content. Auto-hides
|
||||
// once the operator dismisses it.
|
||||
_OnboardingChecklist(),
|
||||
_SectionLabel(textKey: _SectionLabelKey.pillars),
|
||||
SizedBox(height: FaiSpace.md),
|
||||
_PillarRow(),
|
||||
|
|
@ -359,6 +366,259 @@ class _TrustRow extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
/// SharedPreferences key the checklist persists "operator
|
||||
/// dismissed me" under. Once true, the checklist is gone for
|
||||
/// good — this is intentional; we don't want to nag operators
|
||||
/// who chose a non-default path (e.g. CLI-only, no Studio
|
||||
/// onboarding needed).
|
||||
const String _kChecklistDismissedKey = 'welcome.checklist.dismissed';
|
||||
|
||||
/// Live getting-started checklist. Probes the running hub for
|
||||
/// four signals that the operator has the basic loop wired up
|
||||
/// (System AI configured, MCP source added, text module
|
||||
/// installed, any flow run). Each signal is fetched in
|
||||
/// parallel; failures stay false and the checklist still
|
||||
/// renders (with the "Refresh" button for retry).
|
||||
class _OnboardingChecklist extends StatefulWidget {
|
||||
const _OnboardingChecklist();
|
||||
|
||||
@override
|
||||
State<_OnboardingChecklist> createState() => _OnboardingChecklistState();
|
||||
}
|
||||
|
||||
class _OnboardingChecklistState extends State<_OnboardingChecklist> {
|
||||
bool _dismissed = false;
|
||||
bool _initialised = false;
|
||||
bool _refreshing = false;
|
||||
bool _aiOk = false;
|
||||
bool _mcpOk = false;
|
||||
bool _moduleOk = false;
|
||||
bool _flowOk = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bootstrap();
|
||||
}
|
||||
|
||||
Future<void> _bootstrap() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_dismissed = prefs.getBool(_kChecklistDismissedKey) ?? false;
|
||||
_initialised = true;
|
||||
});
|
||||
if (!_dismissed) {
|
||||
await _refresh();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
setState(() => _refreshing = true);
|
||||
final hub = HubService.instance;
|
||||
// Probe each signal independently. A failure in one (e.g.
|
||||
// hub temporarily unreachable) leaves the others
|
||||
// computable; the checklist degrades gracefully.
|
||||
final ai = hub
|
||||
.systemAiStatus()
|
||||
.then((s) => s.enabled)
|
||||
.catchError((_) => false);
|
||||
final mcp = hub
|
||||
.listMcpClients()
|
||||
.then((c) => c.isNotEmpty)
|
||||
.catchError((_) => false);
|
||||
final module = hub
|
||||
.listModules()
|
||||
.then((m) => m.any((x) => x.name.startsWith('text.')))
|
||||
.catchError((_) => false);
|
||||
final flow = hub
|
||||
.recentEvents(limit: 100)
|
||||
.then((events) => events.any((e) => e.type == 'flow.completed'))
|
||||
.catchError((_) => false);
|
||||
final results = await Future.wait([ai, mcp, module, flow]);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_refreshing = false;
|
||||
_aiOk = results[0];
|
||||
_mcpOk = results[1];
|
||||
_moduleOk = results[2];
|
||||
_flowOk = results[3];
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _dismiss() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_kChecklistDismissedKey, true);
|
||||
if (!mounted) return;
|
||||
setState(() => _dismissed = true);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_initialised || _dismissed) return const SizedBox.shrink();
|
||||
final theme = Theme.of(context);
|
||||
final l = AppLocalizations.of(context)!;
|
||||
final allDone = _aiOk && _mcpOk && _moduleOk && _flowOk;
|
||||
final items = <(bool, String, String)>[
|
||||
(_aiOk, l.welcomeChecklistAi, l.welcomeChecklistAiHint),
|
||||
(_mcpOk, l.welcomeChecklistMcp, l.welcomeChecklistMcpHint),
|
||||
(_moduleOk, l.welcomeChecklistModule, l.welcomeChecklistModuleHint),
|
||||
(_flowOk, l.welcomeChecklistFlow, l.welcomeChecklistFlowHint),
|
||||
];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: FaiSpace.xxl),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
l.welcomeChecklistHeader,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
letterSpacing: 0.6,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: _refreshing
|
||||
? const SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.refresh, size: 16),
|
||||
visualDensity: VisualDensity.compact,
|
||||
tooltip: _refreshing
|
||||
? l.welcomeChecklistRefreshing
|
||||
: l.welcomeChecklistRefresh,
|
||||
onPressed: _refreshing ? null : _refresh,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
l.welcomeChecklistBody,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: FaiSpace.md),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(FaiRadius.md),
|
||||
border: Border.all(color: theme.colorScheme.outlineVariant),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
for (var i = 0; i < items.length; i++) ...[
|
||||
if (i > 0)
|
||||
Divider(
|
||||
height: 1,
|
||||
color: theme.colorScheme.outlineVariant,
|
||||
),
|
||||
_ChecklistRow(
|
||||
done: items[i].$1,
|
||||
title: items[i].$2,
|
||||
hint: items[i].$3,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (allDone) ...[
|
||||
const SizedBox(height: FaiSpace.md),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.check_circle,
|
||||
size: 16,
|
||||
color: FaiColors.success,
|
||||
),
|
||||
const SizedBox(width: FaiSpace.xs),
|
||||
Text(
|
||||
l.welcomeChecklistAllDone,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
FilledButton.tonal(
|
||||
onPressed: _dismiss,
|
||||
child: Text(l.welcomeChecklistDismiss),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ChecklistRow extends StatelessWidget {
|
||||
final bool done;
|
||||
final String title;
|
||||
final String hint;
|
||||
|
||||
const _ChecklistRow({
|
||||
required this.done,
|
||||
required this.title,
|
||||
required this.hint,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l = AppLocalizations.of(context)!;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(FaiSpace.lg),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
done ? Icons.check_circle : Icons.radio_button_unchecked,
|
||||
size: 20,
|
||||
color: done ? FaiColors.success : theme.colorScheme.outline,
|
||||
),
|
||||
const SizedBox(width: FaiSpace.lg),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: done
|
||||
? theme.colorScheme.onSurfaceVariant
|
||||
: theme.colorScheme.onSurface,
|
||||
decoration: done ? TextDecoration.lineThrough : null,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
hint,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: FaiSpace.md),
|
||||
FaiPill(
|
||||
label: done ? l.welcomeChecklistDone : l.welcomeChecklistTodo,
|
||||
tone: done ? FaiPillTone.success : FaiPillTone.neutral,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Doc-card entries — each links to a bundled markdown file
|
||||
/// under `assets/docs/`. The slug is the filename stem; the
|
||||
/// loader picks `<slug>.md` or `<slug>_de.md` per locale.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue