split lib/app.dart into lib/src/shell/ (T-394)

app.dart was 1187 LOC mixing five concerns. It now keeps ClideApp +
the WidgetsApp root (~60 LOC); the shell moved to lib/src/shell/:

- root_shell.dart — keyboard/intent routing (keymap resolution,
  double-tap modifiers, menu mnemonics), the overlay stack, and the
  welcome overlay
- hat_bar.dart + project_switcher.dart — the window-chrome bar and
  its recents/file-actions dropdown (now in src/shell, not builtin/ —
  they're app chrome, not extension-shaped contributions)
- slot_host.dart — slot mounting, focus-scope integration, the
  per-slot bodies incl. the workspace split + editor drag handle;
  _SlotBody's static title resolver became the shared resolveTabTitle
- layout.dart — the three-column grid, status bar, collapse toggles,
  bottom icon rails

app.dart re-exports RootLayout, SlotHost, StatusbarHost, and
StatusbarCollapseToggle, so every existing import (incl. the three
app-level test files) is unchanged. Pure move + minimal publics
(RootShell, HatBar, ProjectSwitcherButton); full suite green with no
test edits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 02:29:46 +02:00
co-authored by Claude Fable 5
parent 05584eb9e5
commit 7f16016bc3
8 changed files with 1292 additions and 1151 deletions
+101
View File
@@ -0,0 +1,101 @@
/// The top window-chrome bar (D-57): drag region, menu bar, project
/// switcher, window controls. Split out of app.dart (T-394).
library;
import 'dart:io' show Platform;
import 'package:clide/builtin/menubar/menubar.dart';
import 'package:clide/kernel/kernel.dart';
import 'package:clide/src/shell/project_switcher.dart';
import 'package:clide/widgets/widgets.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/widgets.dart';
class HatBar extends StatelessWidget {
const HatBar({super.key, required this.kernel, required this.menuBar});
final KernelServices kernel;
final MenuBarController menuBar;
@override
Widget build(BuildContext context) {
final tokens = ClideTheme.of(context).surface;
return GestureDetector(
onPanStart: (_) => kernel.window.startDrag(),
child: Container(
height: hatHeight,
decoration: BoxDecoration(
color: tokens.chromeBackground,
border: Border(bottom: BorderSide(color: tokens.chromeBorder, width: 1)),
),
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
children: [
_LeftHatContent(tokens: tokens, wc: kernel.window),
MenuBar(controller: menuBar),
Expanded(
child: Center(
child: ProjectSwitcherButton(kernel: kernel, tokens: tokens),
),
),
_RightHatContent(tokens: tokens, wc: kernel.window),
],
),
),
);
}
}
class _LeftHatContent extends StatelessWidget {
const _LeftHatContent({required this.tokens, required this.wc});
final SurfaceTokens tokens;
final WindowControls wc;
@override
Widget build(BuildContext context) {
if (kIsWeb) return const SizedBox.shrink();
// On macOS the native titlebar draws traffic lights; skip duplicates.
return const SizedBox.shrink();
}
}
class _RightHatContent extends StatelessWidget {
const _RightHatContent({required this.tokens, required this.wc});
final SurfaceTokens tokens;
final WindowControls wc;
@override
Widget build(BuildContext context) {
if (kIsWeb) return const SizedBox.shrink();
if (!kIsWeb && Platform.isMacOS) return const SizedBox.shrink();
return Row(
children: [
_WinBtn(icon: const PhosphorIconPainter(0xe32a), onTap: wc.minimize, tokens: tokens),
_WinBtn(icon: const PhosphorIconPainter(0xe45e), onTap: wc.toggleMaximize, tokens: tokens),
_WinBtn(icon: PhosphorIcons.byName('x'), onTap: wc.close, tokens: tokens, isClose: true),
],
);
}
}
class _WinBtn extends StatelessWidget {
const _WinBtn({required this.icon, required this.onTap, required this.tokens, this.isClose = false});
final ClideIconPainter icon;
final VoidCallback onTap;
final SurfaceTokens tokens;
final bool isClose;
@override
Widget build(BuildContext context) {
final hoverBg = isClose ? tokens.windowControlCloseHoverBackground : tokens.listItemHoverBackground;
return ClideTappable(
onTap: onTap,
builder: (context, hovered, _) => Container(
width: 36,
height: hatHeight,
color: hovered ? hoverBg : null,
alignment: Alignment.center,
child: ClideIcon(icon, size: 14, color: hovered && isClose ? tokens.windowControlCloseHoverForeground : tokens.chromeForeground),
),
);
}
}