Files
clide/lib/widgets/src/clide_tab_bar.dart
T
jpmschweitzerandClaude Opus 4.8 3dd770c0a6 add 1px breathing room above tab strips (T-324)
The tab strips — Claude session tabs (MultitabPane._TabStrip) and the
slot tab bar (ClideTabBar) — sat flush against the chrome above, reading
as cramped. Add a 1px top margin to both Containers so the pane surface
behind shows as a hairline gap, separating the tabBarBackground strip
from the chrome edge. Both are single-use widgets, so the margin applies
once per surface (no double-apply).

Regenerated the linux clide_tab_bar golden. (The macOS variant is now 1px
stale but is dormant on the linux gate; regenerate on macOS.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 13:36:35 +02:00

115 lines
3.1 KiB
Dart

import 'package:clide/kernel/src/theme/controller.dart';
import 'package:clide/widgets/src/clide_icon.dart';
import 'package:clide/widgets/src/clide_tappable.dart';
import 'package:clide/widgets/src/clide_text.dart';
import 'package:flutter/widgets.dart';
@immutable
class ClideTabItem {
const ClideTabItem({
required this.id,
required this.title,
this.icon,
});
final String id;
final String title;
final ClideIconPainter? icon;
}
class ClideTabBar extends StatelessWidget {
const ClideTabBar({
super.key,
required this.items,
required this.activeId,
required this.onSelect,
this.height = 28,
this.semanticContainerLabel,
});
final List<ClideTabItem> items;
final String? activeId;
final ValueChanged<String> onSelect;
final double height;
/// Optional container-level label for screen readers (e.g. "Sidebar tabs").
final String? semanticContainerLabel;
@override
Widget build(BuildContext context) {
final tokens = ClideTheme.of(context).surface;
return Semantics(
container: true,
label: semanticContainerLabel,
explicitChildNodes: true,
child: Container(
height: height,
// A hairline of the surface behind, so the strip doesn't butt against
// the chrome directly above it (T-324).
margin: const EdgeInsets.only(top: 1),
color: tokens.tabBarBackground,
child: Row(
children: [
for (final item in items)
_Tab(
item: item,
active: item.id == activeId,
onTap: () => onSelect(item.id),
),
],
),
),
);
}
}
class _Tab extends StatelessWidget {
const _Tab({required this.item, required this.active, required this.onTap});
final ClideTabItem item;
final bool active;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final tokens = ClideTheme.of(context).surface;
final fg = active ? tokens.tabActiveForeground : tokens.tabInactiveForeground;
return Semantics(
button: true,
selected: active,
label: item.title,
onTap: onTap,
excludeSemantics: true,
child: ClideTappable(
onTap: onTap,
builder: (context, hovered, _) {
final bg = active ? tokens.tabActive : tokens.tabInactive;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: bg,
border: Border(
bottom: BorderSide(
color: active ? tokens.tabActiveBorder : const Color(0x00000000),
width: 2,
),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (item.icon != null) ...[
ClideIcon(item.icon!, size: 12, color: fg),
const SizedBox(width: 6),
],
ClideText(item.title, color: fg),
],
),
);
},
),
);
}
}