Skip to content

Commit 784ee82

Browse files
committed
Style toasts, confirm dialogs and dropdown menus
- Toasts take the action's button color, an optional close (x) button, corner radius, border, shadows (e.g. a hard stamped shadow), padding, max width, message and button fonts, icon size and a bottom offset per screen size. - The built-in confirm dialog takes a border and shadows in place of its elevation, title/message/button fonts, and outlined buttons with their own radius, colors and shadows (a filled cancel button included). - FeedbackTheme gains border, shadows, buttonBorder and buttonShadows; fromNodes reads the outline and shadow most tappable shapes share. - A dropdown with a menuStyle opens a card anchored under the field: color, border, radius, shadows, offset, row height and padding, a highlighted, bolder selected row with a check mark. Rows (and the field) can show a per-item image from an itemImage template like ${item.image}. - A data-source dropdown finds its selected item by content. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3B1XCZZ7QGvZLUGgSG5H4
1 parent e6a4f61 commit 784ee82

9 files changed

Lines changed: 1397 additions & 174 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
## Unreleased
22

3+
- Toasts, the built-in confirm dialog and dropdown menus take the new
4+
`ShowToastAction`, `ConfirmAction` and `DropdownProperties.menuStyle`
5+
styling: toast action color, close button, radius, border, hard shadows,
6+
fonts, icon size and per-screen-size bottom offsets; confirm dialog border,
7+
shadows, fonts and outlined, shadowed buttons; and a dropdown menu drawn as
8+
a card anchored under the field with a border, shadows, a highlighted
9+
selected row with a check mark and per-item images (`itemImage`).
10+
- `FeedbackTheme` has `border`, `shadows`, `buttonBorder` and
11+
`buttonShadows`; `FeedbackTheme.fromNodes` reads the outline and shadow
12+
most of a layout's tappable shapes share for the confirm dialog's buttons.
13+
- A data-source dropdown finds its selected item by content, so a map value
14+
selects the equal item.
315
- Render `LineChartNode` with `PassiveLineChartTransformer` and
416
`PassiveLineChartWidget`: bands, grid, gradient area, area bands, a
517
polyline or Catmull-Rom curve, reference lines, x axis, markers, vertical

‎lib/src/functions/app_confirm_dialog.dart‎

Lines changed: 197 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ typedef ConfirmOutcome = ({bool confirmed, Map<String, dynamic>? values});
1212
///
1313
/// Without a [ConfirmAction.dialogId] the dialog is built here and takes the
1414
/// look of the layout that shows it ([FeedbackTheme.of]): its font, surface,
15-
/// text and primary colors and its buttons' corner radius. The action's own
16-
/// colors and radius override that look.
15+
/// text and primary colors and its buttons' corner radius, outline and
16+
/// shadows. The action's own colors, radii, borders, shadows and text styles
17+
/// override that look.
1718
///
1819
/// With a [ConfirmAction.dialogId] the dialog is that canvas. A button in it
1920
/// answers with a "Navigate back" action whose params hold
@@ -95,6 +96,21 @@ class AppConfirmDialog {
9596
textColor: action.textColor?.toFlutterColor(),
9697
confirmColor: action.confirmColor?.toFlutterColor(),
9798
cornerRadius: action.cornerRadius,
99+
border: action.border == null ? null : borderSideOf(action.border!),
100+
shadows: action.shadows == null ? null : boxShadowsOf(action.shadows),
101+
titleStyle: action.titleStyle,
102+
messageStyle: action.messageStyle,
103+
buttonStyle: action.buttonStyle,
104+
buttonRadius: action.buttonRadius,
105+
buttonBorder: action.buttonBorder == null
106+
? null
107+
: borderSideOf(action.buttonBorder!),
108+
buttonShadows: action.buttonShadows == null
109+
? null
110+
: boxShadowsOf(action.buttonShadows),
111+
confirmTextColor: action.confirmTextColor?.toFlutterColor(),
112+
cancelColor: action.cancelColor?.toFlutterColor(),
113+
cancelTextColor: action.cancelTextColor?.toFlutterColor(),
98114
),
99115
);
100116
return (confirmed: confirmed ?? false, values: null);
@@ -117,6 +133,17 @@ class ThemedConfirmDialog extends StatelessWidget {
117133
this.textColor,
118134
this.confirmColor,
119135
this.cornerRadius,
136+
this.border,
137+
this.shadows,
138+
this.titleStyle,
139+
this.messageStyle,
140+
this.buttonStyle,
141+
this.buttonRadius,
142+
this.buttonBorder,
143+
this.buttonShadows,
144+
this.confirmTextColor,
145+
this.cancelColor,
146+
this.cancelTextColor,
120147
});
121148

122149
/// The app's look.
@@ -149,6 +176,40 @@ class ThemedConfirmDialog extends StatelessWidget {
149176
/// Overrides the dialog's corner radius.
150177
final double? cornerRadius;
151178

179+
/// Overrides the theme's dialog outline.
180+
final BorderSide? border;
181+
182+
/// Overrides the theme's dialog shadows; set, they replace the elevation.
183+
final List<BoxShadow>? shadows;
184+
185+
/// Changes to the title's font, size and color.
186+
final TextStyleModel? titleStyle;
187+
188+
/// Changes to the message's font, size and color.
189+
final TextStyleModel? messageStyle;
190+
191+
/// Changes to the buttons' font, size and label color.
192+
final TextStyleModel? buttonStyle;
193+
194+
/// Overrides the theme's button corner radius.
195+
final double? buttonRadius;
196+
197+
/// Overrides the theme's button outline.
198+
final BorderSide? buttonBorder;
199+
200+
/// Overrides the theme's button shadows. The cancel button takes them only
201+
/// when it has a [cancelColor].
202+
final List<BoxShadow>? buttonShadows;
203+
204+
/// Label color of the confirm button.
205+
final Color? confirmTextColor;
206+
207+
/// Fill of the cancel button; null leaves it a text button.
208+
final Color? cancelColor;
209+
210+
/// Label color of the cancel button.
211+
final Color? cancelTextColor;
212+
152213
@override
153214
Widget build(BuildContext context) {
154215
final Color surface = backgroundColor ?? theme.surface;
@@ -159,105 +220,157 @@ class ThemedConfirmDialog extends StatelessWidget {
159220
: ResolvedFeedbackTheme.onColor(surface));
160221
final Color confirmFill =
161222
confirmColor ?? (destructive ? theme.error : theme.primary);
162-
final Color onConfirm = ResolvedFeedbackTheme.onColor(confirmFill);
163223
final double radius = cornerRadius ?? theme.radius;
224+
final double buttonCorner = buttonRadius ?? theme.buttonRadius;
225+
final BorderSide buttonSide =
226+
buttonBorder ?? theme.buttonBorder ?? BorderSide.none;
227+
final List<BoxShadow> buttonShadowList =
228+
buttonShadows ?? theme.buttonShadows ?? const [];
164229
final OutlinedBorder buttonShape = RoundedRectangleBorder(
165-
borderRadius: BorderRadius.circular(theme.buttonRadius),
230+
borderRadius: BorderRadius.circular(buttonCorner),
166231
);
167-
final TextStyle buttonText = theme.buttonStyle.copyWith(
168-
fontSize: 15,
169-
height: 1.2,
232+
final TextStyle buttonText = buttonStyle.applyTo(
233+
theme.buttonStyle.copyWith(fontSize: 15, height: 1.2),
170234
);
235+
final Color onConfirm =
236+
buttonStyle?.color?.toFlutterColor() ??
237+
confirmTextColor ??
238+
ResolvedFeedbackTheme.onColor(confirmFill);
239+
final Color onCancel =
240+
buttonStyle?.color?.toFlutterColor() ?? cancelTextColor ?? onSurface;
171241
const Size buttonSize = Size(64, 44);
172242
const EdgeInsets buttonPadding = EdgeInsets.symmetric(horizontal: 18);
243+
final BorderSide? dialogSide = border ?? theme.border;
244+
final List<BoxShadow>? dialogShadows = shadows ?? theme.shadows;
173245

174-
return Theme(
175-
data: theme.apply(Theme.of(context)),
176-
child: Dialog(
177-
backgroundColor: surface,
178-
surfaceTintColor: Colors.transparent,
179-
elevation: 8,
180-
insetPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24),
181-
shape: RoundedRectangleBorder(
182-
borderRadius: BorderRadius.circular(radius),
183-
),
184-
child: ConstrainedBox(
185-
constraints: const BoxConstraints(maxWidth: 400),
186-
child: Semantics(
187-
scopesRoute: true,
188-
namesRoute: true,
189-
explicitChildNodes: true,
190-
label: title.isEmpty ? null : title,
191-
child: Padding(
192-
padding: const EdgeInsets.fromLTRB(24, 24, 24, 16),
193-
child: Column(
194-
mainAxisSize: MainAxisSize.min,
195-
crossAxisAlignment: CrossAxisAlignment.start,
196-
children: [
197-
if (title.isNotEmpty)
198-
Text(
199-
title,
200-
style: theme.titleStyle.copyWith(
201-
fontSize: 18,
202-
height: 1.3,
203-
color: onSurface,
204-
),
246+
Widget withShadow(Widget button) => buttonShadowList.isEmpty
247+
? button
248+
: DecoratedBox(
249+
decoration: BoxDecoration(
250+
borderRadius: BorderRadius.circular(buttonCorner),
251+
boxShadow: buttonShadowList,
252+
),
253+
child: button,
254+
);
255+
256+
final Widget cancel = TextButton(
257+
key: const ValueKey('codelessly-confirm-cancel'),
258+
onPressed: () => Navigator.of(context).pop(false),
259+
style: TextButton.styleFrom(
260+
foregroundColor: onCancel,
261+
backgroundColor: cancelColor,
262+
textStyle: buttonText,
263+
shape: buttonShape,
264+
side: buttonSide,
265+
minimumSize: buttonSize,
266+
padding: buttonPadding,
267+
),
268+
child: Text(cancelLabel),
269+
);
270+
final Widget confirm = FilledButton(
271+
key: const ValueKey('codelessly-confirm-ok'),
272+
onPressed: () => Navigator.of(context).pop(true),
273+
style: FilledButton.styleFrom(
274+
backgroundColor: confirmFill,
275+
foregroundColor: onConfirm,
276+
textStyle: buttonText,
277+
shape: buttonShape,
278+
side: buttonSide,
279+
minimumSize: buttonSize,
280+
padding: buttonPadding,
281+
),
282+
child: Text(confirmLabel),
283+
);
284+
285+
Widget content = ConstrainedBox(
286+
constraints: const BoxConstraints(maxWidth: 400),
287+
child: Semantics(
288+
scopesRoute: true,
289+
namesRoute: true,
290+
explicitChildNodes: true,
291+
label: title.isEmpty ? null : title,
292+
child: Padding(
293+
padding: const EdgeInsets.fromLTRB(24, 24, 24, 16),
294+
child: Column(
295+
mainAxisSize: MainAxisSize.min,
296+
crossAxisAlignment: CrossAxisAlignment.start,
297+
children: [
298+
if (title.isNotEmpty)
299+
Text(
300+
title,
301+
style: titleStyle.applyTo(
302+
theme.titleStyle.copyWith(
303+
fontSize: 18,
304+
height: 1.3,
305+
color: onSurface,
205306
),
206-
if (title.isNotEmpty && message.isNotEmpty)
207-
const SizedBox(height: 8),
208-
if (message.isNotEmpty)
209-
Flexible(
210-
child: SingleChildScrollView(
211-
child: Text(
212-
message,
213-
style: theme.bodyStyle.copyWith(
214-
fontSize: 15,
215-
height: 1.45,
216-
color: onSurface.withValues(alpha: 0.74),
217-
),
307+
),
308+
),
309+
if (title.isNotEmpty && message.isNotEmpty)
310+
const SizedBox(height: 8),
311+
if (message.isNotEmpty)
312+
Flexible(
313+
child: SingleChildScrollView(
314+
child: Text(
315+
message,
316+
style: messageStyle.applyTo(
317+
theme.bodyStyle.copyWith(
318+
fontSize: 15,
319+
height: 1.45,
320+
color: onSurface.withValues(alpha: 0.74),
218321
),
219322
),
220323
),
221-
const SizedBox(height: 20),
222-
OverflowBar(
223-
alignment: MainAxisAlignment.end,
224-
overflowAlignment: OverflowBarAlignment.end,
225-
spacing: 8,
226-
overflowSpacing: 8,
227-
children: [
228-
TextButton(
229-
key: const ValueKey('codelessly-confirm-cancel'),
230-
onPressed: () => Navigator.of(context).pop(false),
231-
style: TextButton.styleFrom(
232-
foregroundColor: onSurface,
233-
textStyle: buttonText,
234-
shape: buttonShape,
235-
minimumSize: buttonSize,
236-
padding: buttonPadding,
237-
),
238-
child: Text(cancelLabel),
239-
),
240-
FilledButton(
241-
key: const ValueKey('codelessly-confirm-ok'),
242-
onPressed: () => Navigator.of(context).pop(true),
243-
style: FilledButton.styleFrom(
244-
backgroundColor: confirmFill,
245-
foregroundColor: onConfirm,
246-
textStyle: buttonText,
247-
shape: buttonShape,
248-
minimumSize: buttonSize,
249-
padding: buttonPadding,
250-
),
251-
child: Text(confirmLabel),
252-
),
253-
],
254324
),
325+
),
326+
const SizedBox(height: 20),
327+
OverflowBar(
328+
alignment: MainAxisAlignment.end,
329+
overflowAlignment: OverflowBarAlignment.end,
330+
// Outlined or shadowed buttons need room between them.
331+
spacing: buttonShadowList.isEmpty ? 8 : 12,
332+
overflowSpacing: 8,
333+
children: [
334+
cancelColor != null ? withShadow(cancel) : cancel,
335+
withShadow(confirm),
255336
],
256337
),
257-
),
338+
],
258339
),
259340
),
260341
),
261342
);
343+
// Shadows of its own replace the dialog's elevation: they are drawn
344+
// around a box that carries its fill and outline.
345+
if (dialogShadows != null) {
346+
content = DecoratedBox(
347+
key: const ValueKey('codelessly-confirm-surface'),
348+
decoration: BoxDecoration(
349+
color: surface,
350+
borderRadius: BorderRadius.circular(radius),
351+
border: dialogSide == null ? null : Border.fromBorderSide(dialogSide),
352+
boxShadow: dialogShadows,
353+
),
354+
child: content,
355+
);
356+
}
357+
358+
return Theme(
359+
data: theme.apply(Theme.of(context)),
360+
child: Dialog(
361+
backgroundColor: dialogShadows != null ? Colors.transparent : surface,
362+
surfaceTintColor: Colors.transparent,
363+
shadowColor: dialogShadows != null ? Colors.transparent : null,
364+
elevation: dialogShadows != null ? 0 : 8,
365+
insetPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24),
366+
shape: RoundedRectangleBorder(
367+
borderRadius: BorderRadius.circular(radius),
368+
side: dialogShadows == null
369+
? dialogSide ?? BorderSide.none
370+
: BorderSide.none,
371+
),
372+
child: content,
373+
),
374+
);
262375
}
263376
}

0 commit comments

Comments
 (0)