Files
clide/lib/builtin/pql/src/backlinks_view.dart
T
jpmschweitzerandClaude Opus 4.6 cf3e7700a3 sweep ClideTappable across 15 files, remove 267 lines
23 StatefulWidget+State hover pairs converted to StatelessWidget
using ClideTappable(builder: (ctx, hovered, _) => ...). Covers
app.dart (5), welcome (2), claude session host (2), column hat (2),
spine (1), pane chrome (1), tab bar (1), decisions (1), tickets (1),
graph (1), pql panel (1), backlinks (1), file tree (2), git panel (1),
git status (1). Only EditorDragHandle and ClidePalette excluded
(drag/keyboard behavior beyond hover+tap).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 11:28:35 +02:00

183 lines
5.2 KiB
Dart

/// Context panel showing backlinks and outlinks for the active file.
library;
import 'dart:async';
import 'package:clide/kernel/kernel.dart';
import 'package:clide/widgets/widgets.dart';
import 'package:flutter/widgets.dart';
import 'backlinks_controller.dart';
class BacklinksView extends StatefulWidget {
const BacklinksView({super.key});
@override
State<BacklinksView> createState() => _BacklinksViewState();
}
class _BacklinksViewState extends State<BacklinksView> {
BacklinksController? _controller;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (_controller != null) return;
final kernel = ClideKernel.of(context);
_controller = BacklinksController(ipc: kernel.ipc, events: kernel.events);
}
@override
void dispose() {
_controller?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final c = _controller;
if (c == null) return const SizedBox.shrink();
return ListenableBuilder(
listenable: c,
builder: (context, _) {
final tokens = ClideTheme.of(context).surface;
if (c.activePath == null) {
return const Padding(
padding: EdgeInsets.all(12),
child: ClideText(
'Open a file to see its links.',
muted: true,
),
);
}
return Semantics(
label: 'backlinks for ${c.activePath}',
container: true,
explicitChildNodes: true,
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 4),
child: ClideText(
c.activePath!.split('/').last,
color: tokens.globalForeground,
),
),
if (c.error != null)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 4),
child: ClideText(
c.error!,
color: tokens.statusError,
fontSize: clideFontCaption,
),
),
if (c.loading)
const Padding(
padding: EdgeInsets.all(12),
child: ClideText('Loading…', muted: true),
),
_LinkGroup(
label: 'Backlinks',
links: c.backlinks,
pathKey: 'source',
),
_LinkGroup(
label: 'Outlinks',
links: c.outlinks,
pathKey: 'target',
),
],
),
),
);
},
);
}
}
class _LinkGroup extends StatelessWidget {
const _LinkGroup({
required this.label,
required this.links,
required this.pathKey,
});
final String label;
final List<Map<String, Object?>> links;
final String pathKey;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding:
const EdgeInsets.only(left: 12, right: 8, top: 8, bottom: 2),
child: ClideText(
'$label (${links.length})',
fontSize: clideFontCaption,
muted: true,
),
),
if (links.isEmpty)
const Padding(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 2),
child: ClideText('None', fontSize: clideFontCaption, muted: true),
),
for (final link in links)
_LinkRow(link: link, pathKey: pathKey),
],
);
}
}
class _LinkRow extends StatelessWidget {
const _LinkRow({required this.link, required this.pathKey});
final Map<String, Object?> link;
final String pathKey;
@override
Widget build(BuildContext context) {
final tokens = ClideTheme.of(context).surface;
final target = link[pathKey] as String? ?? '';
final alias = link['alias'] as String?;
final display = alias ?? target;
return Semantics(
button: true,
label: target,
child: ClideTappable(
onTap: () {
if (!target.startsWith('http')) {
final kernel = ClideKernel.of(context);
unawaited(
kernel.ipc.request('editor.open', args: {'path': target}));
}
},
builder: (context, hovered, _) => Container(
color: hovered ? tokens.sidebarItemHover : null,
padding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 2),
child: ClideText(
display,
maxLines: 1,
overflow: TextOverflow.ellipsis,
color: target.startsWith('http')
? tokens.statusInfo
: tokens.sidebarForeground,
),
),
),
);
}
}