add shared pane widgets + Q-023 ssh-remote question
ClidePtyView is a theme-bound wrapper around xterm.dart's TerminalView — token-derived TerminalTheme, JetBrainsMono as the face, Semantics live-region label so screen readers + Playwright both hear it. The consumer (terminal / Claude extensions) owns the `Terminal` model and wires IPC pane.write / pane.output → terminal.write() themselves; the widget deliberately has no IPC dependency so it stays trivially testable. ClidePaneChrome is the shared pane header — title + subtitle + leading icon + trailing widgets + optional close button. The close button is null-conditional so primary Claude panes (D-041, landing in step 7) can render without one. xterm 4.0.0 added as a justified runtime dep + logged in licenses.yaml per D-042. 3 new widget tests cover header rendering, close-button presence, and the close-tap round-trip. Q-023 records the SSH-remote-development question so the daemon + IPC seams don't accrete local-only assumptions during Tier 1-5. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
import 'package:clide_app/kernel/src/theme/controller.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'clide_divider.dart';
|
||||
import 'clide_icon.dart';
|
||||
import 'clide_text.dart';
|
||||
import 'icons/x.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(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_Header(
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
leading: leading,
|
||||
onClose: onClose,
|
||||
trailing: trailing,
|
||||
),
|
||||
const ClideDivider(),
|
||||
Expanded(child: child),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CloseButton extends StatefulWidget {
|
||||
const _CloseButton({required this.onPressed});
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
State<_CloseButton> createState() => _CloseButtonState();
|
||||
}
|
||||
|
||||
class _CloseButtonState extends State<_CloseButton> {
|
||||
bool _hover = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: 'Close pane',
|
||||
onTap: widget.onPressed,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onEnter: (_) => setState(() => _hover = true),
|
||||
onExit: (_) => setState(() => _hover = false),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: widget.onPressed,
|
||||
child: Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: _hover ? 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: 12,
|
||||
color: tokens.panelHeaderForeground,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (subtitle != null)
|
||||
ClideText(
|
||||
subtitle!,
|
||||
fontSize: 11,
|
||||
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!),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import 'package:clide_app/kernel/src/theme/controller.dart';
|
||||
import 'package:clide_app/kernel/src/theme/tokens.dart';
|
||||
import 'package:clide_app/widgets/src/typography.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:xterm/xterm.dart';
|
||||
|
||||
/// Theme-aware terminal view. Wraps xterm.dart's [TerminalView] with
|
||||
/// clide token bindings, JetBrainsMono as the face, and a Semantics
|
||||
/// wrapper that exposes the pane as a live region with the terminal's
|
||||
/// aria label.
|
||||
///
|
||||
/// Callers provide the [Terminal] model; hooking its `onOutput` to an
|
||||
/// IPC `pane.write` call and feeding `pane.output` event bytes into
|
||||
/// `terminal.write()` is the consumer's job (typically a builtin
|
||||
/// extension — see `builtin.terminal` / `builtin.claude`).
|
||||
class ClidePtyView extends StatelessWidget {
|
||||
const ClidePtyView({
|
||||
super.key,
|
||||
required this.terminal,
|
||||
this.label,
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
this.fontSize = 13,
|
||||
});
|
||||
|
||||
final Terminal terminal;
|
||||
|
||||
/// A11y label — typically the pane title ("terminal — ~/repo",
|
||||
/// "claude — primary", …).
|
||||
final String? label;
|
||||
|
||||
final FocusNode? focusNode;
|
||||
final bool autofocus;
|
||||
final double fontSize;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
return Semantics(
|
||||
label: label,
|
||||
textField: true,
|
||||
multiline: true,
|
||||
focusable: true,
|
||||
liveRegion: true,
|
||||
child: ColoredBox(
|
||||
color: tokens.panelBackground,
|
||||
child: TerminalView(
|
||||
terminal,
|
||||
focusNode: focusNode,
|
||||
autofocus: autofocus,
|
||||
theme: _buildTheme(tokens),
|
||||
textStyle: TerminalStyle(
|
||||
fontSize: fontSize,
|
||||
fontFamily: clideMonoFamily,
|
||||
fontFamilyFallback: clideMonoFamilyFallback,
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
backgroundOpacity: 1,
|
||||
cursorType: TerminalCursorType.block,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive an xterm [TerminalTheme] from our surface tokens.
|
||||
///
|
||||
/// Foreground / background pull from the editor tokens so the terminal
|
||||
/// visually matches the rest of the IDE. The 16-color ANSI palette is
|
||||
/// chosen to read well on our current two bundled themes
|
||||
/// (summer-night + whatever else ships); a future pass lets themes
|
||||
/// override the palette directly in their YAML.
|
||||
TerminalTheme _buildTheme(SurfaceTokens t) {
|
||||
// Selection tint derived from the focus accent at 40% alpha — no
|
||||
// dedicated token yet; revisit when the theme layer grows an
|
||||
// editor.selection.* token family.
|
||||
final selection = t.globalFocus.withAlpha(0x66);
|
||||
return TerminalTheme(
|
||||
cursor: t.globalForeground,
|
||||
selection: selection,
|
||||
foreground: t.globalForeground,
|
||||
background: t.panelBackground,
|
||||
// ANSI palette — reasonable defaults tuned for dark themes. The
|
||||
// bright variants are the same hue with higher luminance.
|
||||
black: const Color(0xFF1b1d23),
|
||||
red: const Color(0xFFe06c75),
|
||||
green: const Color(0xFF98c379),
|
||||
yellow: const Color(0xFFe5c07b),
|
||||
blue: const Color(0xFF61afef),
|
||||
magenta: const Color(0xFFc678dd),
|
||||
cyan: const Color(0xFF56b6c2),
|
||||
white: const Color(0xFFabb2bf),
|
||||
brightBlack: const Color(0xFF5c6370),
|
||||
brightRed: const Color(0xFFff7b85),
|
||||
brightGreen: const Color(0xFFabd486),
|
||||
brightYellow: const Color(0xFFffd89a),
|
||||
brightBlue: const Color(0xFF82c5ff),
|
||||
brightMagenta: const Color(0xFFdb8fe4),
|
||||
brightCyan: const Color(0xFF6fcbd6),
|
||||
brightWhite: const Color(0xFFffffff),
|
||||
searchHitBackground: const Color(0xFFffeb8c),
|
||||
searchHitBackgroundCurrent: const Color(0xFFffd54a),
|
||||
searchHitForeground: const Color(0xFF1b1d23),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user