claude_session_viewer/lib/widgets/common/expandable_card.dart
Mathias Beaulieu-Duncan 364877d376 Initial commit: Claude Code session viewer (Flutter macOS)
A desktop app that parses Claude Code .jsonl session logs and provides
a rich UI for exploring conversations, tool usage, subagents, and token
consumption. Features include project browser with auto-discovery of
~/.claude/projects, conversation timeline with inline subagent expansion,
agents overview, toolbelt chart, and token usage dashboard.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 16:17:23 -04:00

114 lines
3.0 KiB
Dart

import 'package:flutter/material.dart';
import '../../theme/app_theme.dart';
class ExpandableCard extends StatefulWidget {
final Widget header;
final Widget child;
final bool initiallyExpanded;
final Color? backgroundColor;
final Color? borderColor;
const ExpandableCard({
super.key,
required this.header,
required this.child,
this.initiallyExpanded = false,
this.backgroundColor,
this.borderColor,
});
@override
State<ExpandableCard> createState() => _ExpandableCardState();
}
class _ExpandableCardState extends State<ExpandableCard>
with SingleTickerProviderStateMixin {
late bool _expanded;
late AnimationController _controller;
late Animation<double> _rotation;
@override
void initState() {
super.initState();
_expanded = widget.initiallyExpanded;
_controller = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
value: _expanded ? 1.0 : 0.0,
);
_rotation = Tween<double>(begin: 0, end: 0.25).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _toggle() {
setState(() {
_expanded = !_expanded;
if (_expanded) {
_controller.forward();
} else {
_controller.reverse();
}
});
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: widget.backgroundColor ?? AppColors.surface,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: widget.borderColor ?? AppColors.surfaceBorder,
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
InkWell(
onTap: _toggle,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Row(
children: [
RotationTransition(
turns: _rotation,
child: const Icon(
Icons.chevron_right,
size: 18,
color: AppColors.textSecondary,
),
),
const SizedBox(width: 8),
Expanded(child: widget.header),
],
),
),
),
ClipRect(
child: AnimatedCrossFade(
firstChild: const SizedBox.shrink(),
secondChild: Padding(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
child: widget.child,
),
crossFadeState: _expanded
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
duration: const Duration(milliseconds: 200),
),
),
],
),
);
}
}