Files
clide/lib/widgets/src/clide_pane_chrome.dart
T
2026-05-03 21:51:59 +02:00

173 lines
4.8 KiB
Dart

import 'package:clide/kernel/src/theme/controller.dart';
import 'package:flutter/widgets.dart';
import 'clide_divider.dart';
import 'clide_icon.dart';
import 'clide_tappable.dart';
import 'clide_text.dart';
import 'icons/x.dart';
import 'typography.dart';
/// Shared chrome for any pane that sits in a tab or split: a title
/// strip at the top, an optional close button, and the pane body
/// underneath. `ClidePtyView`, diff views, canvas tabs, graph tabs —
/// everything with a "pane header" surface reuses this.
class ClidePaneChrome extends StatelessWidget {
const ClidePaneChrome({
super.key,
required this.title,
required this.child,
this.subtitle,
this.leading,
this.onClose,
this.trailing,
});
/// Primary label in the header — typically the pane kind + an
/// abbreviated path / session name (`terminal — ~/clide`).
final String title;
/// Optional secondary line (cwd hint, session id, status).
final String? subtitle;
/// Icon or badge drawn before the title.
final Widget? leading;
/// Main pane content.
final Widget child;
/// If provided, renders an `x` close button on the right. Primary
/// Claude panes deliberately pass `null` so the user can't hide the
/// primary (D-041).
final VoidCallback? onClose;
/// Extra trailing widgets (status indicator, menu button, etc.).
/// Drawn before the close button when both are present.
final List<Widget>? trailing;
@override
Widget build(BuildContext context) {
final tokens = ClideTheme.of(context).surface;
return ColoredBox(
color: tokens.panelBackground,
child: Column(
children: [
_Header(
title: title,
subtitle: subtitle,
leading: leading,
onClose: onClose,
trailing: trailing,
),
const ClideDivider(),
Expanded(child: child),
],
),
);
}
}
class _CloseButton extends StatelessWidget {
const _CloseButton({required this.onPressed});
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
final tokens = ClideTheme.of(context).surface;
return Semantics(
button: true,
label: 'Close pane',
onTap: onPressed,
child: ClideTappable(
onTap: onPressed,
tooltip: 'Close pane',
builder: (context, hovered, _) => Container(
width: 20,
height: 20,
alignment: Alignment.center,
decoration: BoxDecoration(
color: hovered ? tokens.tabCloseHover : null,
),
child: ClideIcon(
const CloseIcon(),
size: 10,
color: tokens.panelHeaderForeground,
),
),
),
);
}
}
class _Header extends StatelessWidget {
const _Header({
required this.title,
required this.subtitle,
required this.leading,
required this.onClose,
required this.trailing,
});
final String title;
final String? subtitle;
final Widget? leading;
final VoidCallback? onClose;
final List<Widget>? trailing;
@override
Widget build(BuildContext context) {
final tokens = ClideTheme.of(context).surface;
return Semantics(
container: true,
explicitChildNodes: true,
label: 'pane header: $title',
child: ColoredBox(
color: tokens.panelHeader,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
child: Row(
children: [
if (leading != null) ...[leading!, const SizedBox(width: 6)],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
ClideText(
title,
fontSize: clideFontCaption,
color: tokens.panelHeaderForeground,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (subtitle != null)
ClideText(
subtitle!,
fontSize: clideFontCaption,
muted: true,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
if (trailing != null)
...trailing!.map(
(w) => Padding(
padding: const EdgeInsets.only(left: 6),
child: w,
),
),
if (onClose != null)
Padding(
padding: const EdgeInsets.only(left: 6),
child: _CloseButton(onPressed: onClose!),
),
],
),
),
),
);
}
}