Noo design system rebuild #1
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user