Files
clide/lib/widgets/src/clide_tab_bar.dart
T
jpmschweitzerandClaude Opus 4.6 cf3e7700a3 sweep ClideTappable across 15 files, remove 267 lines
23 StatefulWidget+State hover pairs converted to StatelessWidget
using ClideTappable(builder: (ctx, hovered, _) => ...). Covers
app.dart (5), welcome (2), claude session host (2), column hat (2),
spine (1), pane chrome (1), tab bar (1), decisions (1), tickets (1),
graph (1), pql panel (1), backlinks (1), file tree (2), git panel (1),
git status (1). Only EditorDragHandle and ClidePalette excluded
(drag/keyboard behavior beyond hover+tap).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 11:28:35 +02:00

112 lines
2.9 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,
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),
],
),
);
},
),
);
}
}