/// The guided stepper's full-screen per-step view. /// /// Split out of `note_editor.dart` for file size. Stateless on purpose: the /// controllers, the current step and the template all stay owned by /// `_NoteEditorState`, and are passed in — this widget only lays them out. /// /// Returned directly from the editor's `build` so the [TextField] is a /// first-level [Expanded] child of THIS column, the same depth as the raw /// editor's. Nesting it inside a second Column broke the flutter constraint /// chain in release builds (the inner Expanded got 0 height), so the flat /// structure here is load-bearing rather than incidental. library; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:todo/data/note_template.dart'; import 'package:todo/ui/motion.dart'; import 'package:todo/ui/template_glyphs.dart'; /// One step of the guided editor: label, guidance, input, and navigation. class NoteEditorStepPage extends StatelessWidget { /// Creates the step view for [section], step [index] of [total]. const NoteEditorStepPage({ required this.section, required this.controller, required this.index, required this.total, required this.autofocus, required this.onChanged, required this.onGoToStep, required this.onExit, super.key, }); /// The template section this step edits. final TemplateSection section; /// Controller for this section's text, owned by the editor. final TextEditingController controller; /// Zero-based index of this step, already clamped to the section range. final int index; /// How many steps the template has in total. final int total; /// Whether the input should take focus on first build. final bool autofocus; /// Called on every keystroke so the editor can re-emit the assembled text. final VoidCallback onChanged; /// Called with the step index to move to. final ValueChanged onGoToStep; /// Called by the back arrow and by "Done" on the last step. final VoidCallback onExit; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Back arrow — only exit from guided mode. Align( alignment: Alignment.centerLeft, child: IconButton( icon: const Icon(Icons.arrow_back, semanticLabel: 'Exit guided'), onPressed: onExit, ), ), // Progress bar + step counter. Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 4), child: Row( children: [ Text('${index + 1} / $total', style: theme.textTheme.labelMedium), const SizedBox(width: 8), Expanded( child: TweenAnimationBuilder( tween: Tween(end: (index + 1) / total), duration: motionDuration(context, AppDuration.base), curve: AppCurve.standard, builder: (_, value, _) => LinearProgressIndicator(value: value), ), ), ], ), ), // Section label + helper text. Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // A glyph per section; its label (the note's `## heading`) and // the helper sentence are spoken only. Icon( sectionIcon(section), color: theme.colorScheme.primary, semanticLabel: section.isTitle ? section.helper : '${section.label}: ${section.helper}', ), const SizedBox(height: 12), ], ), ), // TextField fills all remaining space — directly in this Column's // Expanded, same as the raw editor, so expands:true works correctly. Expanded( child: Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 0), child: TextField( controller: controller, autofocus: autofocus && index == 0, expands: true, maxLines: null, keyboardType: TextInputType.multiline, textAlignVertical: TextAlignVertical.top, // No hintText: a hint is painted text. The section glyph // above already names the field. decoration: const InputDecoration( border: OutlineInputBorder(), isDense: true, ), onChanged: (_) => onChanged(), ), ), ), // Navigation buttons — sibling of Expanded so keyboard pushes them up. Padding( padding: const EdgeInsets.all(16), child: Row( children: [ if (index > 0) TextButton( onPressed: () => onGoToStep(index - 1), child: const Icon( Icons.navigate_before, semanticLabel: 'Back', ), ), const Spacer(), if (index < total - 1) FilledButton( onPressed: () => onGoToStep(index + 1), child: const Icon(Icons.navigate_next, semanticLabel: 'Next'), ) else FilledButton( onPressed: onExit, child: const Icon(Icons.check, semanticLabel: 'Done'), ), ], ), ), ], ); } }