Add responsive people profile editor flow
This commit is contained in:
@@ -7,6 +7,28 @@ Updated: 2026-02-22
|
||||
- After every sensible code/documentation change set, create a git commit as
|
||||
the last step so the next agent session can pick up from clean checkpoints.
|
||||
|
||||
## Latest Milestone (2026-02-22): Responsive People Add/Edit Form (Sheet + Dialog)
|
||||
|
||||
Aligned the People profile creation/edit flow with the newer insights-style UI
|
||||
language and improved platform-specific presentation behavior.
|
||||
|
||||
- `lib/features/people/people_view.dart`
|
||||
- replaced the old `AlertDialog` person editor with a shared responsive form
|
||||
panel:
|
||||
- mobile (`<720px`): `showModalBottomSheet` with a rounded sheet layout
|
||||
- desktop/web/tablet: larger custom dialog panel
|
||||
- added richer form UX with:
|
||||
- hero preview (avatar + live preview chips for name/relationship/location/tags)
|
||||
- grouped sections (`Identity`, `Context`) matching the People/Insights card
|
||||
style
|
||||
- preserved existing labels and `Save` action text for test compatibility
|
||||
- kept add/edit repository behavior unchanged (local-first)
|
||||
- fixed narrow-width section header overflow discovered by widget test
|
||||
|
||||
- Validation
|
||||
- `flutter analyze` -> pass
|
||||
- `flutter test` -> pass
|
||||
|
||||
## Latest Milestone (2026-02-22): People Page UX Redesign (Insights-Aligned)
|
||||
|
||||
Reworked the `People` page to feel closer to the graph long-press `Person
|
||||
|
||||
@@ -221,10 +221,9 @@ class PeopleView extends ConsumerWidget {
|
||||
}
|
||||
|
||||
Future<void> _handleAddPerson(BuildContext context, WidgetRef ref) async {
|
||||
final _PersonDraft? draft = await showDialog<_PersonDraft>(
|
||||
context: context,
|
||||
builder: (BuildContext context) =>
|
||||
const _PersonEditorDialog(title: 'Add Person'),
|
||||
final _PersonDraft? draft = await _showPersonEditor(
|
||||
context,
|
||||
title: 'Add Person',
|
||||
);
|
||||
|
||||
if (draft == null) {
|
||||
@@ -265,17 +264,15 @@ class PeopleView extends ConsumerWidget {
|
||||
WidgetRef ref,
|
||||
PersonProfile person,
|
||||
) async {
|
||||
final _PersonDraft? draft = await showDialog<_PersonDraft>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => _PersonEditorDialog(
|
||||
title: 'Edit Person',
|
||||
initial: _PersonDraft(
|
||||
name: person.name,
|
||||
relationship: person.relationship,
|
||||
notes: person.notes,
|
||||
tags: person.tags,
|
||||
location: person.location,
|
||||
),
|
||||
final _PersonDraft? draft = await _showPersonEditor(
|
||||
context,
|
||||
title: 'Edit Person',
|
||||
initial: _PersonDraft(
|
||||
name: person.name,
|
||||
relationship: person.relationship,
|
||||
notes: person.notes,
|
||||
tags: person.tags,
|
||||
location: person.location,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -623,6 +620,32 @@ class PeopleView extends ConsumerWidget {
|
||||
.replaceAll(RegExp(r'\s+'), ' ');
|
||||
}
|
||||
|
||||
Future<_PersonDraft?> _showPersonEditor(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
_PersonDraft? initial,
|
||||
}) {
|
||||
final bool compact = MediaQuery.sizeOf(context).width < 720;
|
||||
if (compact) {
|
||||
return showModalBottomSheet<_PersonDraft>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (BuildContext context) {
|
||||
return _PersonEditorBottomSheet(title: title, initial: initial);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return showDialog<_PersonDraft>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return _PersonEditorDialog(title: title, initial: initial);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<String?> _showQuickTextCaptureDialog(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
@@ -1715,17 +1738,72 @@ class _PersonListItem extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _PersonEditorDialog extends StatefulWidget {
|
||||
class _PersonEditorDialog extends StatelessWidget {
|
||||
const _PersonEditorDialog({required this.title, this.initial});
|
||||
|
||||
final String title;
|
||||
final _PersonDraft? initial;
|
||||
|
||||
@override
|
||||
State<_PersonEditorDialog> createState() => _PersonEditorDialogState();
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 28, vertical: 24),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 720),
|
||||
child: _PersonEditorPanel(
|
||||
title: title,
|
||||
initial: initial,
|
||||
compact: false,
|
||||
sheetStyle: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PersonEditorDialogState extends State<_PersonEditorDialog> {
|
||||
class _PersonEditorBottomSheet extends StatelessWidget {
|
||||
const _PersonEditorBottomSheet({required this.title, this.initial});
|
||||
|
||||
final String title;
|
||||
final _PersonDraft? initial;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
bottom: MediaQuery.viewInsetsOf(context).bottom + 8,
|
||||
),
|
||||
child: _PersonEditorPanel(
|
||||
title: title,
|
||||
initial: initial,
|
||||
compact: true,
|
||||
sheetStyle: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PersonEditorPanel extends StatefulWidget {
|
||||
const _PersonEditorPanel({
|
||||
required this.title,
|
||||
required this.compact,
|
||||
required this.sheetStyle,
|
||||
this.initial,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final bool compact;
|
||||
final bool sheetStyle;
|
||||
final _PersonDraft? initial;
|
||||
|
||||
@override
|
||||
State<_PersonEditorPanel> createState() => _PersonEditorPanelState();
|
||||
}
|
||||
|
||||
class _PersonEditorPanelState extends State<_PersonEditorPanel> {
|
||||
late final TextEditingController _nameController;
|
||||
late final TextEditingController _relationshipController;
|
||||
late final TextEditingController _locationController;
|
||||
@@ -1735,99 +1813,447 @@ class _PersonEditorDialogState extends State<_PersonEditorDialog> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_nameController = TextEditingController(text: widget.initial?.name ?? '');
|
||||
_nameController = TextEditingController(text: widget.initial?.name ?? '')
|
||||
..addListener(_handlePreviewChanged);
|
||||
_relationshipController = TextEditingController(
|
||||
text: widget.initial?.relationship ?? '',
|
||||
);
|
||||
)..addListener(_handlePreviewChanged);
|
||||
_locationController = TextEditingController(
|
||||
text: widget.initial?.location ?? '',
|
||||
);
|
||||
)..addListener(_handlePreviewChanged);
|
||||
_tagsController = TextEditingController(
|
||||
text: widget.initial?.tags.join(', ') ?? '',
|
||||
);
|
||||
)..addListener(_handlePreviewChanged);
|
||||
_notesController = TextEditingController(text: widget.initial?.notes ?? '');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_relationshipController.dispose();
|
||||
_locationController.dispose();
|
||||
_tagsController.dispose();
|
||||
_nameController
|
||||
..removeListener(_handlePreviewChanged)
|
||||
..dispose();
|
||||
_relationshipController
|
||||
..removeListener(_handlePreviewChanged)
|
||||
..dispose();
|
||||
_locationController
|
||||
..removeListener(_handlePreviewChanged)
|
||||
..dispose();
|
||||
_tagsController
|
||||
..removeListener(_handlePreviewChanged)
|
||||
..dispose();
|
||||
_notesController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handlePreviewChanged() {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(widget.title),
|
||||
content: SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 420),
|
||||
final String name = _nameController.text.trim();
|
||||
final String relationship = _relationshipController.text.trim();
|
||||
final String? location = _locationController.text.trim().isEmpty
|
||||
? null
|
||||
: _locationController.text.trim();
|
||||
final List<String> tags = _tagsController.text
|
||||
.split(',')
|
||||
.map((String tag) => tag.trim())
|
||||
.where((String tag) => tag.isNotEmpty)
|
||||
.toList(growable: false);
|
||||
|
||||
final BorderRadius radius = BorderRadius.circular(widget.compact ? 24 : 22);
|
||||
final double maxHeight = widget.compact ? 720 : 760;
|
||||
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: maxHeight,
|
||||
minHeight: widget.compact ? 360 : 420,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: radius,
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: <Color>[Color(0xFFF7FBFD), Color(0xFFEAF3F9)],
|
||||
),
|
||||
border: Border.all(color: Colors.white.withValues(alpha: 0.95)),
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.08),
|
||||
blurRadius: 26,
|
||||
offset: const Offset(0, 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: radius,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
TextField(
|
||||
controller: _nameController,
|
||||
decoration: const InputDecoration(labelText: 'Name'),
|
||||
),
|
||||
TextField(
|
||||
controller: _relationshipController,
|
||||
decoration: const InputDecoration(labelText: 'Relationship'),
|
||||
),
|
||||
TextField(
|
||||
controller: _locationController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Location (optional)',
|
||||
if (widget.sheetStyle)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Container(
|
||||
width: 42,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFCCD6DD),
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
widget.compact ? 14 : 18,
|
||||
widget.compact ? 10 : 16,
|
||||
widget.compact ? 14 : 18,
|
||||
12,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
_PersonEditorHero(
|
||||
title: widget.title,
|
||||
compact: widget.compact,
|
||||
previewName: name,
|
||||
previewRelationship: relationship,
|
||||
previewLocation: location,
|
||||
previewTags: tags,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (widget.compact)
|
||||
Column(
|
||||
children: <Widget>[
|
||||
_PersonEditorSectionCard(
|
||||
title: 'Identity',
|
||||
icon: Icons.person_outline_rounded,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
TextField(
|
||||
controller: _nameController,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Name',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextField(
|
||||
controller: _relationshipController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Relationship',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextField(
|
||||
controller: _locationController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Location (optional)',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_PersonEditorSectionCard(
|
||||
title: 'Context',
|
||||
icon: Icons.auto_awesome_rounded,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
TextField(
|
||||
controller: _tagsController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Tags (comma separated)',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextField(
|
||||
controller: _notesController,
|
||||
minLines: 3,
|
||||
maxLines: 5,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Notes',
|
||||
hintText:
|
||||
'Preferences, boundaries, gift clues, recurring themes...',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: _PersonEditorSectionCard(
|
||||
title: 'Identity',
|
||||
icon: Icons.person_outline_rounded,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
TextField(
|
||||
controller: _nameController,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Name',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextField(
|
||||
controller: _relationshipController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Relationship',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextField(
|
||||
controller: _locationController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Location (optional)',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _PersonEditorSectionCard(
|
||||
title: 'Context',
|
||||
icon: Icons.auto_awesome_rounded,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
TextField(
|
||||
controller: _tagsController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Tags (comma separated)',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextField(
|
||||
controller: _notesController,
|
||||
minLines: 5,
|
||||
maxLines: 8,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Notes',
|
||||
hintText:
|
||||
'Preferences, boundaries, gift clues, recurring themes...',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
TextField(
|
||||
controller: _tagsController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Tags (comma separated)',
|
||||
Container(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
widget.compact ? 14 : 18,
|
||||
10,
|
||||
widget.compact ? 14 : 18,
|
||||
widget.compact ? 14 : 16,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.75),
|
||||
border: Border(
|
||||
top: BorderSide(color: Colors.white.withValues(alpha: 0.9)),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Required: name and relationship',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
final String submittedName = _nameController.text
|
||||
.trim();
|
||||
final String submittedRelationship =
|
||||
_relationshipController.text.trim();
|
||||
if (submittedName.isEmpty ||
|
||||
submittedRelationship.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final _PersonDraft draft = _PersonDraft(
|
||||
name: submittedName,
|
||||
relationship: submittedRelationship,
|
||||
location: _locationController.text.trim().isEmpty
|
||||
? null
|
||||
: _locationController.text.trim(),
|
||||
notes: _notesController.text.trim(),
|
||||
tags: _tagsController.text
|
||||
.split(',')
|
||||
.map((String tag) => tag.trim())
|
||||
.where((String tag) => tag.isNotEmpty)
|
||||
.toList(growable: false),
|
||||
);
|
||||
Navigator.of(context).pop(draft);
|
||||
},
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
TextField(
|
||||
controller: _notesController,
|
||||
maxLines: 3,
|
||||
decoration: const InputDecoration(labelText: 'Notes'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
final String name = _nameController.text.trim();
|
||||
final String relationship = _relationshipController.text.trim();
|
||||
if (name.isEmpty || relationship.isEmpty) {
|
||||
return;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final _PersonDraft draft = _PersonDraft(
|
||||
name: name,
|
||||
relationship: relationship,
|
||||
location: _locationController.text.trim().isEmpty
|
||||
? null
|
||||
: _locationController.text.trim(),
|
||||
notes: _notesController.text.trim(),
|
||||
tags: _tagsController.text
|
||||
.split(',')
|
||||
.map((String tag) => tag.trim())
|
||||
.where((String tag) => tag.isNotEmpty)
|
||||
.toList(growable: false),
|
||||
);
|
||||
Navigator.of(context).pop(draft);
|
||||
},
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
class _PersonEditorHero extends StatelessWidget {
|
||||
const _PersonEditorHero({
|
||||
required this.title,
|
||||
required this.compact,
|
||||
required this.previewName,
|
||||
required this.previewRelationship,
|
||||
required this.previewLocation,
|
||||
required this.previewTags,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final bool compact;
|
||||
final String previewName;
|
||||
final String previewRelationship;
|
||||
final String? previewLocation;
|
||||
final List<String> previewTags;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String displayName = previewName.isEmpty ? 'New person' : previewName;
|
||||
final String displayRelationship = previewRelationship.isEmpty
|
||||
? 'Relationship type'
|
||||
: previewRelationship;
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(compact ? 14 : 16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: Colors.white.withValues(alpha: 0.72),
|
||||
border: Border.all(color: Colors.white),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
_AvatarSeed(name: displayName, size: compact ? 50 : 56),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(title, style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Shape the profile first, then use quick-add from the People workspace.',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: <Widget>[
|
||||
_InlineMetaPill(
|
||||
icon: Icons.person_outline_rounded,
|
||||
label: displayName,
|
||||
),
|
||||
_InlineMetaPill(
|
||||
icon: Icons.favorite_outline_rounded,
|
||||
label: displayRelationship,
|
||||
),
|
||||
if (previewLocation != null)
|
||||
_InlineMetaPill(
|
||||
icon: Icons.location_on_outlined,
|
||||
label: previewLocation!,
|
||||
),
|
||||
...previewTags
|
||||
.take(compact ? 2 : 4)
|
||||
.map(
|
||||
(String tag) =>
|
||||
_InlineMetaPill(icon: Icons.sell_outlined, label: tag),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PersonEditorSectionCard extends StatelessWidget {
|
||||
const _PersonEditorSectionCard({
|
||||
required this.title,
|
||||
required this.icon,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final IconData icon;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.78),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.white),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Icon(icon, size: 18, color: AppTheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
child,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user