Skip to content

Commit 31f2e14

Browse files
author
Lukas Gundermann
committed
feat: Add list components and showcase screen
This commit introduces several new list-related widgets and a dedicated component showcase screen to demonstrate their usage. ### Key Changes: * **`liIcon` Widget Extension (`lib/extensions/widget_extensions.dart`):** * A new extension on `Widget` that prepends an icon, creating a list-item appearance. * It intelligently handles `Column` widgets by applying the list-item style to each child, making it easy to convert a column of `Text` widgets into a styled list. * **New List Widgets (`lib/screens/widgets/lists/`):** * **`IconList`**: A reusable widget for displaying a list of items with a leading icon, title, and subtitle. It is theme-aware, supporting different colors for light and dark modes. * **`ContactList`**: A widget for rendering a list of contact-like items, each with an `Avatar`, title, and subtitle. * Data models (`IconListModel`, `ContactListModel`) have been added to provide a clear data structure for these new list widgets. * **Lists Showcase Screen (`lib/screens/components/lists/lists_screen.dart`):** * A new screen has been created to demonstrate the various list components, including `liIcon`, `IconList`, and `ContactList`. * Each example is presented within a `CodeCard` to show both the rendered output and the corresponding implementation code. * **Routing and Navigation:** * A new route, `/lists`, has been added to `route_config.dart` and `route_names.dart` to make the `ListsScreen` accessible. * A "Lists" entry has been added to the main navigation (`navigation_items.dart`) for easy access to the new showcase page.
1 parent 8baa979 commit 31f2e14

9 files changed

Lines changed: 471 additions & 0 deletions

File tree

lib/extensions/widget_extensions.dart

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,90 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
99
/// - safe to disable (`enabled: false` returns the original widget)
1010
///
1111
/// Included effects:
12+
/// - [liIcon]: list-item affordance (icon + spacing + content), supports Columns
1213
/// - [animatePulse]: opacity pulsing via [PulseAnimationWrapper]
1314
/// - [withHoverEffect]: hover/tap affordance via [HoverEffect] (desktop/web focused)
1415
extension WidgetExtensions on Widget {
16+
/// Prepends an icon to this widget to render it like a bullet/list item.
17+
///
18+
/// Behavior:
19+
/// - **Default:** wraps the current widget in a `Row`:
20+
/// `Icon` + spacing + `Expanded(child: this)`.
21+
/// - **Special case – Column:** if this modifier is applied to a [Column], it will
22+
/// wrap **each direct child** of that Column as an individual list item (icon per child),
23+
/// while preserving the Column's layout properties (alignment, direction, etc.).
24+
///
25+
/// Nesting / indentation:
26+
/// - Uses [Data.inherit] with [UnorderedListData] to increment a logical list depth.
27+
/// (This allows nested list-like structures to be styled/handled consistently.)
28+
///
29+
/// Parameters:
30+
/// - [icon]: the icon placed before the item (defaults to `LucideIcons.check`).
31+
/// - [color]: optional icon color.
32+
///
33+
/// Example:
34+
/// ```dart
35+
/// Column(
36+
/// crossAxisAlignment: CrossAxisAlignment.start,
37+
/// children: const [
38+
/// Text('Online-Anzeige erstatten'),
39+
/// Text('Fundstelle melden'),
40+
/// ],
41+
/// ).liIcon(color: Colors.green);
42+
/// ```
43+
///
44+
/// Tip:
45+
/// - When calling `.liIcon()` on a Column, you typically **should not** also call
46+
/// `.liIcon()` on the individual children, otherwise you will see duplicate icons.
47+
TextModifier liIcon({IconData icon = LucideIcons.check, Color? color}) =>
48+
WrappedText(
49+
wrapper: (context, child) {
50+
UnorderedListData? data = Data.maybeOf(context);
51+
int depth = data?.depth ?? 0;
52+
TextStyle style = DefaultTextStyle.of(context).style;
53+
double size = (style.fontSize ?? 12) / 6 * 6;
54+
55+
Widget wrapSingle(Widget item) {
56+
return IntrinsicWidth(
57+
child: Row(
58+
crossAxisAlignment: CrossAxisAlignment.start,
59+
children: [
60+
SizedBox(
61+
height:
62+
((style.fontSize ?? 12) * (style.height ?? 1)) * 1.2,
63+
child: Icon(icon, size: size, color: color),
64+
),
65+
const SizedBox(width: 8),
66+
Expanded(
67+
child: Data.inherit(
68+
data: UnorderedListData(depth: depth + 1),
69+
child: item,
70+
),
71+
),
72+
],
73+
),
74+
);
75+
}
76+
77+
if (child is Column) {
78+
final Column col = child;
79+
return Column(
80+
key: col.key,
81+
mainAxisAlignment: col.mainAxisAlignment,
82+
mainAxisSize: col.mainAxisSize,
83+
crossAxisAlignment: col.crossAxisAlignment,
84+
textDirection: col.textDirection,
85+
verticalDirection: col.verticalDirection,
86+
textBaseline: col.textBaseline,
87+
children: [for (final w in col.children) wrapSingle(w)],
88+
);
89+
}
90+
91+
return wrapSingle(child);
92+
},
93+
child: this,
94+
);
95+
1596
/// Applies a pulsing opacity animation to this widget.
1697
///
1798
/// This is useful for subtle attention cues (e.g. status indicators).

lib/routes/route_config.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:police_flutter_template/screens/auth/auth_not_initialized_screen
66
import 'package:police_flutter_template/screens/components/alerts/alerts_screen.dart';
77
import 'package:police_flutter_template/screens/components/buttons/buttons_screen.dart';
88
import 'package:police_flutter_template/screens/components/cards/cards_screen.dart';
9+
import 'package:police_flutter_template/screens/components/lists/lists_screen.dart';
910
import 'package:police_flutter_template/screens/error_pages/internal_server_error_screen.dart';
1011
import 'package:police_flutter_template/screens/error_pages/not_found_screen.dart';
1112
import 'package:police_flutter_template/screens/home/home_screen.dart';
@@ -195,6 +196,15 @@ class RouteConfig {
195196
),
196197
],
197198
),
199+
StatefulShellBranch(
200+
routes: [
201+
GoRoute(
202+
name: RouteNames.lists,
203+
path: RouteNames.listsUrl,
204+
builder: (context, state) => ListsScreen(),
205+
),
206+
],
207+
),
198208
],
199209
),
200210
],

lib/routes/route_names.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,10 @@ class RouteNames {
5858

5959
/// URL path for the cards demo route (e.g. `/cards`).
6060
static const String cardsUrl = '/$cards';
61+
62+
/// Route name for the "lists" component showcase/demo screen.
63+
static const String lists = "lists";
64+
65+
/// URL path for the lists demo route (e.g. `/lists`).
66+
static const String listsUrl = '/$lists';
6167
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import 'package:police_flutter_template/extensions/widget_extensions.dart';
2+
import 'package:police_flutter_template/screens/widgets/lists/contact_list.dart';
3+
import 'package:police_flutter_template/screens/widgets/lists/icon_list.dart';
4+
import 'package:police_flutter_template/screens/widgets/lists/models/contact_list_model.dart';
5+
import 'package:police_flutter_template/screens/widgets/lists/models/icon_list_model.dart';
6+
import 'package:shadcn_flutter/shadcn_flutter.dart';
7+
8+
import '../../widgets/code_card.dart';
9+
10+
class ListsScreen extends StatelessWidget {
11+
const ListsScreen({super.key});
12+
13+
@override
14+
Widget build(BuildContext context) {
15+
return ConstrainedBox(
16+
constraints: BoxConstraints(maxWidth: 752),
17+
child: Column(
18+
crossAxisAlignment: CrossAxisAlignment.stretch,
19+
children: [
20+
Text('Listen verwenden').h3,
21+
CodeCard(
22+
title: "Einfache Liste",
23+
example: SizedBox(
24+
width: double.infinity,
25+
child: Column(
26+
crossAxisAlignment: CrossAxisAlignment.start,
27+
children: [
28+
const Text('Online-Anzeige erstatten'),
29+
const Text('Fundstelle melden'),
30+
const Text('Präventionstipps lesen'),
31+
const Text('Termine vereinbaren'),
32+
],
33+
).liIcon(icon: LucideIcons.check, color: Colors.green[600]),
34+
),
35+
lines: [
36+
CodeTextLine("Column("),
37+
CodeTextLine(" crossAxisAlignment: CrossAxisAlignment.start,"),
38+
CodeTextLine(" children: const ["),
39+
CodeTextLine(" Text('Online-Anzeige erstatten'),"),
40+
CodeTextLine(" Text('Fundstelle melden'),"),
41+
CodeTextLine(" Text('Präventionstipps lesen'),"),
42+
CodeTextLine(" Text('Termine vereinbaren'),"),
43+
CodeTextLine(" ],"),
44+
CodeTextLine(
45+
").liIcon(icon: LucideIcons.check, color: Colors.green[600]),",
46+
),
47+
],
48+
),
49+
CodeCard(
50+
title: "Liste mit Icons",
51+
example: SizedBox(
52+
width: double.infinity,
53+
child: IconList(
54+
items: [
55+
IconListModel(
56+
icon: LucideIcons.phone,
57+
title: "Notruf",
58+
subtitle: "24/7 erreichbar unter 110",
59+
),
60+
IconListModel(
61+
icon: LucideIcons.mail,
62+
title: "E-Mail",
63+
subtitle: "kontakt@polizei.berlin.de",
64+
),
65+
IconListModel(
66+
icon: LucideIcons.pin,
67+
title: "Standorte",
68+
subtitle: "Über 100 Wachen in Berlin",
69+
),
70+
],
71+
),
72+
),
73+
lines: [
74+
CodeTextLine("IconList(items: ["),
75+
CodeTextLine(" IconListModel("),
76+
CodeTextLine(" icon: LucideIcons.phone,"),
77+
CodeTextLine(
78+
" title: 'Notruf',"
79+
" subtitle: '24/7 erreichbar unter 110',",
80+
),
81+
CodeTextLine(" ),"),
82+
CodeTextLine(" IconListModel("),
83+
CodeTextLine(" icon: LucideIcons.mail,"),
84+
CodeTextLine(
85+
" title: 'E-Mail',"
86+
" subtitle: 'kontakt@polizei.berlin.de',",
87+
),
88+
CodeTextLine(" ),"),
89+
CodeTextLine(" IconListModel("),
90+
CodeTextLine(" icon: LucideIcons.pin,"),
91+
CodeTextLine(
92+
" title: 'Standorte',"
93+
" subtitle: 'Über 100 Wachen in Berlin',",
94+
),
95+
CodeTextLine(" ),"),
96+
CodeTextLine("]),"),
97+
],
98+
),
99+
CodeCard(
100+
title: "Kontakt-Liste",
101+
example: SizedBox(
102+
width: double.infinity,
103+
child: ContactList(
104+
items: [
105+
ContactListModel(
106+
avatar: Avatar(
107+
initials: Avatar.getInitials('Maria Schmidt'),
108+
),
109+
title: 'Maria Schmidt',
110+
subtitle: 'Polizeioberkommissarin • PN-2024-1234',
111+
),
112+
ContactListModel(
113+
avatar: Avatar(
114+
initials: Avatar.getInitials('Thomas Müller'),
115+
),
116+
title: 'Thomas Müller',
117+
subtitle: 'Polizeihauptkommissar • PN-2024-5678',
118+
),
119+
ContactListModel(
120+
avatar: Avatar(initials: Avatar.getInitials('Lisa Weber')),
121+
title: 'Lisa Weber',
122+
subtitle: 'Polizeikommissarin • PN-2024-9012',
123+
),
124+
],
125+
),
126+
),
127+
lines: [
128+
CodeTextLine("ContactList(items: ["),
129+
CodeTextLine(" ContactListModel("),
130+
CodeTextLine(" avatar: Avatar("),
131+
CodeTextLine(
132+
" initials: Avatar.getInitials('Maria Schmidt'),",
133+
),
134+
CodeTextLine(" ),"),
135+
CodeTextLine(" title: 'Maria Schmidt',"),
136+
CodeTextLine(
137+
" subtitle: 'Polizeioberkommissarin • PN-2024-1234',",
138+
),
139+
CodeTextLine(" ),"),
140+
CodeTextLine(" ContactListModel("),
141+
CodeTextLine(" avatar: Avatar("),
142+
CodeTextLine(
143+
" initials: Avatar.getInitials('Thomas Müller'),",
144+
),
145+
CodeTextLine(" ),"),
146+
CodeTextLine(" title: 'Thomas Müller',"),
147+
CodeTextLine(
148+
" subtitle: 'Polizeihauptkommissar • PN-2024-5678',",
149+
),
150+
CodeTextLine(" ),"),
151+
CodeTextLine(" ContactListModel("),
152+
CodeTextLine(" avatar: Avatar("),
153+
CodeTextLine(" initials: Avatar.getInitials('Lisa Weber'),"),
154+
CodeTextLine(" ),"),
155+
CodeTextLine(" title: 'Lisa Weber',"),
156+
CodeTextLine(
157+
" subtitle: 'Polizeikommissarin • PN-2024-9012',",
158+
),
159+
CodeTextLine(" ),"),
160+
CodeTextLine("]),"),
161+
],
162+
),
163+
],
164+
).gap(15).withPadding(vertical: 30),
165+
).withPadding(horizontal: 20);
166+
}
167+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import 'package:shadcn_flutter/shadcn_flutter.dart';
2+
3+
import 'models/contact_list_model.dart';
4+
5+
/// Renders a simple vertical list of "contact-like" items using `shadcn_flutter`'s [Basic].
6+
///
7+
/// Each entry uses:
8+
/// - `leading`: [ContactListModel.avatar]
9+
/// - `title`: [ContactListModel.title]
10+
/// - `subtitle`: [ContactListModel.subtitle]
11+
///
12+
/// Layout:
13+
/// - Implemented as a [Column] with `crossAxisAlignment: CrossAxisAlignment.start`.
14+
/// - Adds vertical spacing between rows via `.gap(10)`.
15+
///
16+
/// When to use:
17+
/// - Small lists that fit on screen (settings-like pages, small directories).
18+
///
19+
/// When *not* to use:
20+
/// - Large/scrolling lists: prefer `ListView.builder` (better performance and scrolling).
21+
///
22+
/// Interactions:
23+
/// - This widget is display-only. If you need taps, context menus, trailing actions, etc.,
24+
/// wrap/extend the [Basic] widget in the `.map(...)` section.
25+
class ContactList extends StatelessWidget {
26+
const ContactList({super.key, required this.items});
27+
28+
/// Items to be rendered as rows.
29+
final List<ContactListModel> items;
30+
31+
@override
32+
Widget build(BuildContext context) {
33+
return Column(
34+
crossAxisAlignment: CrossAxisAlignment.start,
35+
children: items
36+
.map(
37+
(item) => (Basic(
38+
leading: item.avatar,
39+
title: Text(item.title),
40+
subtitle: Text(item.subtitle),
41+
)),
42+
)
43+
.toList(),
44+
).gap(10);
45+
}
46+
}

0 commit comments

Comments
 (0)