todo-app/lib/ui/notes_grid.dart
Krzysztof kuhy Rudnicki f7ea0ffb3d Notes list: 2-column tile grid on phones, whole rows fill the screen
Below 600dp the notes list is a compact tile grid (title + priority/status
cues). Tile height is derived so whole rows exactly fill the viewport between
the status and navigation bars; past a screenful it scrolls vertically (no
paging). Body and dates moved into the long-press sheet. Wider windows keep
the list. Not using design_system FitGrid: it is square-celled and pages
horizontally.

Pins: design_system v0.3.0 (newest tag); also bumped crdt_sync_dart 0.11.2,
sync_settings_ui 0.4.2, file_picker 13.3.1, idb_shim 2.11.1,
sqflite_common_ffi 2.4.5, web 1.1.2 because the freshness gate was already
red on them at HEAD.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 09:28:18 +01:00

77 lines
2.0 KiB
Dart

import 'dart:math' as math;
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:todo/data/note.dart';
import 'package:todo/ui/note_grid_tile.dart';
/// Below this width the notes list is a grid; at or above it, the list.
const double kNotesGridMaxWidth = 600;
const _columns = 2;
const double _gap = AppSpacing.sm;
const double _sidePadding = AppSpacing.md - AppSpacing.xs;
const _minTileHeight = 96.0;
double _tileHeightFor(double height) {
final rows = math.max(1, ((height + _gap) / (_minTileHeight + _gap)).floor());
return (height + _gap) / rows - _gap;
}
/// Phone notes list: two columns, scrolling only past one screenful.
class NotesGrid extends StatelessWidget {
/// Creates the grid; [onOpen] and [onActions] get the tapped note.
const NotesGrid({
required this.notes,
required this.onOpen,
required this.onActions,
super.key,
});
/// Notes in display order.
final List<Note> notes;
/// Tap on a tile.
final ValueChanged<Note> onOpen;
/// Long-press on a tile.
final ValueChanged<Note> onActions;
Widget _tile(Note note) {
return NoteGridTile(
key: ValueKey(note.id),
note: note,
onTap: () => onOpen(note),
onActions: () => onActions(note),
);
}
SliverGridDelegate _delegate(double viewportHeight) {
return SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: _columns,
mainAxisSpacing: _gap,
crossAxisSpacing: _gap,
mainAxisExtent: _tileHeightFor(viewportHeight),
);
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: _gap),
child: SafeArea(
top: false,
child: LayoutBuilder(
builder: (context, box) => GridView.builder(
padding: const EdgeInsets.symmetric(horizontal: _sidePadding),
gridDelegate: _delegate(box.maxHeight),
itemCount: notes.length,
itemBuilder: (context, i) => _tile(notes[i]),
),
),
),
);
}
}