Files
clide/lib/builtin/markdown/src/markdown_viewer.dart
T
jpmschweitzerandClaude 04d133f4ed sidebar focus indicators, bus navigation, font constants
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 <noreply@anthropic.com>
2026-04-23 18:53:53 +02:00

100 lines
2.8 KiB
Dart

import 'dart:async';
import 'package:clide/kernel/kernel.dart';
import 'package:clide/widgets/widgets.dart';
import 'package:flutter/widgets.dart';
class MarkdownViewer extends StatefulWidget {
const MarkdownViewer({super.key});
@override
State<MarkdownViewer> createState() => _MarkdownViewerState();
}
class _MarkdownViewerState extends State<MarkdownViewer> {
String? _path;
String? _content;
String? _error;
StreamSubscription<DaemonEvent>? _eventSub;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (_eventSub != null) return;
final kernel = ClideKernel.of(context);
_eventSub = kernel.events.on<DaemonEvent>().listen((e) {
if (e.kind == 'editor.buffer_activated') {
final path = e.data['path'] as String?;
if (path != null && path.endsWith('.md')) {
_loadFile(path);
}
}
});
final activeTab = kernel.panels.activeTabIn(Slots.workspace);
if (activeTab == 'editor.active') {
unawaited(_loadActiveBuffer());
}
}
@override
void dispose() {
_eventSub?.cancel();
super.dispose();
}
Future<void> _loadActiveBuffer() async {
final kernel = ClideKernel.of(context);
final resp = await kernel.ipc.request('editor.active');
if (!mounted || !resp.ok) return;
final path = resp.data['path'] as String?;
if (path != null && path.endsWith('.md')) {
await _loadFile(path);
}
}
Future<void> _loadFile(String path) async {
final kernel = ClideKernel.of(context);
final resp = await kernel.ipc.request('files.read', args: {'path': path});
if (!mounted) return;
if (resp.ok) {
setState(() {
_path = path;
_content = resp.data['content'] as String? ?? '';
_error = null;
});
} else {
setState(() => _error = resp.error?.message);
}
}
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) {
return Padding(padding: const EdgeInsets.all(12), child: ClideText(_error!, muted: true));
}
if (_content == null) {
return const Padding(
padding: EdgeInsets.all(12),
child: ClideText('Open a .md file to preview it here.', muted: true),
);
}
return ClidePaneChrome(
title: _path ?? 'viewer',
subtitle: '${_content!.split('\n').length} lines',
child: SingleChildScrollView(
padding: const EdgeInsets.all(12),
child: ClideMarkdown(_content!, onRecordTap: (id) => _navigateToRecord(context, id)),
),
);
}
}