Files
clide/lib/builtin/shared/reader_chrome.dart
T
jpmschweitzerandClaude Opus 4.8 0eb7b0df2f fix decision first-click + editor reveal via retained reader nav
Two reveal-on-open bugs:

Decisions opened only on the second click (T-196): the detail view
subscribed in didChangeDependencies, which runs after the tab is
revealed, so the broadcast 'selection' that triggered the reveal was
already gone. Hoist the back/forward history out of per-view State into
a retained per-reader ReaderNav (kernel ChangeNotifier in a
ReaderNavRegistry, D-81). The nav records selections, emits 'load' (the
single channel readers display from), and survives mount/unmount — the
reader grabs nav.current on mount, so the first selection lands. Both
the markdown and decisions readers move to this model; the per-view
ReaderHistoryMixin and the markdown post-frame forward hack are gone.

The editor pane never opened (T-197): EditorExtension contributed a
workspace tab but nothing activated it on editor.open. Add an activate()
that reveals the tab on editor.opened / editor.active-changed; the
view's hydrate() pulls the active buffer on mount.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 12:45:48 +02:00

223 lines
6.7 KiB
Dart

/// Shared action-bar chrome for sidebar reader widgets (T-189, T-190, T-191).
///
/// Provides [ReaderActionBar] — the visible action bar: back, forward, pin,
/// edit pencil. Plugs into a [ClidePaneChrome] via its `trailing:` slot. The
/// retained back/forward history that drives it is the kernel `ReaderNav`
/// (one per right-pane reader); this file is just the buttons.
library;
import 'package:clide/kernel/kernel.dart';
import 'package:clide/widgets/widgets.dart';
import 'package:flutter/widgets.dart';
// ---------------------------------------------------------------------------
// Action bar widget
// ---------------------------------------------------------------------------
/// A row of reader-chrome action buttons: back, forward, pin (set/jump), edit.
///
/// Designed to plug into [ClidePaneChrome.trailing]. All callbacks are
/// optional — pass null to hide/disable the corresponding button.
class ReaderActionBar extends StatelessWidget {
const ReaderActionBar({
super.key,
required this.canGoBack,
required this.canGoForward,
required this.hasPinned,
required this.onBack,
required this.onForward,
required this.onPin,
required this.onJumpToPin,
required this.onEdit,
});
final bool canGoBack;
final bool canGoForward;
final bool hasPinned;
final VoidCallback? onBack;
final VoidCallback? onForward;
/// Called when the pin button is tapped (set / replace current pin).
final VoidCallback? onPin;
/// Called when the jump-to-pin affordance is tapped.
final VoidCallback? onJumpToPin;
/// Called when the edit pencil is tapped. Pass null to hide the button.
final VoidCallback? onEdit;
@override
Widget build(BuildContext context) {
final tokens = ClideTheme.of(context).surface;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
_ActionButton(
painter: PhosphorIcons.caretLeft,
tooltip: 'Back',
enabled: canGoBack,
onTap: canGoBack ? onBack : null,
tokens: tokens,
),
const SizedBox(width: 2),
_ActionButton(
painter: PhosphorIcons.caretRight,
tooltip: 'Forward',
enabled: canGoForward,
onTap: canGoForward ? onForward : null,
tokens: tokens,
),
const SizedBox(width: 4),
_PinButton(
hasPinned: hasPinned,
onPin: onPin,
onJumpToPin: onJumpToPin,
tokens: tokens,
),
if (onEdit != null) ...[
const SizedBox(width: 2),
_ActionButton(
painter: PhosphorIcons.pencilSimple,
tooltip: 'Edit in editor',
enabled: true,
onTap: onEdit,
tokens: tokens,
),
],
],
);
}
}
// ---------------------------------------------------------------------------
// Private button widgets
// ---------------------------------------------------------------------------
class _ActionButton extends StatelessWidget {
const _ActionButton({
required this.painter,
required this.tooltip,
required this.enabled,
required this.onTap,
required this.tokens,
});
final ClideIconPainter painter;
final String tooltip;
final bool enabled;
final VoidCallback? onTap;
final SurfaceTokens tokens;
@override
Widget build(BuildContext context) {
return Semantics(
button: true,
label: tooltip,
enabled: enabled,
onTap: onTap,
excludeSemantics: true,
child: ClideTappable(
onTap: onTap,
tooltip: tooltip,
builder: (ctx, hovered, _) => Container(
width: 20,
height: 20,
alignment: Alignment.center,
decoration: BoxDecoration(
color: hovered && enabled ? tokens.sidebarItemHover : null,
borderRadius: BorderRadius.circular(3),
),
child: ClideIcon(
painter,
size: 11,
color: enabled ? tokens.panelHeaderForeground : tokens.globalTextMuted,
),
),
),
);
}
}
/// Pin button: when [hasPinned] is true it shows the pin "filled" and the
/// button activates jump-to-pin; a long-press (or secondary tap) sets a new
/// pin. When [hasPinned] is false the single tap sets the pin.
///
/// For simplicity (and to keep the widget API flat): a single tap ALWAYS sets
/// the pin (replacing the previous one), while the adjacent jump-to-pin uses a
/// separate Tappable rendered as a small indicator badge.
class _PinButton extends StatelessWidget {
const _PinButton({
required this.hasPinned,
required this.onPin,
required this.onJumpToPin,
required this.tokens,
});
final bool hasPinned;
final VoidCallback? onPin;
final VoidCallback? onJumpToPin;
final SurfaceTokens tokens;
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
// The pin-set button (always shown, sets/replaces the pin).
Semantics(
button: true,
label: hasPinned ? 'Replace pin' : 'Pin current',
onTap: onPin,
excludeSemantics: true,
child: ClideTappable(
onTap: onPin,
tooltip: hasPinned ? 'Replace pin' : 'Pin current',
builder: (ctx, hovered, _) => Container(
width: 20,
height: 20,
alignment: Alignment.center,
decoration: BoxDecoration(
color: hovered ? tokens.sidebarItemHover : null,
borderRadius: BorderRadius.circular(3),
),
child: ClideIcon(
PhosphorIcons.link,
size: 11,
color: hasPinned ? tokens.globalFocus : tokens.panelHeaderForeground,
),
),
),
),
// Jump-to-pin affordance — only visible when a pin is set.
if (hasPinned) ...[
const SizedBox(width: 1),
Semantics(
button: true,
label: 'Jump to pin',
onTap: onJumpToPin,
excludeSemantics: true,
child: ClideTappable(
onTap: onJumpToPin,
tooltip: 'Jump to pin',
builder: (ctx, hovered, _) => Container(
width: 16,
height: 16,
alignment: Alignment.center,
decoration: BoxDecoration(
color: hovered ? tokens.sidebarItemHover : tokens.panelBorder,
borderRadius: BorderRadius.circular(2),
),
child: ClideText(
'↩',
fontSize: 9,
color: tokens.globalFocus,
),
),
),
),
],
],
);
}
}