Skip to content

Commit 2f89170

Browse files
authored
fix(fortal): restore dialog and popover layout defaults (#129)
1 parent b013b9f commit 2f89170

10 files changed

Lines changed: 186 additions & 34 deletions

File tree

docs/components/dialog.mdx

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -31,22 +31,20 @@ class DialogExample extends StatelessWidget {
3131
onPressed: () {
3232
showRemixDialog(
3333
context: context,
34-
builder: (context) => Center(
35-
child: FortalDialog(
36-
title: 'Revoke access',
37-
description:
38-
'This application will no longer be accessible.',
39-
actions: [
40-
FortalButton.ghost(
41-
label: 'Cancel',
42-
onPressed: () => Navigator.pop(context),
43-
),
44-
FortalButton(
45-
label: 'Revoke access',
46-
onPressed: () => Navigator.pop(context),
47-
),
48-
],
49-
),
34+
builder: (context) => FortalDialog(
35+
title: 'Revoke access',
36+
description:
37+
'This application will no longer be accessible.',
38+
actions: [
39+
FortalButton.ghost(
40+
label: 'Cancel',
41+
onPressed: () => Navigator.pop(context),
42+
),
43+
FortalButton(
44+
label: 'Revoke access',
45+
onPressed: () => Navigator.pop(context),
46+
),
47+
],
5048
),
5149
);
5250
},
@@ -74,21 +72,19 @@ Future<void> showDeleteDialog(BuildContext context) async {
7472
await showRemixAlertDialog<void>(
7573
context: context,
7674
semanticLabel: 'Delete project confirmation',
77-
builder: (context) => Center(
78-
child: FortalDialog(
79-
title: 'Delete project?',
80-
description: 'This permanently deletes the project and all of its data.',
81-
actions: [
82-
FortalButton.ghost(
83-
label: 'Cancel',
84-
onPressed: () => Navigator.pop(context),
85-
),
86-
FortalButton(
87-
label: 'Delete project',
88-
onPressed: () => Navigator.pop(context),
89-
),
90-
],
91-
),
75+
builder: (context) => FortalDialog(
76+
title: 'Delete project?',
77+
description: 'This permanently deletes the project and all of its data.',
78+
actions: [
79+
FortalButton.ghost(
80+
label: 'Cancel',
81+
onPressed: () => Navigator.pop(context),
82+
),
83+
FortalButton(
84+
label: 'Delete project',
85+
onPressed: () => Navigator.pop(context),
86+
),
87+
],
9288
),
9389
);
9490
}
@@ -99,6 +95,11 @@ Future<void> showDeleteDialog(BuildContext context) async {
9995

10096
Remix includes a Fortal-themed widget for this component:
10197

98+
`FortalDialog` defaults to `FortalDialogSize.size3`,
99+
`FortalDialogAlign.center`, fills the available width up to 600 logical pixels,
100+
preserves safe viewport insets, and uses `modal: true`. Set
101+
`align: FortalDialogAlign.start` to place the surface at the safe top inset.
102+
102103
<CodeGroup title="Fortal widget" defaultLanguage="dart">
103104
```dart
104105
import 'package:flutter/material.dart';
@@ -110,6 +111,7 @@ class FortalDialogExample extends StatelessWidget {
110111
@override
111112
Widget build(BuildContext context) {
112113
return FortalDialog(
114+
align: FortalDialogAlign.center,
113115
title: 'Confirm changes',
114116
description: 'Save these settings before leaving?',
115117
actions: [

docs/components/popover.mdx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,10 @@ final styledPopover = RemixPopover(
122122
);
123123
```
124124

125-
The `FortalPopover` preset adds Fortal spacing, border, radius, surface color,
126-
shadow, and a maximum width. Content remains fully composable.
125+
The `FortalPopover` preset defaults to `FortalPopoverSize.size2`, adds Fortal
126+
spacing, border, radius, surface color, and shadow, constrains the surface to a
127+
maximum width of 480 logical pixels, and renders no arrow. Content remains
128+
fully composable.
127129

128130
## Keyboard and accessibility
129131

packages/remix_fortal/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
## 0.1.0-beta.1
22

3+
- **FIX**: Match the pinned Radix Themes dialog and popover layout defaults.
4+
`FortalDialog` now defaults to centered placement, exposes `start` and
5+
`center` alignment options, fills up to 600 pixels, and preserves safe
6+
viewport insets;
7+
`FortalPopover` has a 480-pixel maximum width.
38
- Initial release. Fortal — the Radix Themes-inspired preset theme for
49
[Remix](https://pub.dev/packages/remix) — now ships as its own package.
510
`FortalScope`, `FortalTokens`, the `Fortal*` widgets, and the `fortal*Style()`

packages/remix_fortal/lib/src/recipes/dialog.dart

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'dart:math' as math;
2+
13
import 'package:flutter/material.dart';
24
import 'package:mix_annotations/mix_annotations.dart';
35
import 'package:remix/remix.dart';
@@ -9,10 +11,32 @@ part 'dialog.g.dart';
911
/// Fortal dialog size presets matching Radix Themes 3.3.0.
1012
enum FortalDialogSize { size1, size2, size3, size4 }
1113

14+
/// Fortal dialog vertical alignment matching Radix Themes 3.3.0.
15+
enum FortalDialogAlign { start, center }
16+
17+
final _dialogViewportInsets = ContextToken<EdgeInsetsGeometry>((context) {
18+
final safeArea = MediaQuery.paddingOf(context);
19+
final viewportHeight = MediaQuery.sizeOf(context).height;
20+
final horizontal = FortalTokens.space4.resolve(context);
21+
final vertical = FortalTokens.space6.resolve(context);
22+
23+
return EdgeInsets.fromLTRB(
24+
math.max(safeArea.left, horizontal),
25+
math.max(safeArea.top, vertical),
26+
math.max(safeArea.right, horizontal),
27+
math.max(safeArea.bottom, math.max(vertical, viewportHeight * 0.06)),
28+
);
29+
});
30+
1231
/// Fortal-themed preset for [RemixDialog].
32+
///
33+
/// The generated [FortalDialog] defaults to [FortalDialogSize.size3],
34+
/// [FortalDialogAlign.center], fills up to 600 logical pixels, preserves safe
35+
/// viewport insets, and is modal.
1336
@MixWidget(target: RemixDialog.new)
1437
DialogStyler fortalDialogStyle({
1538
FortalDialogSize size = FortalDialogSize.size3,
39+
FortalDialogAlign align = FortalDialogAlign.center,
1640
}) {
1741
final radius = switch (size) {
1842
FortalDialogSize.size1 || FortalDialogSize.size2 => FortalTokens.radius4(),
@@ -24,8 +48,20 @@ DialogStyler fortalDialogStyle({
2448
FortalDialogSize.size3 => FortalTokens.space5(),
2549
FortalDialogSize.size4 => FortalTokens.space6(),
2650
};
51+
final alignment = switch (align) {
52+
FortalDialogAlign.start => Alignment.topCenter,
53+
FortalDialogAlign.center => Alignment.center,
54+
};
2755

2856
return DialogStyler()
57+
.wrap(
58+
.modifier(
59+
PaddingModifierMix.create(padding: Prop.token(_dialogViewportInsets)),
60+
).align(alignment: alignment).orderOfModifiers([
61+
PaddingModifier,
62+
AlignModifier,
63+
]),
64+
)
2965
.title(
3066
.style(FortalTokens.text5.mix())
3167
.fontWeight(FortalTokens.fontWeightBold())
@@ -46,6 +82,7 @@ DialogStyler fortalDialogStyle({
4682
.spacing(FortalTokens.space3())
4783
.marginTop(FortalTokens.space5()),
4884
)
85+
.width(600)
4986
.padding(.all(padding))
5087
.borderRadius(.all(radius))
5188
.color(FortalTokens.colorPanel())

packages/remix_fortal/lib/src/recipes/dialog.g.dart

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/remix_fortal/lib/src/recipes/popover.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ part 'popover.g.dart';
1010
enum FortalPopoverSize { size1, size2, size3, size4 }
1111

1212
/// Fortal-themed preset for [RemixPopover].
13+
///
14+
/// The generated [FortalPopover] defaults to [FortalPopoverSize.size2], a
15+
/// 480-pixel maximum width, and no arrow.
1316
@MixWidget(target: RemixPopover.new)
1417
PopoverStyler fortalPopoverStyle({
1518
FortalPopoverSize size = FortalPopoverSize.size2,
@@ -28,6 +31,7 @@ PopoverStyler fortalPopoverStyle({
2831
};
2932

3033
return PopoverStyler()
34+
.maxWidth(480)
3135
.paddingAll(padding)
3236
.borderRadiusAll(radius)
3337
.color(FortalTokens.colorPanel())

packages/remix_fortal/lib/src/recipes/popover.g.dart

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/remix_fortal/reference/radix_themes_3_3_0/coverage_evidence.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,11 @@
269269
}
270270
],
271271
"dialog": [
272+
{
273+
"test": "test/components/dialog/dialog_widget_test.dart",
274+
"case": "public contract has the pinned align order and default",
275+
"covers": ["enum:align.start", "enum:align.center"]
276+
},
272277
{
273278
"test": "test/components/dialog/dialog_widget_test.dart",
274279
"case": "opens and renders alert content",

packages/remix_fortal/test/components/dialog/dialog_widget_test.dart

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,80 @@ import 'package:remix_fortal/remix_fortal.dart';
66

77
import '../../helpers/test_helpers.dart';
88

9+
Future<Rect> _pumpDialogSurface(
10+
WidgetTester tester,
11+
FortalDialog dialog, {
12+
MediaQueryData? mediaQueryData,
13+
}) async {
14+
Widget child = SizedBox.expand(child: dialog);
15+
if (mediaQueryData != null) {
16+
child = MediaQuery(data: mediaQueryData, child: child);
17+
}
18+
await tester.pumpRemixApp(child);
19+
await tester.pumpAndSettle();
20+
21+
final align = find.descendant(
22+
of: find.byType(RemixDialog),
23+
matching: find.byType(Align),
24+
);
25+
final surface = tester.widget<Align>(align).child!;
26+
27+
return tester.getRect(find.byWidget(surface));
28+
}
29+
930
void main() {
31+
testWidgets('default surface fills available width up to 600 pixels', (
32+
tester,
33+
) async {
34+
final rect = await _pumpDialogSurface(
35+
tester,
36+
const FortalDialog(title: 'Short'),
37+
);
38+
39+
expect(rect.width, 600);
40+
});
41+
42+
test('public contract has the pinned align order and default', () {
43+
const dialog = FortalDialog(title: 'Defaults');
44+
45+
expect(FortalDialogAlign.values, const [
46+
FortalDialogAlign.start,
47+
FortalDialogAlign.center,
48+
]);
49+
expect(dialog.align, FortalDialogAlign.center);
50+
});
51+
52+
testWidgets('start alignment respects the safe top inset', (tester) async {
53+
final rect = await _pumpDialogSurface(
54+
tester,
55+
const FortalDialog(
56+
align: FortalDialogAlign.start,
57+
title: 'Start aligned',
58+
),
59+
mediaQueryData: const MediaQueryData(
60+
size: Size(800, 600),
61+
padding: EdgeInsets.fromLTRB(24, 48, 28, 40),
62+
),
63+
);
64+
65+
expect(rect.top, 48);
66+
});
67+
68+
testWidgets('default alignment centers within the safe padded viewport', (
69+
tester,
70+
) async {
71+
final rect = await _pumpDialogSurface(
72+
tester,
73+
const FortalDialog(title: 'Centered safely'),
74+
mediaQueryData: const MediaQueryData(
75+
size: Size(800, 600),
76+
padding: EdgeInsets.fromLTRB(24, 48, 28, 40),
77+
),
78+
);
79+
80+
expect(rect.center.dy, 304);
81+
});
82+
1083
testWidgets('bounded large-text structured content does not overflow', (
1184
tester,
1285
) async {

packages/remix_fortal/test/components/popover/popover_widget_test.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@ import 'package:remix_fortal/remix_fortal.dart';
55
import '../../helpers/test_helpers.dart';
66

77
void main() {
8+
testWidgets('default style constrains the popover to 480 pixels', (
9+
tester,
10+
) async {
11+
final resolved = await resolveInFortalScope(
12+
tester,
13+
(context) => fortalPopoverStyle().build(context),
14+
);
15+
16+
expect(
17+
resolved.spec.container.spec.constraints,
18+
const BoxConstraints(maxWidth: 480),
19+
);
20+
});
21+
822
testWidgets('FortalPopover supplies the themed overlay style', (
923
tester,
1024
) async {

0 commit comments

Comments
 (0)