add a reader chrome: back/forward, pin, and edit-pencil
The markdown and decision sidebar readers gain a shared action bar. A new lib/builtin/shared/reader_chrome.dart provides ReaderHistory (browser-style back/forward stack — push truncates forward), a ReaderHistoryMixin that also holds a single pin slot, and a ReaderActionBar widget. Both readers push to history only on external selection; back/forward and jump-to-pin reload in-place without re-publishing a selection (no bus churn / no decision-tab re-trigger). The edit pencil opens the current doc in the editor (editor.open) — the markdown path, or the decision's file_path. T-189, T-190, T-191. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:clide/builtin/decisions/src/decision_colors.dart';
|
||||
import 'package:clide/builtin/shared/reader_chrome.dart';
|
||||
import 'package:clide/kernel/kernel.dart';
|
||||
import 'package:clide/widgets/widgets.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
@@ -13,7 +14,7 @@ class DecisionDetailView extends StatefulWidget {
|
||||
State<DecisionDetailView> createState() => _DecisionDetailViewState();
|
||||
}
|
||||
|
||||
class _DecisionDetailViewState extends State<DecisionDetailView> {
|
||||
class _DecisionDetailViewState extends State<DecisionDetailView> with ReaderHistoryMixin<String, DecisionDetailView> {
|
||||
Map<String, Object?>? _decision;
|
||||
bool _loading = false;
|
||||
StreamSubscription<Message>? _sub;
|
||||
@@ -38,6 +39,7 @@ class _DecisionDetailViewState extends State<DecisionDetailView> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Load [id] and push it onto the history stack (external navigation).
|
||||
Future<void> _load(String id) async {
|
||||
setState(() => _loading = true);
|
||||
final kernel = ClideKernel.of(context);
|
||||
@@ -50,6 +52,48 @@ class _DecisionDetailViewState extends State<DecisionDetailView> {
|
||||
_loading = false;
|
||||
_decision = resp.ok ? resp.data : null;
|
||||
});
|
||||
if (resp.ok) historyPush(id);
|
||||
}
|
||||
|
||||
/// Load [id] WITHOUT pushing onto the history stack (back/forward nav).
|
||||
Future<void> _loadInPlace(String id) async {
|
||||
setState(() => _loading = true);
|
||||
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 _onBack() {
|
||||
final entry = historyBack();
|
||||
if (entry != null) _loadInPlace(entry);
|
||||
}
|
||||
|
||||
void _onForward() {
|
||||
final entry = historyForward();
|
||||
if (entry != null) _loadInPlace(entry);
|
||||
}
|
||||
|
||||
void _onPin() {
|
||||
pinCurrent();
|
||||
}
|
||||
|
||||
void _onJumpToPin() {
|
||||
final entry = jumpToPin();
|
||||
if (entry != null) _loadInPlace(entry);
|
||||
}
|
||||
|
||||
void _onEdit() {
|
||||
final filePath = _decision?['file_path'] as String?;
|
||||
if (filePath == null) return;
|
||||
final kernel = ClideKernel.of(context);
|
||||
unawaited(kernel.ipc.request('editor.open', args: {'path': filePath}));
|
||||
}
|
||||
|
||||
void _navigateToRecord(BuildContext context, String id) {
|
||||
@@ -80,63 +124,80 @@ class _DecisionDetailViewState extends State<DecisionDetailView> {
|
||||
final body = d['body'] as String?;
|
||||
final refs = (d['refs'] as List?)?.cast<Map<String, Object?>>() ?? const [];
|
||||
final typeColor = typeColors.forType(type);
|
||||
final filePath = d['file_path'] as String?;
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: tokens.panelBackground,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: tokens.panelBorder),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
ClideTooltip(
|
||||
message: type ?? 'confirmed',
|
||||
child: Container(width: 10, height: 10, decoration: BoxDecoration(color: typeColor, shape: BoxShape.circle)),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
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: clideFontBadge, color: tokens.globalTextMuted, fontFamily: clideMonoFamily),
|
||||
return ClidePaneChrome(
|
||||
title: id,
|
||||
subtitle: title,
|
||||
trailing: [
|
||||
ReaderActionBar(
|
||||
canGoBack: canGoBack,
|
||||
canGoForward: canGoForward,
|
||||
hasPinned: hasPinned,
|
||||
onBack: canGoBack ? _onBack : null,
|
||||
onForward: canGoForward ? _onForward : null,
|
||||
onPin: _decision != null ? _onPin : null,
|
||||
onJumpToPin: hasPinned ? _onJumpToPin : null,
|
||||
onEdit: filePath != null ? _onEdit : null,
|
||||
),
|
||||
],
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: tokens.panelBackground,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: tokens.panelBorder),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
ClideTooltip(
|
||||
message: type ?? 'confirmed',
|
||||
child: Container(width: 10, height: 10, decoration: BoxDecoration(color: typeColor, shape: BoxShape.circle)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ClideText(title, fontSize: 15, fontWeight: FontWeight.w500),
|
||||
if (date != null) ...[
|
||||
const SizedBox(height: 6),
|
||||
ClideText(date, muted: true, fontSize: clideFontSmall, fontFamily: clideMonoFamily),
|
||||
],
|
||||
if (status != null && status != 'active') ...[
|
||||
const SizedBox(width: 8),
|
||||
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: clideFontBadge, color: tokens.globalTextMuted, fontFamily: clideMonoFamily),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_StatusBadge(status: status, tokens: tokens),
|
||||
ClideText(title, fontSize: 15, fontWeight: FontWeight.w500),
|
||||
if (date != null) ...[
|
||||
const SizedBox(height: 6),
|
||||
ClideText(date, muted: true, fontSize: clideFontSmall, fontFamily: clideMonoFamily),
|
||||
],
|
||||
if (status != null && status != 'active') ...[
|
||||
const SizedBox(height: 8),
|
||||
_StatusBadge(status: status, tokens: tokens),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (body != null && body.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
ClideMarkdown(body, onRecordTap: (id) => _navigateToRecord(context, id)),
|
||||
if (body != null && body.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
ClideMarkdown(body, onRecordTap: (id) => _navigateToRecord(context, id)),
|
||||
],
|
||||
if (refs.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
ClideText('CROSS-REFERENCES', fontSize: clideFontSmall, color: tokens.sidebarSectionHeader, fontFamily: clideMonoFamily),
|
||||
const SizedBox(height: 6),
|
||||
for (final ref in refs) _RefCard(ref: ref, tokens: tokens),
|
||||
],
|
||||
],
|
||||
if (refs.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
ClideText('CROSS-REFERENCES', fontSize: clideFontSmall, color: tokens.sidebarSectionHeader, fontFamily: clideMonoFamily),
|
||||
const SizedBox(height: 6),
|
||||
for (final ref in refs) _RefCard(ref: ref, tokens: tokens),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user