From 04d133f4edb22757fd74116964a1ecefbff4ee33 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Thu, 23 Apr 2026 18:53:53 +0200 Subject: [PATCH] sidebar focus indicators, bus navigation, font constants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All sidebar and detail panes use clideFontSmall/clideFontBadge/ clideFontCaption instead of hardcoded sizes — single place to tune. Decision detail view subscribes to the message bus for selection events (same pattern as ticket controller) and publishes focus after loading. Both decisions and tickets sidebar views subscribe to the focus channel, expand the accordion section, highlight the active card, and scroll it into view. Clickable DQRT links wired in decision detail, ticket detail, and markdown viewer via onRecordTap. Ticket descriptions now render through ClideMarkdown instead of plain text. Co-Authored-By: Claude --- .../decisions/src/decision_detail_view.dart | 38 +++++++++---- lib/builtin/decisions/src/decisions_view.dart | 53 +++++++++++++++---- lib/builtin/graph/src/graph_view.dart | 4 +- lib/builtin/markdown/src/markdown_viewer.dart | 11 +++- .../tickets/src/ticket_detail_view.dart | 31 +++++++---- lib/builtin/tickets/src/tickets_view.dart | 52 ++++++++++++------ 6 files changed, 140 insertions(+), 49 deletions(-) diff --git a/lib/builtin/decisions/src/decision_detail_view.dart b/lib/builtin/decisions/src/decision_detail_view.dart index 597e077b..0ca9b9c0 100644 --- a/lib/builtin/decisions/src/decision_detail_view.dart +++ b/lib/builtin/decisions/src/decision_detail_view.dart @@ -16,17 +16,25 @@ class DecisionDetailView extends StatefulWidget { class _DecisionDetailViewState extends State { Map? _decision; bool _loading = false; + StreamSubscription? _sub; @override void didChangeDependencies() { super.didChangeDependencies(); - if (widget.initialId != null && _decision == null && !_loading) { + if (_sub != null) return; + final kernel = ClideKernel.of(context); + _sub = kernel.messages.subscribe(publisher: 'builtin.decisions', channel: 'selection').listen((msg) { + final id = msg.data['id'] as String?; + if (id != null) unawaited(_load(id)); + }); + if (widget.initialId != null) { unawaited(_load(widget.initialId!)); } } @override void dispose() { + _sub?.cancel(); super.dispose(); } @@ -35,12 +43,24 @@ class _DecisionDetailViewState extends State { final kernel = ClideKernel.of(context); final resp = await kernel.ipc.request('pql.decisions.read', args: {'id': id}); if (!mounted) return; + if (resp.ok) { + kernel.messages.publish('builtin.decisions', 'focus', {'id': id}); + } setState(() { _loading = false; _decision = resp.ok ? resp.data : null; }); } + void _navigateToRecord(BuildContext context, String id) { + final kernel = ClideKernel.of(context); + if (id.startsWith('T-')) { + kernel.messages.publish('builtin.tickets', 'selection', {'id': id}); + } else { + kernel.messages.publish('builtin.decisions', 'selection', {'id': id}); + } + } + @override Widget build(BuildContext context) { if (_loading) return const Center(child: ClideText('Loading…', muted: true)); @@ -83,13 +103,13 @@ class _DecisionDetailViewState extends State { child: Container(width: 10, height: 10, decoration: BoxDecoration(color: typeColor, shape: BoxShape.circle)), ), const SizedBox(width: 8), - ClideText(id, fontSize: 13, color: typeColor, fontFamily: clideMonoFamily), + ClideText(id, fontSize: clideFontSmall, color: typeColor, fontFamily: clideMonoFamily), const Spacer(), if (domain != null) Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration(color: tokens.panelBorder, borderRadius: BorderRadius.circular(3)), - child: ClideText(domain, fontSize: 10, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), + child: ClideText(domain, fontSize: clideFontBadge, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), ), ], ), @@ -97,7 +117,7 @@ class _DecisionDetailViewState extends State { ClideText(title, fontSize: 15, fontWeight: FontWeight.w500), if (date != null) ...[ const SizedBox(height: 6), - ClideText(date, muted: true, fontSize: 12, fontFamily: clideMonoFamily), + ClideText(date, muted: true, fontSize: clideFontSmall, fontFamily: clideMonoFamily), ], if (status != null && status != 'active') ...[ const SizedBox(height: 8), @@ -108,11 +128,11 @@ class _DecisionDetailViewState extends State { ), if (body != null && body.isNotEmpty) ...[ const SizedBox(height: 12), - ClideMarkdown(body), + ClideMarkdown(body, onRecordTap: (id) => _navigateToRecord(context, id)), ], if (refs.isNotEmpty) ...[ const SizedBox(height: 16), - ClideText('CROSS-REFERENCES', fontSize: 11, color: tokens.sidebarSectionHeader, fontFamily: clideMonoFamily), + ClideText('CROSS-REFERENCES', fontSize: clideFontSmall, color: tokens.sidebarSectionHeader, fontFamily: clideMonoFamily), const SizedBox(height: 6), for (final ref in refs) _RefCard(ref: ref, tokens: tokens), ], @@ -144,9 +164,9 @@ class _RefCard extends StatelessWidget { ), child: Row( children: [ - ClideText(targetId, fontSize: 11, color: tokens.globalFocus, fontFamily: clideMonoFamily), + ClideText(targetId, fontSize: clideFontSmall, color: tokens.globalFocus, fontFamily: clideMonoFamily), const SizedBox(width: 8), - ClideText(refType, fontSize: 11, color: tokens.globalTextMuted), + ClideText(refType, fontSize: clideFontSmall, color: tokens.globalTextMuted), ], ), ), @@ -170,7 +190,7 @@ class _StatusBadge extends StatelessWidget { return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration(color: color.withAlpha(0x30), borderRadius: BorderRadius.circular(3)), - child: ClideText(status.toUpperCase(), fontSize: 10, color: color, fontFamily: clideMonoFamily), + child: ClideText(status.toUpperCase(), fontSize: clideFontBadge, color: color, fontFamily: clideMonoFamily), ); } } diff --git a/lib/builtin/decisions/src/decisions_view.dart b/lib/builtin/decisions/src/decisions_view.dart index 3d194928..c09cfb19 100644 --- a/lib/builtin/decisions/src/decisions_view.dart +++ b/lib/builtin/decisions/src/decisions_view.dart @@ -17,11 +17,18 @@ class _DecisionsViewState extends State { String? _error; bool _loading = true; String _filter = ''; + String? _focusedId; + final _focusedKey = GlobalKey(); final Set _expanded = {'confirmed'}; + StreamSubscription? _focusSub; @override void didChangeDependencies() { super.didChangeDependencies(); + if (_focusSub == null) { + final kernel = ClideKernel.of(context); + _focusSub = kernel.messages.subscribe(publisher: 'builtin.decisions', channel: 'focus').listen(_onFocus); + } if (!_loading || _decisions.isNotEmpty) return; unawaited(_load()); } @@ -49,6 +56,27 @@ class _DecisionsViewState extends State { } } + @override + void dispose() { + _focusSub?.cancel(); + super.dispose(); + } + + void _onFocus(Message msg) { + final id = msg.data['id'] as String?; + if (id == null || id == _focusedId) return; + final entry = _decisions.where((d) => d.id == id).firstOrNull; + final section = entry?.type ?? 'confirmed'; + setState(() { + _focusedId = id; + _expanded.add(section); + }); + WidgetsBinding.instance.addPostFrameCallback((_) { + final ctx = _focusedKey.currentContext; + if (ctx != null) Scrollable.ensureVisible(ctx, duration: const Duration(milliseconds: 200), alignment: 0.3); + }); + } + void _toggleSection(String key) { setState(() { if (_expanded.contains(key)) { @@ -91,21 +119,21 @@ class _DecisionsViewState extends State { color: typeColors.confirmed, expanded: hasFilter || _expanded.contains('confirmed'), onToggle: () => _toggleSection('confirmed'), - children: [for (final d in confirmed) _DecisionCard(entry: d, tokens: tokens, typeColors: typeColors)], + children: [for (final d in confirmed) _DecisionCard(entry: d, tokens: tokens, typeColors: typeColors, focused: d.id == _focusedId, focusKey: d.id == _focusedId ? _focusedKey : null)], ), if (questions.isNotEmpty) _AccordionSection( label: 'QUESTIONS', count: questions.length, tokens: tokens, color: typeColors.question, expanded: hasFilter || _expanded.contains('question'), onToggle: () => _toggleSection('question'), - children: [for (final d in questions) _DecisionCard(entry: d, tokens: tokens, typeColors: typeColors)], + children: [for (final d in questions) _DecisionCard(entry: d, tokens: tokens, typeColors: typeColors, focused: d.id == _focusedId, focusKey: d.id == _focusedId ? _focusedKey : null)], ), if (rejected.isNotEmpty) _AccordionSection( label: 'REJECTED', count: rejected.length, tokens: tokens, color: typeColors.rejected, expanded: hasFilter || _expanded.contains('rejected'), onToggle: () => _toggleSection('rejected'), - children: [for (final d in rejected) _DecisionCard(entry: d, tokens: tokens, typeColors: typeColors)], + children: [for (final d in rejected) _DecisionCard(entry: d, tokens: tokens, typeColors: typeColors, focused: d.id == _focusedId, focusKey: d.id == _focusedId ? _focusedKey : null)], ), ], ), @@ -162,7 +190,7 @@ class _AccordionSection extends StatelessWidget { const SizedBox(width: 6), Container(width: 8, height: 8, decoration: BoxDecoration(color: color, shape: BoxShape.circle)), const SizedBox(width: 6), - ClideText('$label · $count', fontSize: 11, color: hovered ? tokens.globalForeground : tokens.sidebarSectionHeader, fontFamily: clideMonoFamily), + ClideText('$label · $count', fontSize: clideFontSmall, color: hovered ? tokens.globalForeground : tokens.sidebarSectionHeader, fontFamily: clideMonoFamily), ], ), ), @@ -174,24 +202,27 @@ class _AccordionSection extends StatelessWidget { } class _DecisionCard extends StatelessWidget { - const _DecisionCard({required this.entry, required this.tokens, required this.typeColors}); + const _DecisionCard({required this.entry, required this.tokens, required this.typeColors, this.focused = false, this.focusKey}); final _DecisionEntry entry; final SurfaceTokens tokens; final DecisionTypeColors typeColors; + final bool focused; + final GlobalKey? focusKey; @override Widget build(BuildContext context) { final typeColor = typeColors.forType(entry.type); return Padding( + key: focusKey, padding: const EdgeInsets.only(bottom: 4), child: ClideTappable( onTap: () => ClideKernel.of(context).messages.publish('builtin.decisions', 'selection', {'id': entry.id}), builder: (ctx, hovered, _) => Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: hovered ? tokens.sidebarItemHover : tokens.panelBackground, + color: hovered ? tokens.sidebarItemHover : (focused ? tokens.sidebarItemSelected : tokens.panelBackground), borderRadius: BorderRadius.circular(4), - border: Border.all(color: hovered ? tokens.panelActiveBorder : tokens.panelBorder, width: 1), + border: Border.all(color: focused ? tokens.globalFocus : (hovered ? tokens.panelActiveBorder : tokens.panelBorder), width: 1), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -203,20 +234,20 @@ class _DecisionCard extends StatelessWidget { child: Container(width: 8, height: 8, decoration: BoxDecoration(color: typeColor, shape: BoxShape.circle)), ), const SizedBox(width: 6), - ClideText(entry.id, fontSize: 11, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), + ClideText(entry.id, fontSize: clideFontSmall, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), const Spacer(), if (entry.domain != null) - ClideText(entry.domain!, fontSize: 10, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), + ClideText(entry.domain!, fontSize: clideFontBadge, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), ], ), const SizedBox(height: 4), - ClideText(entry.title, fontSize: 13), + ClideText(entry.title, fontSize: clideFontCaption), if (entry.status == 'resolved') ...[ const SizedBox(height: 6), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration(color: tokens.statusSuccess.withAlpha(0x30), borderRadius: BorderRadius.circular(3)), - child: ClideText('resolved', fontSize: 10, color: tokens.statusSuccess, fontFamily: clideMonoFamily), + child: ClideText('resolved', fontSize: clideFontBadge, color: tokens.statusSuccess, fontFamily: clideMonoFamily), ), ], ], diff --git a/lib/builtin/graph/src/graph_view.dart b/lib/builtin/graph/src/graph_view.dart index 2149e150..e6c2391d 100644 --- a/lib/builtin/graph/src/graph_view.dart +++ b/lib/builtin/graph/src/graph_view.dart @@ -103,8 +103,8 @@ class _NodeRow extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: Row( children: [ - Expanded(child: ClideText(node.path, fontSize: 13)), - ClideText('${node.inbound}in ${node.outbound}out', color: tokens.globalTextMuted, fontSize: 11), + Expanded(child: ClideText(node.path, fontSize: clideFontCaption)), + ClideText('${node.inbound}in ${node.outbound}out', color: tokens.globalTextMuted, fontSize: clideFontSmall), ], ), ), diff --git a/lib/builtin/markdown/src/markdown_viewer.dart b/lib/builtin/markdown/src/markdown_viewer.dart index 39f19710..e8a31ecf 100644 --- a/lib/builtin/markdown/src/markdown_viewer.dart +++ b/lib/builtin/markdown/src/markdown_viewer.dart @@ -67,6 +67,15 @@ class _MarkdownViewerState extends State { } } + void _navigateToRecord(BuildContext context, String id) { + final kernel = ClideKernel.of(context); + if (id.startsWith('T-')) { + kernel.messages.publish('builtin.tickets', 'selection', {'id': id}); + } else { + kernel.messages.publish('builtin.decisions', 'selection', {'id': id}); + } + } + @override Widget build(BuildContext context) { if (_error != null) { @@ -83,7 +92,7 @@ class _MarkdownViewerState extends State { subtitle: '${_content!.split('\n').length} lines', child: SingleChildScrollView( padding: const EdgeInsets.all(12), - child: ClideMarkdown(_content!), + child: ClideMarkdown(_content!, onRecordTap: (id) => _navigateToRecord(context, id)), ), ); } diff --git a/lib/builtin/tickets/src/ticket_detail_view.dart b/lib/builtin/tickets/src/ticket_detail_view.dart index 81d029f4..d6092b71 100644 --- a/lib/builtin/tickets/src/ticket_detail_view.dart +++ b/lib/builtin/tickets/src/ticket_detail_view.dart @@ -32,6 +32,15 @@ class _TicketDetailViewState extends State { super.dispose(); } + void _navigateToRecord(BuildContext context, String id) { + final kernel = ClideKernel.of(context); + if (id.startsWith('T-')) { + kernel.messages.publish('builtin.tickets', 'selection', {'id': id}); + } else { + kernel.messages.publish('builtin.decisions', 'selection', {'id': id}); + } + } + @override Widget build(BuildContext context) { final c = _controller; @@ -57,7 +66,7 @@ class _TicketDetailViewState extends State { _StatusControls(detail: d, tokens: tokens, controller: c), if (d.description != null && d.description!.isNotEmpty) ...[ const SizedBox(height: 12), - ClideText(d.description!, muted: true, fontSize: 13), + ClideMarkdown(d.description!, onRecordTap: (id) => _navigateToRecord(ctx, id)), ], if (d.parents.isNotEmpty) ...[ const SizedBox(height: 16), @@ -111,17 +120,17 @@ class _TicketHeader extends StatelessWidget { child: Container(width: 10, height: 10, decoration: BoxDecoration(color: typeColor, shape: BoxShape.circle)), ), const SizedBox(width: 8), - ClideText(detail.id, fontSize: 13, color: typeColor, fontFamily: clideMonoFamily), + ClideText(detail.id, fontSize: clideFontSmall, color: typeColor, fontFamily: clideMonoFamily), const Spacer(), if (detail.priority != null) - ClideText(detail.priority!, fontSize: 11, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), + ClideText(detail.priority!, fontSize: clideFontSmall, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), ], ), const SizedBox(height: 8), ClideText(detail.title, fontSize: 15, fontWeight: FontWeight.w500), if (detail.assignedTo != null) ...[ const SizedBox(height: 6), - ClideText('assigned: ${detail.assignedTo}', muted: true, fontSize: 12, fontFamily: clideMonoFamily), + ClideText('assigned: ${detail.assignedTo}', muted: true, fontSize: clideFontSmall, fontFamily: clideMonoFamily), ], ], ), @@ -159,7 +168,7 @@ class _StatusControls extends StatelessWidget { border: Border.all(color: active ? tokens.statusInfo : tokens.panelBorder), ), alignment: Alignment.center, - child: ClideText(_shortLabel(s), fontSize: 10, color: color, fontFamily: clideMonoFamily), + child: ClideText(_shortLabel(s), fontSize: clideFontBadge, color: color, fontFamily: clideMonoFamily), ); }, ), @@ -187,7 +196,7 @@ class _SectionLabel extends StatelessWidget { @override Widget build(BuildContext context) { - return ClideText(label, fontSize: 11, color: tokens.sidebarSectionHeader, fontFamily: clideMonoFamily); + return ClideText(label, fontSize: clideFontSmall, color: tokens.sidebarSectionHeader, fontFamily: clideMonoFamily); } } @@ -219,9 +228,9 @@ class _CompactCard extends StatelessWidget { children: [ Container(width: 6, height: 6, decoration: BoxDecoration(color: typeColor, shape: BoxShape.circle)), const SizedBox(width: 6), - ClideText(id, fontSize: 11, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), + ClideText(id, fontSize: clideFontSmall, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), const SizedBox(width: 8), - Expanded(child: ClideText(title, fontSize: 12)), + Expanded(child: ClideText(title, fontSize: clideFontSmall)), ], ), ), @@ -265,13 +274,13 @@ class _DecisionRefCard extends StatelessWidget { children: [ Container(width: 6, height: 6, decoration: BoxDecoration(color: color, shape: BoxShape.circle)), const SizedBox(width: 6), - ClideText(id, fontSize: 11, color: color, fontFamily: clideMonoFamily), + ClideText(id, fontSize: clideFontSmall, color: color, fontFamily: clideMonoFamily), const Spacer(), - if (domain != null) ClideText(domain, fontSize: 10, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), + if (domain != null) ClideText(domain, fontSize: clideFontBadge, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), ], ), const SizedBox(height: 3), - ClideText(title, fontSize: 12), + ClideText(title, fontSize: clideFontSmall), ], ), ), diff --git a/lib/builtin/tickets/src/tickets_view.dart b/lib/builtin/tickets/src/tickets_view.dart index 5fcab9c2..86311632 100644 --- a/lib/builtin/tickets/src/tickets_view.dart +++ b/lib/builtin/tickets/src/tickets_view.dart @@ -19,6 +19,7 @@ class _TicketsViewState extends State { bool _loading = true; String _filter = ''; String? _focusedId; + final _focusedKey = GlobalKey(); final Set _expanded = {'active', 'backlog'}; StreamSubscription? _focusSub; @@ -28,15 +29,34 @@ class _TicketsViewState extends State { }); } + static String _sectionForStatus(String? status) => switch (status) { + 'in_progress' => 'active', + 'backlog' || 'ready' => 'backlog', + 'done' => 'done', + _ => 'other', + }; + + void _onFocus(Message msg) { + final id = msg.data['id'] as String?; + if (id == null || id == _focusedId) return; + final entry = _tickets.where((t) => t.id == id).firstOrNull; + final section = _sectionForStatus(entry?.status); + setState(() { + _focusedId = id; + _expanded.add(section); + }); + WidgetsBinding.instance.addPostFrameCallback((_) { + final ctx = _focusedKey.currentContext; + if (ctx != null) Scrollable.ensureVisible(ctx, duration: const Duration(milliseconds: 200), alignment: 0.3); + }); + } + @override void didChangeDependencies() { super.didChangeDependencies(); if (_focusSub == null) { final kernel = ClideKernel.of(context); - _focusSub = kernel.messages.subscribe(publisher: 'builtin.tickets', channel: 'focus').listen((msg) { - final id = msg.data['id'] as String?; - if (id != _focusedId) setState(() => _focusedId = id); - }); + _focusSub = kernel.messages.subscribe(publisher: 'builtin.tickets', channel: 'focus').listen(_onFocus); } if (!_loading || _tickets.isNotEmpty) return; unawaited(_load()); @@ -98,10 +118,10 @@ class _TicketsViewState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (active.isNotEmpty) _AccordionSection(label: 'ACTIVE', count: active.length, tokens: tokens, expanded: hasFilter || _expanded.contains('active'), onToggle: () => _toggle('active'), children: [for (final t in active) _TicketCard(entry: t, tokens: tokens, typeColors: typeColors, focused: t.id == _focusedId)]), - if (backlog.isNotEmpty) _AccordionSection(label: 'BACKLOG', count: backlog.length, tokens: tokens, expanded: hasFilter || _expanded.contains('backlog'), onToggle: () => _toggle('backlog'), children: [for (final t in backlog) _TicketCard(entry: t, tokens: tokens, typeColors: typeColors, focused: t.id == _focusedId)]), - if (done.isNotEmpty) _AccordionSection(label: 'DONE', count: done.length, tokens: tokens, expanded: hasFilter || _expanded.contains('done'), onToggle: () => _toggle('done'), children: [for (final t in done) _TicketCard(entry: t, tokens: tokens, typeColors: typeColors, focused: t.id == _focusedId)]), - if (other.isNotEmpty) _AccordionSection(label: 'OTHER', count: other.length, tokens: tokens, expanded: hasFilter || _expanded.contains('other'), onToggle: () => _toggle('other'), children: [for (final t in other) _TicketCard(entry: t, tokens: tokens, typeColors: typeColors, focused: t.id == _focusedId)]), + if (active.isNotEmpty) _AccordionSection(label: 'ACTIVE', count: active.length, tokens: tokens, expanded: hasFilter || _expanded.contains('active'), onToggle: () => _toggle('active'), children: [for (final t in active) _TicketCard(entry: t, tokens: tokens, typeColors: typeColors, focused: t.id == _focusedId, focusKey: t.id == _focusedId ? _focusedKey : null)]), + if (backlog.isNotEmpty) _AccordionSection(label: 'BACKLOG', count: backlog.length, tokens: tokens, expanded: hasFilter || _expanded.contains('backlog'), onToggle: () => _toggle('backlog'), children: [for (final t in backlog) _TicketCard(entry: t, tokens: tokens, typeColors: typeColors, focused: t.id == _focusedId, focusKey: t.id == _focusedId ? _focusedKey : null)]), + if (done.isNotEmpty) _AccordionSection(label: 'DONE', count: done.length, tokens: tokens, expanded: hasFilter || _expanded.contains('done'), onToggle: () => _toggle('done'), children: [for (final t in done) _TicketCard(entry: t, tokens: tokens, typeColors: typeColors, focused: t.id == _focusedId, focusKey: t.id == _focusedId ? _focusedKey : null)]), + if (other.isNotEmpty) _AccordionSection(label: 'OTHER', count: other.length, tokens: tokens, expanded: hasFilter || _expanded.contains('other'), onToggle: () => _toggle('other'), children: [for (final t in other) _TicketCard(entry: t, tokens: tokens, typeColors: typeColors, focused: t.id == _focusedId, focusKey: t.id == _focusedId ? _focusedKey : null)]), ], ), ), @@ -152,7 +172,7 @@ class _AccordionSection extends StatelessWidget { children: [ ClideIcon(expanded ? PhosphorIcons.caretDown : PhosphorIcons.caretRight, size: 10, color: tokens.globalTextMuted), const SizedBox(width: 6), - ClideText('$label · $count', fontSize: 11, color: hovered ? tokens.globalForeground : tokens.sidebarSectionHeader, fontFamily: clideMonoFamily), + ClideText('$label · $count', fontSize: clideFontSmall, color: hovered ? tokens.globalForeground : tokens.sidebarSectionHeader, fontFamily: clideMonoFamily), ], ), ), @@ -164,11 +184,12 @@ class _AccordionSection extends StatelessWidget { } class _TicketCard extends StatelessWidget { - const _TicketCard({required this.entry, required this.tokens, required this.typeColors, this.focused = false}); + const _TicketCard({required this.entry, required this.tokens, required this.typeColors, this.focused = false, this.focusKey}); final _TicketEntry entry; final SurfaceTokens tokens; final TicketTypeColors typeColors; final bool focused; + final GlobalKey? focusKey; @override Widget build(BuildContext context) { @@ -176,6 +197,7 @@ class _TicketCard extends StatelessWidget { final statusLabel = _statusLabel(entry.status); return Padding( + key: focusKey, padding: const EdgeInsets.only(bottom: 4), child: ClideTappable( onTap: () => ClideKernel.of(context).messages.publish('builtin.tickets', 'selection', {'id': entry.id}), @@ -200,15 +222,15 @@ class _TicketCard extends StatelessWidget { ), ), const SizedBox(width: 6), - ClideText(entry.id, fontSize: 11, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), + ClideText(entry.id, fontSize: clideFontSmall, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), if (entry.parentId != null) ...[ - ClideText(' ← ', fontSize: 11, color: tokens.globalTextMuted), - ClideText(entry.parentId!, fontSize: 11, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), + ClideText(' ← ', fontSize: clideFontSmall, color: tokens.globalTextMuted), + ClideText(entry.parentId!, fontSize: clideFontSmall, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), ], ], ), const SizedBox(height: 4), - ClideText(entry.title, fontSize: 13), + ClideText(entry.title, fontSize: clideFontCaption), if (statusLabel != null) ...[ const SizedBox(height: 6), _StatusBadge(label: statusLabel, tokens: tokens, status: entry.status), @@ -248,7 +270,7 @@ class _StatusBadge extends StatelessWidget { color: color.withAlpha(0x30), borderRadius: BorderRadius.circular(3), ), - child: ClideText(label, fontSize: 10, color: color, fontFamily: clideMonoFamily), + child: ClideText(label, fontSize: clideFontBadge, color: color, fontFamily: clideMonoFamily), ); } }