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>
This commit is contained in:
2026-04-23 11:28:35 +02:00
co-authored by Claude Opus 4.6
parent fbd5f1ae04
commit cf3e7700a3
15 changed files with 416 additions and 683 deletions
+72 -129
View File
@@ -290,105 +290,72 @@ class _RightHatContent extends StatelessWidget {
} }
} }
class _TrafficDot extends StatefulWidget { class _TrafficDot extends StatelessWidget {
const _TrafficDot({required this.color, required this.onTap}); const _TrafficDot({required this.color, required this.onTap});
final Color color; final Color color;
final VoidCallback onTap; final VoidCallback onTap;
@override
State<_TrafficDot> createState() => _TrafficDotState();
}
class _TrafficDotState extends State<_TrafficDot> {
bool _hover = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MouseRegion( return ClideTappable(
onEnter: (_) => setState(() => _hover = true), onTap: onTap,
onExit: (_) => setState(() => _hover = false), builder: (context, hovered, _) => Container(
child: GestureDetector( width: 12, height: 12,
behavior: HitTestBehavior.opaque, decoration: BoxDecoration(color: hovered ? color : color.withAlpha(0xCC), shape: BoxShape.circle),
onTap: widget.onTap,
child: Container(
width: 12, height: 12,
decoration: BoxDecoration(color: _hover ? widget.color : widget.color.withAlpha(0xCC), shape: BoxShape.circle),
),
), ),
); );
} }
} }
class _WinBtn extends StatefulWidget { class _WinBtn extends StatelessWidget {
const _WinBtn({required this.icon, required this.onTap, required this.tokens, this.isClose = false}); const _WinBtn({required this.icon, required this.onTap, required this.tokens, this.isClose = false});
final ClideIconPainter icon; final ClideIconPainter icon;
final VoidCallback onTap; final VoidCallback onTap;
final SurfaceTokens tokens; final SurfaceTokens tokens;
final bool isClose; final bool isClose;
@override
State<_WinBtn> createState() => _WinBtnState();
}
class _WinBtnState extends State<_WinBtn> {
bool _hover = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final hoverBg = widget.isClose ? const Color(0xFFE81123) : widget.tokens.listItemHoverBackground; final hoverBg = isClose ? const Color(0xFFE81123) : tokens.listItemHoverBackground;
return MouseRegion( return ClideTappable(
onEnter: (_) => setState(() => _hover = true), onTap: onTap,
onExit: (_) => setState(() => _hover = false), builder: (context, hovered, _) => Container(
child: GestureDetector( width: 36, height: hatHeight,
behavior: HitTestBehavior.opaque, color: hovered ? hoverBg : null,
onTap: widget.onTap, alignment: Alignment.center,
child: Container( child: ClideIcon(icon, size: 14, color: hovered && isClose ? const Color(0xFFFFFFFF) : tokens.chromeForeground),
width: 36, height: hatHeight,
color: _hover ? hoverBg : null,
alignment: Alignment.center,
child: ClideIcon(widget.icon, size: 14, color: _hover && widget.isClose ? const Color(0xFFFFFFFF) : widget.tokens.chromeForeground),
),
), ),
); );
} }
} }
class _ProjectSwitcherButton extends StatefulWidget { class _ProjectSwitcherButton extends StatelessWidget {
const _ProjectSwitcherButton({required this.kernel, required this.tokens}); const _ProjectSwitcherButton({required this.kernel, required this.tokens});
final KernelServices kernel; final KernelServices kernel;
final SurfaceTokens tokens; final SurfaceTokens tokens;
@override
State<_ProjectSwitcherButton> createState() => _ProjectSwitcherButtonState();
}
class _ProjectSwitcherButtonState extends State<_ProjectSwitcherButton> {
bool _hover = false;
void _openSwitcher() { void _openSwitcher() {
widget.kernel.dialog.show<String>((ctx, dismiss) { kernel.dialog.show<String>((ctx, dismiss) {
return _ProjectSwitcherDropdown(kernel: widget.kernel, onDismiss: dismiss); return _ProjectSwitcherDropdown(kernel: kernel, onDismiss: dismiss);
}); });
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListenableBuilder( return ListenableBuilder(
listenable: widget.kernel.project, listenable: kernel.project,
builder: (ctx, _) { builder: (ctx, _) {
final name = widget.kernel.project.current?.path.split('/').last; final name = kernel.project.current?.path.split('/').last;
final label = name != null ? 'clide > $name' : 'clide'; final label = name != null ? 'clide > $name' : 'clide';
return MouseRegion( return ClideTappable(
cursor: SystemMouseCursors.click, onTap: _openSwitcher,
onEnter: (_) => setState(() => _hover = true), builder: (context, hovered, _) => Row(
onExit: (_) => setState(() => _hover = false), mainAxisSize: MainAxisSize.min,
child: GestureDetector( children: [
behavior: HitTestBehavior.opaque, ClideText(label, fontSize: 12, color: hovered ? tokens.globalForeground : tokens.chromeForeground, fontFamily: clideMonoFamily),
onTap: _openSwitcher, const SizedBox(width: 4),
child: Row( ClideIcon(PhosphorIcons.caretDown, size: 8, color: tokens.chromeForeground),
mainAxisSize: MainAxisSize.min, ],
children: [
ClideText(label, fontSize: 12, color: _hover ? widget.tokens.globalForeground : widget.tokens.chromeForeground, fontFamily: clideMonoFamily),
const SizedBox(width: 4),
ClideIcon(PhosphorIcons.caretDown, size: 8, color: widget.tokens.chromeForeground),
],
),
), ),
); );
}, },
@@ -523,95 +490,71 @@ class _ProjectSwitcherDropdownState extends State<_ProjectSwitcherDropdown> {
} }
} }
class _RecentProjectRow extends StatefulWidget { class _RecentProjectRow extends StatelessWidget {
const _RecentProjectRow({required this.project, required this.tokens, required this.onTap}); const _RecentProjectRow({required this.project, required this.tokens, required this.onTap});
final RecentProject project; final RecentProject project;
final SurfaceTokens tokens; final SurfaceTokens tokens;
final VoidCallback onTap; final VoidCallback onTap;
@override
State<_RecentProjectRow> createState() => _RecentProjectRowState();
}
class _RecentProjectRowState extends State<_RecentProjectRow> {
bool _hover = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MouseRegion( return ClideTappable(
cursor: SystemMouseCursors.click, onTap: onTap,
onEnter: (_) => setState(() => _hover = true), builder: (context, hovered, _) => Container(
onExit: (_) => setState(() => _hover = false), color: hovered ? tokens.listItemHoverBackground : null,
child: GestureDetector( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
onTap: widget.onTap, child: Row(
child: Container( children: [
color: _hover ? widget.tokens.listItemHoverBackground : null, ClideIcon(PhosphorIcons.folder, size: 14, color: tokens.globalTextMuted),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), const SizedBox(width: 8),
child: Row( Expanded(
children: [ child: Column(
ClideIcon(PhosphorIcons.folder, size: 14, color: widget.tokens.globalTextMuted), crossAxisAlignment: CrossAxisAlignment.start,
const SizedBox(width: 8), children: [
Expanded( ClideText(project.name, fontSize: 14),
child: Column( if (project.branch != null)
crossAxisAlignment: CrossAxisAlignment.start, Row(
children: [ children: [
ClideText(widget.project.name, fontSize: 14), ClideText(project.relativePath, muted: true, fontSize: 12, fontFamily: clideMonoFamily),
if (widget.project.branch != null) ClideText(' · ', muted: true, fontSize: 12),
Row( ClideIcon(PhosphorIcons.gitBranch, size: 10, color: tokens.globalTextMuted),
children: [ const SizedBox(width: 3),
ClideText(widget.project.relativePath, muted: true, fontSize: 12, fontFamily: clideMonoFamily), ClideText(project.branch!, muted: true, fontSize: 12, fontFamily: clideMonoFamily),
ClideText(' · ', muted: true, fontSize: 12), ],
ClideIcon(PhosphorIcons.gitBranch, size: 10, color: widget.tokens.globalTextMuted), )
const SizedBox(width: 3), else
ClideText(widget.project.branch!, muted: true, fontSize: 12, fontFamily: clideMonoFamily), ClideText(project.relativePath, muted: true, fontSize: 12, fontFamily: clideMonoFamily),
], ],
)
else
ClideText(widget.project.relativePath, muted: true, fontSize: 12, fontFamily: clideMonoFamily),
],
),
), ),
ClideText(widget.project.timeAgo, muted: true, fontSize: 11), ),
], ClideText(project.timeAgo, muted: true, fontSize: 11),
), ],
), ),
), ),
); );
} }
} }
class _ActionRow extends StatefulWidget { class _ActionRow extends StatelessWidget {
const _ActionRow({required this.label, this.shortcut, required this.tokens, required this.onTap}); const _ActionRow({required this.label, this.shortcut, required this.tokens, required this.onTap});
final String label; final String label;
final String? shortcut; final String? shortcut;
final SurfaceTokens tokens; final SurfaceTokens tokens;
final VoidCallback onTap; final VoidCallback onTap;
@override
State<_ActionRow> createState() => _ActionRowState();
}
class _ActionRowState extends State<_ActionRow> {
bool _hover = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MouseRegion( return ClideTappable(
cursor: SystemMouseCursors.click, onTap: onTap,
onEnter: (_) => setState(() => _hover = true), builder: (context, hovered, _) => Container(
onExit: (_) => setState(() => _hover = false), color: hovered ? tokens.listItemHoverBackground : null,
child: GestureDetector( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
onTap: widget.onTap, child: Row(
child: Container( children: [
color: _hover ? widget.tokens.listItemHoverBackground : null, Expanded(child: ClideText(label, fontSize: 14)),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), if (shortcut != null && shortcut!.isNotEmpty)
child: Row( ClideText(shortcut!, fontSize: 12, color: tokens.globalTextMuted, fontFamily: clideMonoFamily),
children: [ ],
Expanded(child: ClideText(widget.label, fontSize: 14)),
if (widget.shortcut != null && widget.shortcut!.isNotEmpty)
ClideText(widget.shortcut!, fontSize: 12, color: widget.tokens.globalTextMuted, fontFamily: clideMonoFamily),
],
),
), ),
), ),
); );
+31 -54
View File
@@ -120,7 +120,7 @@ class _TabRow extends StatelessWidget {
} }
} }
class _Tab extends StatefulWidget { class _Tab extends StatelessWidget {
const _Tab({required this.session, required this.active, required this.tokens, required this.onTap, this.onClose}); const _Tab({required this.session, required this.active, required this.tokens, required this.onTap, this.onClose});
final _Session session; final _Session session;
final bool active; final bool active;
@@ -128,75 +128,52 @@ class _Tab extends StatefulWidget {
final VoidCallback onTap; final VoidCallback onTap;
final VoidCallback? onClose; final VoidCallback? onClose;
@override
State<_Tab> createState() => _TabState();
}
class _TabState extends State<_Tab> {
bool _hovered = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MouseRegion( return ClideTappable(
cursor: SystemMouseCursors.click, onTap: onTap,
onEnter: (_) => setState(() => _hovered = true), builder: (context, hovered, _) => Container(
onExit: (_) => setState(() => _hovered = false), padding: const EdgeInsets.symmetric(horizontal: 10),
child: GestureDetector( decoration: BoxDecoration(
onTap: widget.onTap, color: hovered && !active ? tokens.tabInactive : null,
child: Container( border: Border(bottom: BorderSide(color: active ? tokens.tabActiveBorder : const Color(0x00000000), width: 2)),
padding: const EdgeInsets.symmetric(horizontal: 10), ),
decoration: BoxDecoration( child: Row(
color: _hovered && !widget.active ? widget.tokens.tabInactive : null, mainAxisSize: MainAxisSize.min,
border: Border(bottom: BorderSide(color: widget.active ? widget.tokens.tabActiveBorder : const Color(0x00000000), width: 2)), children: [
), ClideText(
child: Row( session.label,
mainAxisSize: MainAxisSize.min, fontSize: 12,
children: [ color: active ? tokens.tabActiveForeground : tokens.tabInactiveForeground,
ClideText( fontFamily: clideMonoFamily,
widget.session.label, ),
fontSize: 12, if (onClose != null) ...[
color: widget.active ? widget.tokens.tabActiveForeground : widget.tokens.tabInactiveForeground, const SizedBox(width: 6),
fontFamily: clideMonoFamily, GestureDetector(
onTap: onClose,
child: ClideIcon(PhosphorIcons.xMark, size: 10, color: hovered ? tokens.globalForeground : tokens.globalTextMuted),
), ),
if (widget.onClose != null) ...[
const SizedBox(width: 6),
GestureDetector(
onTap: widget.onClose,
child: ClideIcon(PhosphorIcons.xMark, size: 10, color: _hovered ? widget.tokens.globalForeground : widget.tokens.globalTextMuted),
),
],
], ],
), ],
), ),
), ),
); );
} }
} }
class _AddButton extends StatefulWidget { class _AddButton extends StatelessWidget {
const _AddButton({required this.tokens, required this.onTap}); const _AddButton({required this.tokens, required this.onTap});
final SurfaceTokens tokens; final SurfaceTokens tokens;
final VoidCallback onTap; final VoidCallback onTap;
@override
State<_AddButton> createState() => _AddButtonState();
}
class _AddButtonState extends State<_AddButton> {
bool _hovered = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MouseRegion( return ClideTappable(
cursor: SystemMouseCursors.click, onTap: onTap,
onEnter: (_) => setState(() => _hovered = true), tooltip: 'New session',
onExit: (_) => setState(() => _hovered = false), builder: (context, hovered, _) => Padding(
child: GestureDetector( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
onTap: widget.onTap, child: ClideText('+', fontSize: 14, color: hovered ? tokens.globalForeground : tokens.globalTextMuted),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: ClideText('+', fontSize: 14, color: _hovered ? widget.tokens.globalForeground : widget.tokens.globalTextMuted),
),
), ),
); );
} }
+6 -15
View File
@@ -102,31 +102,22 @@ class _DecisionEntry {
); );
} }
class _DecisionRow extends StatefulWidget { class _DecisionRow extends StatelessWidget {
const _DecisionRow({required this.entry, required this.tokens}); const _DecisionRow({required this.entry, required this.tokens});
final _DecisionEntry entry; final _DecisionEntry entry;
final SurfaceTokens tokens; final SurfaceTokens tokens;
@override
State<_DecisionRow> createState() => _DecisionRowState();
}
class _DecisionRowState extends State<_DecisionRow> {
bool _hovered = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MouseRegion( return ClideTappable(
onEnter: (_) => setState(() => _hovered = true), builder: (context, hovered, _) => Container(
onExit: (_) => setState(() => _hovered = false), color: hovered ? tokens.listItemHoverBackground : null,
child: Container(
color: _hovered ? widget.tokens.listItemHoverBackground : null,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
child: Row( child: Row(
children: [ children: [
ClideText(widget.entry.id, color: widget.tokens.globalTextMuted, fontSize: 12), ClideText(entry.id, color: tokens.globalTextMuted, fontSize: 12),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded(child: ClideText(widget.entry.title, fontSize: 13)), Expanded(child: ClideText(entry.title, fontSize: 13)),
], ],
), ),
), ),
+35 -60
View File
@@ -225,7 +225,7 @@ class _FileRow extends StatelessWidget {
} }
} }
class _Row extends StatefulWidget { class _Row extends StatelessWidget {
const _Row({ const _Row({
required this.depth, required this.depth,
required this.onTap, required this.onTap,
@@ -240,81 +240,56 @@ class _Row extends StatefulWidget {
final Widget? leading; final Widget? leading;
final bool rotateLeading; final bool rotateLeading;
@override
State<_Row> createState() => _RowState();
}
class _RowState extends State<_Row> {
bool _hover = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tokens = ClideTheme.of(context).surface; final tokens = ClideTheme.of(context).surface;
final leftPadding = 8.0 + (widget.depth * 14.0); final leftPadding = 8.0 + (depth * 14.0);
return MouseRegion( return ClideTappable(
cursor: SystemMouseCursors.click, onTap: onTap,
onEnter: (_) => setState(() => _hover = true), builder: (context, hovered, _) => Container(
onExit: (_) => setState(() => _hover = false), color: hovered ? tokens.sidebarItemHover : null,
child: GestureDetector( padding: EdgeInsets.only(left: leftPadding, right: 8, top: 3, bottom: 3),
behavior: HitTestBehavior.opaque, child: Row(
onTap: widget.onTap, children: [
child: Container( if (leading != null) ...[
color: _hover ? tokens.sidebarItemHover : null, Transform.rotate(
padding: EdgeInsets.only(left: leftPadding, right: 8, top: 3, bottom: 3), angle: rotateLeading ? 1.5708 : 0, // 90° when expanded
child: Row( child: leading,
children: [
if (widget.leading != null) ...[
Transform.rotate(
angle: widget.rotateLeading ? 1.5708 : 0, // 90° when expanded
child: widget.leading,
),
const SizedBox(width: 6),
] else
const SizedBox(width: 16),
Expanded(
child: ClideText(
widget.label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
color: tokens.sidebarForeground,
),
), ),
], const SizedBox(width: 6),
), ] else
const SizedBox(width: 16),
Expanded(
child: ClideText(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
color: tokens.sidebarForeground,
),
),
],
), ),
), ),
); );
} }
} }
class _FilteredFileRow extends StatefulWidget { class _FilteredFileRow extends StatelessWidget {
const _FilteredFileRow({required this.entry}); const _FilteredFileRow({required this.entry});
final FileEntry entry; final FileEntry entry;
@override
State<_FilteredFileRow> createState() => _FilteredFileRowState();
}
class _FilteredFileRowState extends State<_FilteredFileRow> {
bool _hover = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tokens = ClideTheme.of(context).surface; final tokens = ClideTheme.of(context).surface;
return MouseRegion( return ClideTappable(
cursor: SystemMouseCursors.click, onTap: () {
onEnter: (_) => setState(() => _hover = true), final kernel = ClideKernel.of(context);
onExit: (_) => setState(() => _hover = false), unawaited(kernel.ipc.request('editor.open', args: {'path': entry.path}));
child: GestureDetector( },
behavior: HitTestBehavior.opaque, builder: (context, hovered, _) => Container(
onTap: () { color: hovered ? tokens.sidebarItemHover : null,
final kernel = ClideKernel.of(context); padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 3),
unawaited(kernel.ipc.request('editor.open', args: {'path': widget.entry.path})); child: ClideText(entry.path, maxLines: 1, overflow: TextOverflow.ellipsis, color: tokens.sidebarForeground),
},
child: Container(
color: _hover ? tokens.sidebarItemHover : null,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 3),
child: ClideText(widget.entry.path, maxLines: 1, overflow: TextOverflow.ellipsis, color: tokens.sidebarForeground),
),
), ),
); );
} }
+46 -59
View File
@@ -336,7 +336,7 @@ class _FileGroup extends StatelessWidget {
} }
} }
class _GitFileRow extends StatefulWidget { class _GitFileRow extends StatelessWidget {
const _GitFileRow({ const _GitFileRow({
required this.entry, required this.entry,
this.onStage, this.onStage,
@@ -349,78 +349,65 @@ class _GitFileRow extends StatefulWidget {
final void Function(String path)? onUnstage; final void Function(String path)? onUnstage;
final void Function(String path)? onDiscard; final void Function(String path)? onDiscard;
@override
State<_GitFileRow> createState() => _GitFileRowState();
}
class _GitFileRowState extends State<_GitFileRow> {
bool _hover = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tokens = ClideTheme.of(context).surface; final tokens = ClideTheme.of(context).surface;
final path = widget.entry['path'] as String? ?? ''; final path = entry['path'] as String? ?? '';
final name = path.split('/').last; final name = path.split('/').last;
final indexState = widget.entry['indexState'] as String?; final indexState = entry['indexState'] as String?;
final workTreeState = widget.entry['workTreeState'] as String?; final workTreeState = entry['workTreeState'] as String?;
final state = indexState ?? workTreeState ?? ''; final state = indexState ?? workTreeState ?? '';
final stateLabel = _stateLabel(state); final stateLabel = _stateLabel(state);
return MouseRegion( return Semantics(
cursor: SystemMouseCursors.click, button: true,
onEnter: (_) => setState(() => _hover = true), label: '$name $stateLabel',
onExit: (_) => setState(() => _hover = false), child: ClideTappable(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () { onTap: () {
final kernel = ClideKernel.of(context); final kernel = ClideKernel.of(context);
unawaited(kernel.ipc.request('editor.open', args: {'path': path})); unawaited(kernel.ipc.request('editor.open', args: {'path': path}));
}, },
child: Semantics( builder: (context, hovered, _) => Container(
button: true, color: hovered ? tokens.sidebarItemHover : null,
label: '$name $stateLabel', padding: const EdgeInsets.only(
child: Container( left: 20, right: 8, top: 2, bottom: 2),
color: _hover ? tokens.sidebarItemHover : null, child: Row(
padding: const EdgeInsets.only( children: [
left: 20, right: 8, top: 2, bottom: 2), ClideText(
child: Row( _stateIndicator(state),
children: [ fontSize: clideFontCaption,
ClideText( color: _stateColor(state, tokens),
_stateIndicator(state), ),
fontSize: clideFontCaption, const SizedBox(width: 6),
color: _stateColor(state, tokens), Expanded(
child: ClideText(
name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
color: tokens.sidebarForeground,
), ),
const SizedBox(width: 6), ),
Expanded( if (hovered) ...[
child: ClideText( if (onStage != null)
name, _SmallAction(
maxLines: 1, label: '+',
overflow: TextOverflow.ellipsis, semanticsLabel: 'stage $name',
color: tokens.sidebarForeground, onTap: () => onStage!(path),
),
if (onUnstage != null)
_SmallAction(
label: '-',
semanticsLabel: 'unstage $name',
onTap: () => onUnstage!(path),
),
if (onDiscard != null)
_SmallAction(
label: 'x',
semanticsLabel: 'discard changes to $name',
onTap: () => onDiscard!(path),
), ),
),
if (_hover) ...[
if (widget.onStage != null)
_SmallAction(
label: '+',
semanticsLabel: 'stage $name',
onTap: () => widget.onStage!(path),
),
if (widget.onUnstage != null)
_SmallAction(
label: '-',
semanticsLabel: 'unstage $name',
onTap: () => widget.onUnstage!(path),
),
if (widget.onDiscard != null)
_SmallAction(
label: 'x',
semanticsLabel: 'discard changes to $name',
onTap: () => widget.onDiscard!(path),
),
],
], ],
), ],
), ),
), ),
), ),
+29 -41
View File
@@ -228,7 +228,7 @@ class _BranchPickerState extends State<_BranchPicker> {
} }
} }
class _BranchRow extends StatefulWidget { class _BranchRow extends StatelessWidget {
const _BranchRow({ const _BranchRow({
required this.name, required this.name,
required this.current, required this.current,
@@ -239,51 +239,39 @@ class _BranchRow extends StatefulWidget {
final bool current; final bool current;
final VoidCallback? onTap; final VoidCallback? onTap;
@override
State<_BranchRow> createState() => _BranchRowState();
}
class _BranchRowState extends State<_BranchRow> {
bool _hover = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tokens = ClideTheme.of(context).surface; final tokens = ClideTheme.of(context).surface;
return MouseRegion( return ClideTappable(
cursor: onTap: onTap,
widget.onTap != null ? SystemMouseCursors.click : MouseCursor.defer, cursor: onTap != null ? SystemMouseCursors.click : MouseCursor.defer,
onEnter: (_) => setState(() => _hover = true), builder: (context, hovered, _) => Container(
onExit: (_) => setState(() => _hover = false), color: hovered ? tokens.listItemHoverBackground : null,
child: GestureDetector( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
onTap: widget.onTap, child: Row(
child: Container( children: [
color: _hover ? tokens.listItemHoverBackground : null, if (current)
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), Padding(
child: Row( padding: const EdgeInsets.only(right: 8),
children: [ child: ClideIcon(
if (widget.current) const CheckIcon(),
Padding( size: 12,
padding: const EdgeInsets.only(right: 8), color: tokens.statusSuccess,
child: ClideIcon(
const CheckIcon(),
size: 12,
color: tokens.statusSuccess,
),
)
else
const SizedBox(width: 20),
Expanded(
child: ClideText(
widget.name,
fontFamily: clideMonoFamily,
fontSize: clideFontMono,
color: widget.current
? tokens.globalForeground
: tokens.listItemForeground,
), ),
)
else
const SizedBox(width: 20),
Expanded(
child: ClideText(
name,
fontFamily: clideMonoFamily,
fontSize: clideFontMono,
color: current
? tokens.globalForeground
: tokens.listItemForeground,
), ),
], ),
), ],
), ),
), ),
); );
+6 -15
View File
@@ -90,30 +90,21 @@ class _GraphNode {
); );
} }
class _NodeRow extends StatefulWidget { class _NodeRow extends StatelessWidget {
const _NodeRow({required this.node, required this.tokens}); const _NodeRow({required this.node, required this.tokens});
final _GraphNode node; final _GraphNode node;
final SurfaceTokens tokens; final SurfaceTokens tokens;
@override
State<_NodeRow> createState() => _NodeRowState();
}
class _NodeRowState extends State<_NodeRow> {
bool _hovered = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MouseRegion( return ClideTappable(
onEnter: (_) => setState(() => _hovered = true), builder: (context, hovered, _) => Container(
onExit: (_) => setState(() => _hovered = false), color: hovered ? tokens.listItemHoverBackground : null,
child: Container(
color: _hovered ? widget.tokens.listItemHoverBackground : null,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
child: Row( child: Row(
children: [ children: [
Expanded(child: ClideText(widget.node.path, fontSize: 13)), Expanded(child: ClideText(node.path, fontSize: 13)),
ClideText('${widget.node.inbound}in ${widget.node.outbound}out', color: widget.tokens.globalTextMuted, fontSize: 11), ClideText('${node.inbound}in ${node.outbound}out', color: tokens.globalTextMuted, fontSize: 11),
], ],
), ),
), ),
+18 -31
View File
@@ -140,31 +140,22 @@ class _LinkGroup extends StatelessWidget {
} }
} }
class _LinkRow extends StatefulWidget { class _LinkRow extends StatelessWidget {
const _LinkRow({required this.link, required this.pathKey}); const _LinkRow({required this.link, required this.pathKey});
final Map<String, Object?> link; final Map<String, Object?> link;
final String pathKey; final String pathKey;
@override
State<_LinkRow> createState() => _LinkRowState();
}
class _LinkRowState extends State<_LinkRow> {
bool _hover = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tokens = ClideTheme.of(context).surface; final tokens = ClideTheme.of(context).surface;
final target = widget.link[widget.pathKey] as String? ?? ''; final target = link[pathKey] as String? ?? '';
final alias = widget.link['alias'] as String?; final alias = link['alias'] as String?;
final display = alias ?? target; final display = alias ?? target;
return MouseRegion( return Semantics(
cursor: SystemMouseCursors.click, button: true,
onEnter: (_) => setState(() => _hover = true), label: target,
onExit: (_) => setState(() => _hover = false), child: ClideTappable(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () { onTap: () {
if (!target.startsWith('http')) { if (!target.startsWith('http')) {
final kernel = ClideKernel.of(context); final kernel = ClideKernel.of(context);
@@ -172,21 +163,17 @@ class _LinkRowState extends State<_LinkRow> {
kernel.ipc.request('editor.open', args: {'path': target})); kernel.ipc.request('editor.open', args: {'path': target}));
} }
}, },
child: Semantics( builder: (context, hovered, _) => Container(
button: true, color: hovered ? tokens.sidebarItemHover : null,
label: target, padding:
child: Container( const EdgeInsets.symmetric(horizontal: 20, vertical: 2),
color: _hover ? tokens.sidebarItemHover : null, child: ClideText(
padding: display,
const EdgeInsets.symmetric(horizontal: 20, vertical: 2), maxLines: 1,
child: ClideText( overflow: TextOverflow.ellipsis,
display, color: target.startsWith('http')
maxLines: 1, ? tokens.statusInfo
overflow: TextOverflow.ellipsis, : tokens.sidebarForeground,
color: target.startsWith('http')
? tokens.statusInfo
: tokens.sidebarForeground,
),
), ),
), ),
), ),
+15 -28
View File
@@ -155,45 +155,32 @@ class _ViewTabs extends StatelessWidget {
}; };
} }
class _FileRow extends StatefulWidget { class _FileRow extends StatelessWidget {
const _FileRow({required this.entry}); const _FileRow({required this.entry});
final Map<String, Object?> entry; final Map<String, Object?> entry;
@override
State<_FileRow> createState() => _FileRowState();
}
class _FileRowState extends State<_FileRow> {
bool _hover = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tokens = ClideTheme.of(context).surface; final tokens = ClideTheme.of(context).surface;
final path = widget.entry['path'] as String? ?? ''; final path = entry['path'] as String? ?? '';
final name = widget.entry['name'] as String? ?? path.split('/').last; final name = entry['name'] as String? ?? path.split('/').last;
return MouseRegion( return Semantics(
cursor: SystemMouseCursors.click, button: true,
onEnter: (_) => setState(() => _hover = true), label: 'Open $name',
onExit: (_) => setState(() => _hover = false), child: ClideTappable(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () { onTap: () {
final kernel = ClideKernel.of(context); final kernel = ClideKernel.of(context);
unawaited( unawaited(
kernel.ipc.request('editor.open', args: {'path': path})); kernel.ipc.request('editor.open', args: {'path': path}));
}, },
child: Semantics( builder: (context, hovered, _) => Container(
button: true, color: hovered ? tokens.sidebarItemHover : null,
label: 'Open $name', padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 3),
child: Container( child: ClideText(
color: _hover ? tokens.sidebarItemHover : null, path,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 3), maxLines: 1,
child: ClideText( overflow: TextOverflow.ellipsis,
path, color: tokens.sidebarForeground,
maxLines: 1,
overflow: TextOverflow.ellipsis,
color: tokens.sidebarForeground,
),
), ),
), ),
), ),
+11 -20
View File
@@ -102,35 +102,26 @@ class _TicketEntry {
); );
} }
class _TicketRow extends StatefulWidget { class _TicketRow extends StatelessWidget {
const _TicketRow({required this.entry, required this.tokens}); const _TicketRow({required this.entry, required this.tokens});
final _TicketEntry entry; final _TicketEntry entry;
final SurfaceTokens tokens; final SurfaceTokens tokens;
@override
State<_TicketRow> createState() => _TicketRowState();
}
class _TicketRowState extends State<_TicketRow> {
bool _hovered = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final statusColor = switch (widget.entry.status) { final statusColor = switch (entry.status) {
'done' => widget.tokens.statusSuccess, 'done' => tokens.statusSuccess,
'in_progress' => widget.tokens.statusInfo, 'in_progress' => tokens.statusInfo,
'cancelled' => widget.tokens.statusError, 'cancelled' => tokens.statusError,
_ => widget.tokens.globalTextMuted, _ => tokens.globalTextMuted,
}; };
return MouseRegion( return ClideTappable(
onEnter: (_) => setState(() => _hovered = true), builder: (context, hovered, _) => Container(
onExit: (_) => setState(() => _hovered = false), color: hovered ? tokens.listItemHoverBackground : null,
child: Container(
color: _hovered ? widget.tokens.listItemHoverBackground : null,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
child: Row( child: Row(
children: [ children: [
ClideText(widget.entry.id, color: widget.tokens.globalTextMuted, fontSize: 12), ClideText(entry.id, color: tokens.globalTextMuted, fontSize: 12),
const SizedBox(width: 6), const SizedBox(width: 6),
Container( Container(
width: 6, width: 6,
@@ -138,7 +129,7 @@ class _TicketRowState extends State<_TicketRow> {
decoration: BoxDecoration(color: statusColor, shape: BoxShape.circle), decoration: BoxDecoration(color: statusColor, shape: BoxShape.circle),
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
Expanded(child: ClideText(widget.entry.title, fontSize: 13)), Expanded(child: ClideText(entry.title, fontSize: 13)),
], ],
), ),
), ),
+48 -72
View File
@@ -129,7 +129,7 @@ class _StartColumn extends StatelessWidget {
} }
} }
class _ActionRow extends StatefulWidget { class _ActionRow extends StatelessWidget {
const _ActionRow({required this.icon, required this.label, this.shortcut, required this.tokens, required this.onTap}); const _ActionRow({required this.icon, required this.label, this.shortcut, required this.tokens, required this.onTap});
final ClideIconPainter icon; final ClideIconPainter icon;
final String label; final String label;
@@ -137,36 +137,24 @@ class _ActionRow extends StatefulWidget {
final SurfaceTokens tokens; final SurfaceTokens tokens;
final VoidCallback onTap; final VoidCallback onTap;
@override
State<_ActionRow> createState() => _ActionRowState();
}
class _ActionRowState extends State<_ActionRow> {
bool _hover = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MouseRegion( return ClideTappable(
cursor: SystemMouseCursors.click, onTap: onTap,
onEnter: (_) => setState(() => _hover = true), builder: (context, hovered, _) => Container(
onExit: (_) => setState(() => _hover = false), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: GestureDetector( decoration: BoxDecoration(
onTap: widget.onTap, color: hovered ? tokens.listItemHoverBackground : null,
child: Container( borderRadius: BorderRadius.circular(4),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), ),
decoration: BoxDecoration( child: Row(
color: _hover ? widget.tokens.listItemHoverBackground : null, children: [
borderRadius: BorderRadius.circular(4), ClideIcon(icon, size: 18, color: tokens.globalTextMuted),
), const SizedBox(width: 14),
child: Row( Expanded(child: ClideText(label, fontSize: 15, color: tokens.globalForeground)),
children: [ if (shortcut != null)
ClideIcon(widget.icon, size: 18, color: widget.tokens.globalTextMuted), ClideText(shortcut!, fontSize: 13, color: tokens.globalTextMuted, fontFamily: clideMonoFamily),
const SizedBox(width: 14), ],
Expanded(child: ClideText(widget.label, fontSize: 15, color: widget.tokens.globalForeground)),
if (widget.shortcut != null)
ClideText(widget.shortcut!, fontSize: 13, color: widget.tokens.globalTextMuted, fontFamily: clideMonoFamily),
],
),
), ),
), ),
); );
@@ -207,58 +195,46 @@ class _RecentColumn extends StatelessWidget {
} }
} }
class _RecentRow extends StatefulWidget { class _RecentRow extends StatelessWidget {
const _RecentRow({required this.project, required this.tokens, required this.onTap}); const _RecentRow({required this.project, required this.tokens, required this.onTap});
final RecentProject project; final RecentProject project;
final SurfaceTokens tokens; final SurfaceTokens tokens;
final VoidCallback onTap; final VoidCallback onTap;
@override
State<_RecentRow> createState() => _RecentRowState();
}
class _RecentRowState extends State<_RecentRow> {
bool _hover = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MouseRegion( return ClideTappable(
cursor: SystemMouseCursors.click, onTap: onTap,
onEnter: (_) => setState(() => _hover = true), builder: (context, hovered, _) => Container(
onExit: (_) => setState(() => _hover = false), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: GestureDetector( decoration: BoxDecoration(
onTap: widget.onTap, color: hovered ? tokens.listItemHoverBackground : null,
child: Container( borderRadius: BorderRadius.circular(4),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), ),
decoration: BoxDecoration( child: Row(
color: _hover ? widget.tokens.listItemHoverBackground : null, children: [
borderRadius: BorderRadius.circular(4), Expanded(
), child: Column(
child: Row( crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded( ClideText(project.name, fontSize: 15, fontWeight: FontWeight.w500),
child: Column( const SizedBox(height: 3),
crossAxisAlignment: CrossAxisAlignment.start, Row(
children: [ children: [
ClideText(widget.project.name, fontSize: 15, fontWeight: FontWeight.w500), Flexible(child: ClideText(project.relativePath, muted: true, fontSize: 13, fontFamily: clideMonoFamily, maxLines: 1, overflow: TextOverflow.ellipsis)),
const SizedBox(height: 3), if (project.branch != null) ...[
Row( ClideText(' · ', muted: true, fontSize: 13),
children: [ ClideIcon(PhosphorIcons.gitBranch, size: 11, color: tokens.globalTextMuted),
Flexible(child: ClideText(widget.project.relativePath, muted: true, fontSize: 13, fontFamily: clideMonoFamily, maxLines: 1, overflow: TextOverflow.ellipsis)), const SizedBox(width: 3),
if (widget.project.branch != null) ...[ ClideText(project.branch!, muted: true, fontSize: 13, fontFamily: clideMonoFamily),
ClideText(' · ', muted: true, fontSize: 13),
ClideIcon(PhosphorIcons.gitBranch, size: 11, color: widget.tokens.globalTextMuted),
const SizedBox(width: 3),
ClideText(widget.project.branch!, muted: true, fontSize: 13, fontFamily: clideMonoFamily),
],
], ],
), ],
], ),
), ],
), ),
ClideText(widget.project.timeAgo, muted: true, fontSize: 13), ),
], ClideText(project.timeAgo, muted: true, fontSize: 13),
), ],
), ),
), ),
); );
+20 -43
View File
@@ -4,6 +4,7 @@ import 'package:clide/kernel/src/theme/controller.dart';
import 'package:clide/kernel/src/theme/tokens.dart'; import 'package:clide/kernel/src/theme/tokens.dart';
import 'package:clide/kernel/src/window_controls.dart'; import 'package:clide/kernel/src/window_controls.dart';
import 'package:clide/widgets/src/clide_icon.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:clide/widgets/src/clide_text.dart';
import 'package:clide/widgets/src/icons/phosphor.dart'; import 'package:clide/widgets/src/icons/phosphor.dart';
import 'package:clide/widgets/src/typography.dart'; import 'package:clide/widgets/src/typography.dart';
@@ -113,69 +114,45 @@ class _RightContent extends StatelessWidget {
} }
} }
class _TrafficDot extends StatefulWidget { class _TrafficDot extends StatelessWidget {
const _TrafficDot({required this.color, required this.onTap}); const _TrafficDot({required this.color, required this.onTap});
final Color color; final Color color;
final VoidCallback onTap; final VoidCallback onTap;
@override
State<_TrafficDot> createState() => _TrafficDotState();
}
class _TrafficDotState extends State<_TrafficDot> {
bool _hover = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MouseRegion( return ClideTappable(
onEnter: (_) => setState(() => _hover = true), onTap: onTap,
onExit: (_) => setState(() => _hover = false), builder: (context, hovered, _) => Container(
child: GestureDetector( width: 12,
behavior: HitTestBehavior.opaque, height: 12,
onTap: widget.onTap, decoration: BoxDecoration(
child: Container( color: hovered ? color : color.withAlpha(0xCC),
width: 12, shape: BoxShape.circle,
height: 12,
decoration: BoxDecoration(
color: _hover ? widget.color : widget.color.withAlpha(0xCC),
shape: BoxShape.circle,
),
), ),
), ),
); );
} }
} }
class _WinButton extends StatefulWidget { class _WinButton extends StatelessWidget {
const _WinButton({required this.icon, required this.onTap, required this.tokens, this.isClose = false}); const _WinButton({required this.icon, required this.onTap, required this.tokens, this.isClose = false});
final ClideIconPainter icon; final ClideIconPainter icon;
final VoidCallback onTap; final VoidCallback onTap;
final SurfaceTokens tokens; final SurfaceTokens tokens;
final bool isClose; final bool isClose;
@override
State<_WinButton> createState() => _WinButtonState();
}
class _WinButtonState extends State<_WinButton> {
bool _hover = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final hoverBg = widget.isClose ? const Color(0xFFE81123) : widget.tokens.listItemHoverBackground; final hoverBg = isClose ? const Color(0xFFE81123) : tokens.listItemHoverBackground;
return MouseRegion( return ClideTappable(
onEnter: (_) => setState(() => _hover = true), onTap: onTap,
onExit: (_) => setState(() => _hover = false), builder: (context, hovered, _) => Container(
child: GestureDetector( width: 36,
behavior: HitTestBehavior.opaque, height: hatHeight,
onTap: widget.onTap, color: hovered ? hoverBg : null,
child: Container( alignment: Alignment.center,
width: 36, child: ClideIcon(icon, size: 14, color: hovered && isClose ? const Color(0xFFFFFFFF) : tokens.globalTextMuted),
height: hatHeight,
color: _hover ? hoverBg : null,
alignment: Alignment.center,
child: ClideIcon(widget.icon, size: 14, color: _hover && widget.isClose ? const Color(0xFFFFFFFF) : widget.tokens.globalTextMuted),
),
), ),
); );
} }
+17 -28
View File
@@ -3,6 +3,7 @@ import 'package:flutter/widgets.dart';
import 'clide_divider.dart'; import 'clide_divider.dart';
import 'clide_icon.dart'; import 'clide_icon.dart';
import 'clide_tappable.dart';
import 'clide_text.dart'; import 'clide_text.dart';
import 'icons/x.dart'; import 'icons/x.dart';
import 'typography.dart'; import 'typography.dart';
@@ -67,43 +68,31 @@ class ClidePaneChrome extends StatelessWidget {
} }
} }
class _CloseButton extends StatefulWidget { class _CloseButton extends StatelessWidget {
const _CloseButton({required this.onPressed}); const _CloseButton({required this.onPressed});
final VoidCallback onPressed; final VoidCallback onPressed;
@override
State<_CloseButton> createState() => _CloseButtonState();
}
class _CloseButtonState extends State<_CloseButton> {
bool _hover = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tokens = ClideTheme.of(context).surface; final tokens = ClideTheme.of(context).surface;
return Semantics( return Semantics(
button: true, button: true,
label: 'Close pane', label: 'Close pane',
onTap: widget.onPressed, onTap: onPressed,
child: MouseRegion( child: ClideTappable(
cursor: SystemMouseCursors.click, onTap: onPressed,
onEnter: (_) => setState(() => _hover = true), tooltip: 'Close pane',
onExit: (_) => setState(() => _hover = false), builder: (context, hovered, _) => Container(
child: GestureDetector( width: 20,
behavior: HitTestBehavior.opaque, height: 20,
onTap: widget.onPressed, alignment: Alignment.center,
child: Container( decoration: BoxDecoration(
width: 20, color: hovered ? tokens.tabCloseHover : null,
height: 20, ),
alignment: Alignment.center, child: ClideIcon(
decoration: BoxDecoration( const CloseIcon(),
color: _hover ? tokens.tabCloseHover : null, size: 10,
), color: tokens.panelHeaderForeground,
child: ClideIcon(
const CloseIcon(),
size: 10,
color: tokens.panelHeaderForeground,
),
), ),
), ),
), ),
+45 -56
View File
@@ -1,9 +1,10 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:clide/kernel/src/theme/controller.dart'; import 'package:clide/kernel/src/theme/controller.dart';
import 'package:clide/widgets/src/clide_tappable.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class ClideSpine extends StatefulWidget { class ClideSpine extends StatelessWidget {
const ClideSpine({ const ClideSpine({
super.key, super.key,
required this.label, required this.label,
@@ -19,15 +20,6 @@ class ClideSpine extends StatefulWidget {
static const double width = 12; static const double width = 12;
@override
State<ClideSpine> createState() => _ClideSpineState();
}
enum SpineSide { left, right }
class _ClideSpineState extends State<ClideSpine> {
bool _hovered = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tokens = ClideTheme.of(context).surface; final tokens = ClideTheme.of(context).surface;
@@ -35,60 +27,57 @@ class _ClideSpineState extends State<ClideSpine> {
return Semantics( return Semantics(
button: true, button: true,
label: '${widget.label} — click to expand', label: '$label — click to expand',
child: MouseRegion( child: ClideTappable(
cursor: SystemMouseCursors.click, onTap: onExpand,
onEnter: (_) => setState(() => _hovered = true), builder: (context, hovered, _) => Container(
onExit: (_) => setState(() => _hovered = false), width: ClideSpine.width,
child: GestureDetector( decoration: BoxDecoration(
onTap: widget.onExpand, color: hovered ? tokens.sidebarItemHover : tokens.chromeBackground,
child: Container( border: Border(
width: ClideSpine.width, left: side == SpineSide.right ? borderSide : BorderSide.none,
decoration: BoxDecoration( right: side == SpineSide.left ? borderSide : BorderSide.none,
color: _hovered ? tokens.sidebarItemHover : tokens.chromeBackground,
border: Border(
left: widget.side == SpineSide.right ? borderSide : BorderSide.none,
right: widget.side == SpineSide.left ? borderSide : BorderSide.none,
),
), ),
child: Stack( ),
children: [ child: Stack(
Center( children: [
child: Transform.rotate( Center(
angle: widget.side == SpineSide.left ? -math.pi / 2 : math.pi / 2, child: Transform.rotate(
child: Text( angle: side == SpineSide.left ? -math.pi / 2 : math.pi / 2,
widget.label, child: Text(
style: TextStyle( label,
fontSize: 9, style: TextStyle(
color: tokens.globalTextMuted, fontSize: 9,
letterSpacing: 0.5, color: tokens.globalTextMuted,
letterSpacing: 0.5,
),
maxLines: 1,
overflow: TextOverflow.clip,
),
),
),
if (badgeCount > 0)
Positioned(
top: 4,
left: 0,
right: 0,
child: Center(
child: Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: tokens.statusInfo,
shape: BoxShape.circle,
), ),
maxLines: 1,
overflow: TextOverflow.clip,
), ),
), ),
), ),
if (widget.badgeCount > 0) ],
Positioned(
top: 4,
left: 0,
right: 0,
child: Center(
child: Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: tokens.statusInfo,
shape: BoxShape.circle,
),
),
),
),
],
),
), ),
), ),
), ),
); );
} }
} }
enum SpineSide { left, right }
+17 -32
View File
@@ -1,5 +1,6 @@
import 'package:clide/kernel/src/theme/controller.dart'; import 'package:clide/kernel/src/theme/controller.dart';
import 'package:clide/widgets/src/clide_icon.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:clide/widgets/src/clide_text.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@@ -59,51 +60,35 @@ class ClideTabBar extends StatelessWidget {
} }
} }
class _Tab extends StatefulWidget { class _Tab extends StatelessWidget {
const _Tab({required this.item, required this.active, required this.onTap}); const _Tab({required this.item, required this.active, required this.onTap});
final ClideTabItem item; final ClideTabItem item;
final bool active; final bool active;
final VoidCallback onTap; final VoidCallback onTap;
@override
State<_Tab> createState() => _TabState();
}
class _TabState extends State<_Tab> {
bool _hovered = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tokens = ClideTheme.of(context).surface; final tokens = ClideTheme.of(context).surface;
final bg = widget.active final fg = active ? tokens.tabActiveForeground : tokens.tabInactiveForeground;
? tokens.tabActive
: (_hovered ? tokens.tabInactive : tokens.tabInactive);
final fg = widget.active
? tokens.tabActiveForeground
: tokens.tabInactiveForeground;
return Semantics( return Semantics(
button: true, button: true,
selected: widget.active, selected: active,
label: widget.item.title, label: item.title,
onTap: widget.onTap, onTap: onTap,
excludeSemantics: true, excludeSemantics: true,
child: MouseRegion( child: ClideTappable(
cursor: SystemMouseCursors.click, onTap: onTap,
onEnter: (_) => setState(() => _hovered = true), builder: (context, hovered, _) {
onExit: (_) => setState(() => _hovered = false), final bg = active ? tokens.tabActive : tokens.tabInactive;
child: GestureDetector( return Container(
onTap: widget.onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: bg, color: bg,
border: Border( border: Border(
bottom: BorderSide( bottom: BorderSide(
color: widget.active color: active ? tokens.tabActiveBorder : const Color(0x00000000),
? tokens.tabActiveBorder
: const Color(0x00000000),
width: 2, width: 2,
), ),
), ),
@@ -111,15 +96,15 @@ class _TabState extends State<_Tab> {
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (widget.item.icon != null) ...[ if (item.icon != null) ...[
ClideIcon(widget.item.icon!, size: 12, color: fg), ClideIcon(item.icon!, size: 12, color: fg),
const SizedBox(width: 6), const SizedBox(width: 6),
], ],
ClideText(widget.item.title, color: fg), ClideText(item.title, color: fg),
], ],
), ),
), );
), },
), ),
); );
} }