feat: tablet landscape improvements

Shell: move Projects/News/Calendar into ShellRoute so the NavigationRail
persists across all screens. Show all 6 nav destinations on tablet
instead of 3+More overflow. Phone bottom nav unchanged.

Chat: master-detail layout on tablet — conversations list (320px) with
inline chat panel. Tapping a conversation updates state instead of
pushing a new route.

Knowledge: responsive grid (2 cols portrait, 3 cols landscape) with
card layout showing icon, title, body preview, and tags. Fix snippet
data — read json['snippet'] which the API actually sends. Bump snippet
length from 120 to 200 chars. Tasks now show description as snippet.

News: responsive grid with the same breakpoints. Larger snippet
(5 lines) in grid mode via snippetMaxLines parameter.

Briefing: centered reading column (maxWidth 700) on wide screens,
matching the web UI layout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-16 01:31:52 -04:00
parent bdaa5210f0
commit 58d4cfab4d
8 changed files with 346 additions and 125 deletions
+49 -10
View File
@@ -186,18 +186,57 @@ class _KnowledgeScreenState extends ConsumerState<KnowledgeScreen>
}
return RefreshIndicator(
onRefresh: () => ref.read(knowledgeProvider.notifier).refresh(),
child: ListView.separated(
controller: _scrollController,
itemCount: items.length + (state.isLoadingBatch ? 1 : 0),
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (_, i) {
if (i >= items.length) {
return const Padding(
padding: EdgeInsets.all(16),
child: Center(child: CircularProgressIndicator()),
child: LayoutBuilder(
builder: (context, constraints) {
final cols = constraints.maxWidth >= 900
? 3
: constraints.maxWidth >= 600
? 2
: 1;
if (cols == 1) {
return ListView.separated(
controller: _scrollController,
itemCount: items.length + (state.isLoadingBatch ? 1 : 0),
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (_, i) {
if (i >= items.length) {
return const Padding(
padding: EdgeInsets.all(16),
child: Center(child: CircularProgressIndicator()),
);
}
return KnowledgeItemCard(item: items[i]);
},
);
}
return KnowledgeItemCard(item: items[i]);
return CustomScrollView(
controller: _scrollController,
slivers: [
SliverPadding(
padding: const EdgeInsets.all(8),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: cols,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 1.8,
),
delegate: SliverChildBuilderDelegate(
(_, i) {
if (i >= items.length) {
return const Center(
child: CircularProgressIndicator());
}
return KnowledgeItemCard(item: items[i])
.buildGridCard(context);
},
childCount:
items.length + (state.isLoadingBatch ? 1 : 0),
),
),
),
],
);
},
),
);