Noo design system rebuild #1

Merged
ayushya merged 31 commits from noo-design-system-rebuild into main 2026-10-01 09:23:58 -04:00
5 changed files with 310 additions and 81 deletions
Showing only changes of commit c0f702b00e - Show all commits
+10 -6
View File
@@ -340,12 +340,16 @@ Sidebar items are 38px tall with radius 12, an 18px icon and a 14/500 label. The
Location, Modified, Created, ...). Versions and Activity reuse their own Location, Modified, Created, ...). Versions and Activity reuse their own
row/feed treatment. Same on every platform - this sheet has no iOS/Android row/feed treatment. Same on every platform - this sheet has no iOS/Android
split. split.
- **Media viewer** (the full-screen photo/video viewer): a black stage - **Media viewer** (the full-screen photo/video viewer): the stage is `bg`,
regardless of theme, like a native photo/video viewer - not `bg`. A following the app's own theme rather than a fixed black - a black stage
translucent, blurred top bar (back, filename, meta) and bottom bar float in light mode read as jarringly out of place. A translucent, blurred top
over the media; this is the one deliberate exception to "no blur" in bar (back, filename, meta) and bottom bar float over the media in a fixed
product UI, since it's chrome over photo/video content, not over the app's dark panel regardless of theme (it needs to read over arbitrary photo/
own surfaces. Back is a plain arrow (`arrow-left`), not the iOS video brightness, not over `bg`); this is the one deliberate exception to
"no blur" in product UI, since it's chrome over photo/video content, not
over the app's own surfaces. Both bars' background extends edge-to-edge
behind the status bar/gesture area, with only their content padded clear
of it. Back is a plain arrow (`arrow-left`), not the iOS
chevron+label pushed-screen pattern - platform split still to do. The chevron+label pushed-screen pattern - platform split still to do. The
bottom bar holds every action in one row (share, favorite, open bottom bar holds every action in one row (share, favorite, open
externally, download, delete, details) on every platform; don't add a externally, download, delete, details) on every platform; don't add a
+17 -15
View File
@@ -376,14 +376,13 @@ class _FileViewerScreenState extends State<FileViewerScreen> {
final session = context.read<SessionController>(); final session = context.read<SessionController>();
final ops = context.read<ItemOperations>(); final ops = context.read<ItemOperations>();
final colors = context.nooColors; final colors = context.nooColors;
// The stage behind the media itself is pure black rather than `bg` - // The stage follows the app's own theme (`bg`) rather than being pinned
// the near-universal "letterbox" convention of photo/video viewers // to black - a black stage in light mode read as jarringly out of place
// (matting an image/video in the app's warm neutral background reads // next to the rest of the light-themed app. The floating top/action
// as unfinished, and black also hides any letterboxing from // bars stay a fixed dark frosted panel regardless (see
// `BoxFit.contain`/`AspectRatio` at the screen edges). PDFs/text/ // FrostedGlassContainer's doc comment) since they need to read over
// unsupported-file previews aren't "viewed" the same way - they're // arbitrary photo/video brightness, not over this background.
// read, so they stay on the normal `bg` like any other screen. final stageColor = colors.bg;
final stageColor = _isSwipeable ? Colors.black : colors.bg;
return Scaffold( return Scaffold(
backgroundColor: stageColor, backgroundColor: stageColor,
@@ -437,9 +436,13 @@ class _FileViewerScreenState extends State<FileViewerScreen> {
offset: _controlsVisible ? Offset.zero : const Offset(0, -1.4), offset: _controlsVisible ? Offset.zero : const Offset(0, -1.4),
child: IgnorePointer( child: IgnorePointer(
ignoring: !_controlsVisible, ignoring: !_controlsVisible,
child: SafeArea( // The blurred/tinted background lives outside the SafeArea
bottom: false, // (not inside it) so it extends up behind the status bar
child: FrostedGlassContainer( // instead of stopping short beneath it - see the matching
// fix/comment on MediaActionBar's own SafeArea.
child: FrostedGlassContainer(
child: SafeArea(
bottom: false,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 6, horizontal: 6,
@@ -447,10 +450,9 @@ class _FileViewerScreenState extends State<FileViewerScreen> {
), ),
child: IconTheme.merge( child: IconTheme.merge(
// Fixed white, not `colors.fg1`: this bar always // Fixed white, not `colors.fg1`: this bar always
// sits on the dark translucent panel over a black // sits on the dark translucent panel over the media
// media stage (see `FrostedGlassContainer`'s doc // stage (see `FrostedGlassContainer`'s doc comment),
// comment), regardless of the app's light/dark // regardless of the app's light/dark theme.
// theme.
data: const IconThemeData( data: const IconThemeData(
color: Colors.white, color: Colors.white,
size: 24, size: 24,
+118 -28
View File
@@ -1,13 +1,23 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../theme/design_tokens.dart'; import '../../../theme/design_tokens.dart';
/// Fraction of the available height a sheet can reach before it switches
/// from sizing to its content to a draggable peek (see [_NooSheetBody]).
const double _kNooSheetPeekFraction = 0.6;
const double _kNooSheetMinFraction = 0.3;
const double _kNooSheetMaxFraction = 0.95;
/// Mobile bottom sheet on a scrim: radius-28 top, grabber, 22px gap between /// Mobile bottom sheet on a scrim: radius-28 top, grabber, 22px gap between
/// sections (Noo Design System project, `components/overlays/Sheet.jsx`). /// sections (Noo Design System project, `components/overlays/Sheet.jsx`).
/// `showModalBottomSheet` already supplies the scrim/backdrop-dismiss /// `showModalBottomSheet` already supplies the scrim/backdrop-dismiss
/// machinery, so this only standardizes the shape/padding - use it instead /// machinery, so this only standardizes the shape/padding - use it instead
/// of a bare `showModalBottomSheet` for any new sheet. Scroll-controlled so a /// of a bare `showModalBottomSheet` for any new sheet. Sizes to its content
/// tall sheet (e.g. share) can grow past half the screen; content scrolls /// like a plain scroll view for anything that fits comfortably on screen
/// once it hits the top. /// (most sheets: a menu, a handful of settings rows); content long enough to
/// need scrolling (Details' Versions/Activity tabs with many rows, a long
/// accounts list, ...) instead opens at a native-style peek height the user
/// can drag up to reveal more of, rather than snapping straight to nearly
/// the full screen with nothing left to adjust - see [_NooSheetBody].
Future<T?> showNooSheet<T>( Future<T?> showNooSheet<T>(
BuildContext context, { BuildContext context, {
required List<Widget> children, required List<Widget> children,
@@ -20,31 +30,111 @@ Future<T?> showNooSheet<T>(
isScrollControlled: true, isScrollControlled: true,
useSafeArea: true, useSafeArea: true,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(NooRadii.sheetTop)), borderRadius: BorderRadius.vertical(
top: Radius.circular(NooRadii.sheetTop),
),
), ),
builder: (context) { builder: (context) => _NooSheetBody(children: children),
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 10, 20, 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 36,
height: 5,
decoration: BoxDecoration(
color: colors.surface3,
borderRadius: BorderRadius.circular(NooRadii.pill),
),
),
for (final child in children) ...[
const SizedBox(height: 22),
child,
],
],
),
),
);
},
); );
} }
/// Renders [children] once, unconstrained, to measure their natural height
/// against the sheet's available height. Content that fits within
/// [_kNooSheetPeekFraction] just stays as that first (plain, content-sized)
/// render - the common case for a short menu/option list. Content taller
/// than that switches to a [DraggableScrollableSheet] opened at the peek
/// fraction instead, so it never jumps straight to (near) full height with
/// no room left to adjust. The one extra layout pass for tall content
/// happens within the sheet's own entrance animation, so it isn't visible
/// as a jump.
class _NooSheetBody extends StatefulWidget {
final List<Widget> children;
const _NooSheetBody({required this.children});
@override
State<_NooSheetBody> createState() => _NooSheetBodyState();
}
class _NooSheetBodyState extends State<_NooSheetBody> {
final _contentKey = GlobalKey();
bool _measured = false;
bool _needsPeek = false;
void _measureAfterFrame(double availableHeight) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || _measured) return;
final box = _contentKey.currentContext?.findRenderObject() as RenderBox?;
final contentHeight = box?.size.height ?? 0;
setState(() {
_measured = true;
_needsPeek = contentHeight > availableHeight * _kNooSheetPeekFraction;
});
});
}
Widget _buildContent(ScrollController? scrollController) {
final colors = context.nooColors;
return SingleChildScrollView(
controller: scrollController,
padding: const EdgeInsets.fromLTRB(20, 10, 20, 20),
child: Column(
key: _measured ? null : _contentKey,
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 36,
height: 5,
decoration: BoxDecoration(
color: colors.surface3,
borderRadius: BorderRadius.circular(NooRadii.pill),
),
),
for (final child in widget.children) ...[
const SizedBox(height: 22),
child,
],
],
),
);
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
if (!_measured) {
_measureAfterFrame(constraints.maxHeight);
return _buildContent(null);
}
if (!_needsPeek) {
return _buildContent(null);
}
return DraggableScrollableSheet(
initialChildSize: _kNooSheetPeekFraction,
minChildSize: _kNooSheetMinFraction,
maxChildSize: _kNooSheetMaxFraction,
expand: false,
builder: (context, scrollController) {
// Plain `DraggableScrollableSheet` doesn't dismiss the modal
// route on its own when dragged down to its floor - it just
// stops resizing there - so pop explicitly once it's been
// dragged (near) all the way down, matching a native
// peek sheet's swipe-to-dismiss.
return NotificationListener<DraggableScrollableNotification>(
onNotification: (notification) {
if (notification.extent <= notification.minExtent + 0.01) {
Navigator.of(context).maybePop();
}
return false;
},
child: _buildContent(scrollController),
);
},
);
},
),
);
}
}
+8 -3
View File
@@ -46,9 +46,14 @@ class MediaActionBar extends StatelessWidget {
// favorite/delete stay legible even when the ambient theme is light. // favorite/delete stay legible even when the ambient theme is light.
const dark = NooColors.dark; const dark = NooColors.dark;
return SafeArea( // The blurred/tinted background lives outside the SafeArea (not inside
top: false, // it) so it extends all the way to the physical bottom edge, behind the
child: FrostedGlassContainer( // gesture bar/home indicator, instead of the panel itself stopping
// short and leaving that strip unstyled - only the actual row content
// needs padding up and away from the gesture area.
return FrostedGlassContainer(
child: SafeArea(
top: false,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
+157 -29
View File
@@ -14,18 +14,26 @@ import 'noo_test_utils.dart';
/// A button that opens an overlay from a context below MaterialApp. /// A button that opens an overlay from a context below MaterialApp.
Widget _launcher(void Function(BuildContext) open) => Builder( Widget _launcher(void Function(BuildContext) open) => Builder(
builder: (context) => TextButton(onPressed: () => open(context), child: const Text('open')), builder: (context) =>
TextButton(onPressed: () => open(context), child: const Text('open')),
); );
Finder _dialogCard() => find.byWidgetPredicate( Finder _dialogCard() => find.byWidgetPredicate(
(w) => w is Container && w.decoration is BoxDecoration && (w.decoration as BoxDecoration).boxShadow != null, (w) =>
w is Container &&
w.decoration is BoxDecoration &&
(w.decoration as BoxDecoration).boxShadow != null,
); );
void main() { void main() {
setUpNooTests(); setUpNooTests();
group('NooOverlayHeader', () { group('NooOverlayHeader', () {
testNooWidgets('title, subtitle, leading and close', (tester, theme, c) async { testNooWidgets('title, subtitle, leading and close', (
tester,
theme,
c,
) async {
var closed = 0; var closed = 0;
await pumpNoo( await pumpNoo(
tester, tester,
@@ -41,23 +49,41 @@ void main() {
theme: theme, theme: theme,
); );
expect(find.text('Report.pdf'), findsOneWidget); expect(find.text('Report.pdf'), findsOneWidget);
expect(tester.widget<Text>(find.text('2 MB · Documents')).style!.color, c.fg3); expect(
expect(decorationOf(tester, find.byIcon(LucideIcons.x)).color, c.surface2); tester.widget<Text>(find.text('2 MB · Documents')).style!.color,
c.fg3,
);
expect(
decorationOf(tester, find.byIcon(LucideIcons.x)).color,
c.surface2,
);
await tester.tap(find.byIcon(LucideIcons.x)); await tester.tap(find.byIcon(LucideIcons.x));
expect(closed, 1); expect(closed, 1);
}); });
testWidgets('no close button without onClose', (tester) async { testWidgets('no close button without onClose', (tester) async {
await pumpNoo(tester, const SizedBox(width: 360, child: NooOverlayHeader(title: 'T'))); await pumpNoo(
tester,
const SizedBox(width: 360, child: NooOverlayHeader(title: 'T')),
);
expect(find.byIcon(LucideIcons.x), findsNothing); expect(find.byIcon(LucideIcons.x), findsNothing);
}); });
}); });
group('showNooSheet', () { group('showNooSheet', () {
testNooWidgets('shows children on a surface sheet and dismisses', (tester, theme, c) async { testNooWidgets('shows children on a surface sheet and dismisses', (
tester,
theme,
c,
) async {
await pumpNoo( await pumpNoo(
tester, tester,
_launcher((ctx) => showNooSheet<void>(ctx, children: const [Text('Section A'), Text('Section B')])), _launcher(
(ctx) => showNooSheet<void>(
ctx,
children: const [Text('Section A'), Text('Section B')],
),
),
theme: theme, theme: theme,
); );
await tester.tap(find.text('open')); await tester.tap(find.text('open'));
@@ -66,7 +92,8 @@ void main() {
expect(find.text('Section B'), findsOneWidget); expect(find.text('Section B'), findsOneWidget);
final sheet = tester.widget<BottomSheet>(find.byType(BottomSheet)); final sheet = tester.widget<BottomSheet>(find.byType(BottomSheet));
expect(sheet.backgroundColor, c.surface); expect(sheet.backgroundColor, c.surface);
final gap = tester.getTopLeft(find.text('Section B')).dy - final gap =
tester.getTopLeft(find.text('Section B')).dy -
tester.getBottomLeft(find.text('Section A')).dy; tester.getBottomLeft(find.text('Section A')).dy;
expect(gap, 22); expect(gap, 22);
@@ -74,13 +101,56 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('Section A'), findsNothing); expect(find.text('Section A'), findsNothing);
}); });
testNooWidgets('long content opens as a draggable peek, not full height', (
tester,
theme,
c,
) async {
await pumpNoo(
tester,
_launcher(
(ctx) => showNooSheet<void>(
ctx,
children: [
for (var i = 0; i < 30; i++)
SizedBox(height: 40, child: Text('Row $i')),
],
),
),
theme: theme,
surfaceSize: const Size(400, 700),
);
await tester.tap(find.text('open'));
await tester.pumpAndSettle();
// Content (30 * (40 + 22) =~ 1860px) far exceeds the peek fraction of
// the 700px-tall surface, so it should switch to a draggable sheet
// instead of the plain content-sized scroll view, opened at a peek
// (well under half the content height) rather than snapping to
// (near) the full 700px available.
expect(find.text('Row 0'), findsOneWidget);
final sheetHeight = tester
.getSize(find.byType(DraggableScrollableSheet))
.height;
expect(sheetHeight, lessThan(500));
expect(sheetHeight, greaterThan(150));
});
}); });
group('NooDialog', () { group('NooDialog', () {
testNooWidgets('card is 540 wide, r24, single dialog shadow', (tester, theme, c) async { testNooWidgets('card is 540 wide, r24, single dialog shadow', (
tester,
theme,
c,
) async {
await pumpNoo( await pumpNoo(
tester, tester,
NooDialog(title: 'Share "Report.pdf"', onClose: () {}, children: const [Text('body')]), NooDialog(
title: 'Share "Report.pdf"',
onClose: () {},
children: const [Text('body')],
),
theme: theme, theme: theme,
surfaceSize: const Size(1200, 900), surfaceSize: const Size(1200, 900),
); );
@@ -92,7 +162,9 @@ void main() {
expect(tester.getSize(card).width, 540); expect(tester.getSize(card).width, 540);
}); });
testWidgets('narrow screens shrink the card to fit with a 24px margin', (tester) async { testWidgets('narrow screens shrink the card to fit with a 24px margin', (
tester,
) async {
await tester.binding.setSurfaceSize(const Size(400, 800)); await tester.binding.setSurfaceSize(const Size(400, 800));
addTearDown(() => tester.binding.setSurfaceSize(null)); addTearDown(() => tester.binding.setSurfaceSize(null));
await tester.pumpWidget( await tester.pumpWidget(
@@ -105,7 +177,11 @@ void main() {
expect(tester.getSize(card).width, 400 - 48); expect(tester.getSize(card).width, 400 - 48);
}); });
testNooWidgets('header close is a 32px surface-2 circle', (tester, theme, c) async { testNooWidgets('header close is a 32px surface-2 circle', (
tester,
theme,
c,
) async {
var closed = 0; var closed = 0;
await pumpNoo( await pumpNoo(
tester, tester,
@@ -118,12 +194,19 @@ void main() {
theme: theme, theme: theme,
); );
expect(tester.getSize(find.byType(NooCloseButton)), const Size(32, 32)); expect(tester.getSize(find.byType(NooCloseButton)), const Size(32, 32));
expect(decorationOf(tester, find.byIcon(LucideIcons.x)).color, c.surface2); expect(
decorationOf(tester, find.byIcon(LucideIcons.x)).color,
c.surface2,
);
await tester.tap(find.byType(NooCloseButton)); await tester.tap(find.byType(NooCloseButton));
expect(closed, 1); expect(closed, 1);
}); });
testNooWidgets('showNooDialog uses the scrim and closes via the button', (tester, theme, c) async { testNooWidgets('showNooDialog uses the scrim and closes via the button', (
tester,
theme,
c,
) async {
Future<String?>? result; Future<String?>? result;
await pumpNoo( await pumpNoo(
tester, tester,
@@ -140,7 +223,9 @@ void main() {
await tester.tap(find.text('open')); await tester.tap(find.text('open'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('dialog body'), findsOneWidget); expect(find.text('dialog body'), findsOneWidget);
final barrier = tester.widgetList<ModalBarrier>(find.byType(ModalBarrier)).last; final barrier = tester
.widgetList<ModalBarrier>(find.byType(ModalBarrier))
.last;
expect(barrier.color, c.scrim); expect(barrier.color, c.scrim);
await tester.tap(find.byType(NooCloseButton)); await tester.tap(find.byType(NooCloseButton));
@@ -176,7 +261,10 @@ void main() {
(ctx) => showNooDialog<void>( (ctx) => showNooDialog<void>(
ctx, ctx,
title: 'T', title: 'T',
children: [for (var i = 0; i < 40; i++) SizedBox(height: 60, child: Text('row $i'))], children: [
for (var i = 0; i < 40; i++)
SizedBox(height: 60, child: Text('row $i')),
],
), ),
), ),
surfaceSize: const Size(1200, 600), surfaceSize: const Size(1200, 600),
@@ -189,13 +277,20 @@ void main() {
}); });
group('NooTextField', () { group('NooTextField', () {
testNooWidgets('surface-2, r14, fg-3 placeholder, typing', (tester, theme, c) async { testNooWidgets('surface-2, r14, fg-3 placeholder, typing', (
tester,
theme,
c,
) async {
String? typed; String? typed;
await pumpNoo( await pumpNoo(
tester, tester,
SizedBox( SizedBox(
width: 400, width: 400,
child: NooTextField(placeholder: 'Name, email or group', onChanged: (v) => typed = v), child: NooTextField(
placeholder: 'Name, email or group',
onChanged: (v) => typed = v,
),
), ),
theme: theme, theme: theme,
); );
@@ -209,14 +304,20 @@ void main() {
expect(typed, 'ali'); expect(typed, 'ali');
}); });
testNooWidgets('mono URL field with inline Copy link button', (tester, theme, c) async { testNooWidgets('mono URL field with inline Copy link button', (
tester,
theme,
c,
) async {
var copied = 0; var copied = 0;
await pumpNoo( await pumpNoo(
tester, tester,
SizedBox( SizedBox(
width: 480, width: 480,
child: NooTextField( child: NooTextField(
controller: TextEditingController(text: 'https://cloud.example/s/abc'), controller: TextEditingController(
text: 'https://cloud.example/s/abc',
),
mono: true, mono: true,
readOnly: true, readOnly: true,
onSurface: false, onSurface: false,
@@ -233,7 +334,10 @@ void main() {
expect(field.style!.fontFamily, NooText.mono.fontFamily); expect(field.style!.fontFamily, NooText.mono.fontFamily);
expect(field.readOnly, isTrue); expect(field.readOnly, isTrue);
expect(decorationOf(tester, find.byType(TextField)).color, c.surface); expect(decorationOf(tester, find.byType(TextField)).color, c.surface);
expect(tester.getSize(find.byType(NooButton)).height, NooSizes.buttonField); expect(
tester.getSize(find.byType(NooButton)).height,
NooSizes.buttonField,
);
await tester.tap(find.text('Copy link')); await tester.tap(find.text('Copy link'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(copied, 1); expect(copied, 1);
@@ -241,7 +345,11 @@ void main() {
}); });
group('Share parts', () { group('Share parts', () {
testNooWidgets('NooShareSection title, trailing and caption', (tester, theme, c) async { testNooWidgets('NooShareSection title, trailing and caption', (
tester,
theme,
c,
) async {
await pumpNoo( await pumpNoo(
tester, tester,
const SizedBox( const SizedBox(
@@ -255,13 +363,26 @@ void main() {
), ),
theme: theme, theme: theme,
); );
expect(tester.widget<Text>(find.text('Send file directly')).style!.color, c.fg1); expect(
expect(tester.widget<Text>(find.text("Link settings don't apply")).style!.color, c.fg3); tester.widget<Text>(find.text('Send file directly')).style!.color,
c.fg1,
);
expect(
tester
.widget<Text>(find.text("Link settings don't apply"))
.style!
.color,
c.fg3,
);
expect(find.byIcon(LucideIcons.link), findsOneWidget); expect(find.byIcon(LucideIcons.link), findsOneWidget);
expect(find.text('content'), findsOneWidget); expect(find.text('content'), findsOneWidget);
}); });
testNooWidgets('owner row is static, others get a tappable pill', (tester, theme, c) async { testNooWidgets('owner row is static, others get a tappable pill', (
tester,
theme,
c,
) async {
var taps = 0; var taps = 0;
await pumpNoo( await pumpNoo(
tester, tester,
@@ -296,13 +417,19 @@ void main() {
expect(taps, 1); expect(taps, 1);
}); });
testWidgets('same parts lay out inside the sheet and the dialog', (tester) async { testWidgets('same parts lay out inside the sheet and the dialog', (
tester,
) async {
List<Widget> parts() => [ List<Widget> parts() => [
NooShareSection( NooShareSection(
title: 'Share with people', title: 'Share with people',
child: const NooTextField(placeholder: 'Name, email or group'), child: const NooTextField(placeholder: 'Name, email or group'),
), ),
const NooPersonAccessRow(avatar: NooAvatar(initials: 'JD'), name: 'Jane', permission: 'Can view'), const NooPersonAccessRow(
avatar: NooAvatar(initials: 'JD'),
name: 'Jane',
permission: 'Can view',
),
]; ];
await pumpNoo( await pumpNoo(
tester, tester,
@@ -312,7 +439,8 @@ void main() {
_launcher((ctx) => showNooSheet<void>(ctx, children: parts())), _launcher((ctx) => showNooSheet<void>(ctx, children: parts())),
Builder( Builder(
builder: (ctx) => TextButton( builder: (ctx) => TextButton(
onPressed: () => showNooDialog<void>(ctx, title: 'Share', children: parts()), onPressed: () =>
showNooDialog<void>(ctx, title: 'Share', children: parts()),
child: const Text('dialog'), child: const Text('dialog'),
), ),
), ),