panel-to-panel focus traversal via F6 / Shift+F6 (T-105)

Each SlotHost now owns a FocusScopeNode and registers it with
FocusTracker on mount. The render is wrapped in
FocusScope + FocusTraversalGroup so Tab stays within a panel and
slot-level focus is observable.

When a slot's scope gains focus, SlotHost pushes
(slot, activeContributionId) to FocusTracker — this collapses the
parallel-tracker model the consultant flagged. FocusTracker keeps
its setActive surface for explicit callers (palette, etc.) but
slot-scoped tab activation feeds it automatically.

Two new intents, two new bindings:
  FocusNextPanelIntent     → F6
  FocusPreviousPanelIntent → Shift+F6
(VS Code convention; preset YAML.)

The cycle skips slots without a registered scope, so a layout that
hides the context panel doesn't strand focus on a missing target.
Fewer than two registered → no-op.

SlotHost split into a stateful outer (scope + registry) and a
stateless `_SlotBody` (the existing slot-specific rendering),
keeping the build straightforward.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 21:56:35 +02:00
co-authored by Claude Opus 4.7
parent 12e0509fa3
commit 7937da1734
8 changed files with 278 additions and 53 deletions
+15
View File
@@ -19,6 +19,19 @@ library;
import 'package:flutter/widgets.dart';
// -- Panel-to-panel focus traversal -----------------------------------------
/// Move focus to the next panel in `FocusTracker.traversalOrder`
/// (sidebar → workspace → context). Bound to F6 by default.
class FocusNextPanelIntent extends Intent {
const FocusNextPanelIntent();
}
/// Move focus to the previous panel. Bound to Shift+F6 by default.
class FocusPreviousPanelIntent extends Intent {
const FocusPreviousPanelIntent();
}
// -- Command palette --------------------------------------------------------
/// Open the command palette.
@@ -76,6 +89,8 @@ class InvokeCommandIntent extends Intent {
final Map<String, Intent Function()> builtinIntents = {
'activate': () => const ActivateIntent(),
'dismiss': () => const DismissIntent(),
'focus.nextPanel': () => const FocusNextPanelIntent(),
'focus.previousPanel': () => const FocusPreviousPanelIntent(),
'palette.open': () => const PaletteOpenIntent(),
'palette.selectNext': () => const PaletteSelectNextIntent(),
'palette.selectPrevious': () => const PaletteSelectPreviousIntent(),