group consecutive same-file edits into one collapsed card (T-296)
A run of 2+ consecutive edits to the same file now folds into one ClideHolderCard labelled '# edits' (coalesceEditRuns, run after groupConversation) instead of a stack of cards; a different file or an interleaving step splits the run. Every edit stays reachable on expand. The holder gained an optional aggregate status. New owned primitives: ClideSpinner (the logo mark, monochrome, 3D Y-axis rotation, reduced-motion-aware) and ClideStatusIndicator (running→spinner / success→check / error→cross, with an AnimatedSwitcher seam for a richer transition later — kept self-contained, not built on ConversationCard's mark). The activity card shares the same indicator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
/// A compact in-progress spinner: the clide logo mark, monochrome, rotating in
|
||||
/// 3D about its vertical axis (T-296).
|
||||
///
|
||||
/// Reuses `assets/logo/logo.svg` as the single source of truth for the mark
|
||||
/// (tinted to one colour via a srcIn [ColorFilter]) rather than re-coding the
|
||||
/// geometry, and spins it with a perspective Y-rotation. Honours
|
||||
/// reduced-motion: when animations are disabled it shows the static, front-on
|
||||
/// mark. Animation is one [AnimationController] (no timers) so tests advance it
|
||||
/// with bounded pumps.
|
||||
library;
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:clide/kernel/src/theme/controller.dart';
|
||||
import 'package:clide/widgets/src/clide_svg_view.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class ClideSpinner extends StatefulWidget {
|
||||
const ClideSpinner({
|
||||
super.key,
|
||||
this.size = 14,
|
||||
this.color,
|
||||
this.period = const Duration(milliseconds: 1500),
|
||||
this.semanticLabel,
|
||||
});
|
||||
|
||||
final double size;
|
||||
|
||||
/// Mark colour; defaults to the theme's foreground (monochrome on the chrome).
|
||||
final Color? color;
|
||||
|
||||
/// Time for one full rotation.
|
||||
final Duration period;
|
||||
|
||||
/// Optional AT label (e.g. 'running'); omit when a parent announces status.
|
||||
final String? semanticLabel;
|
||||
|
||||
@override
|
||||
State<ClideSpinner> createState() => _ClideSpinnerState();
|
||||
}
|
||||
|
||||
class _ClideSpinnerState extends State<ClideSpinner> with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _ctrl = AnimationController(vsync: this, duration: widget.period);
|
||||
bool _reducedMotion = false;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_reducedMotion = MediaQuery.maybeOf(context)?.disableAnimations ?? false;
|
||||
_sync();
|
||||
}
|
||||
|
||||
void _sync() {
|
||||
if (_reducedMotion) {
|
||||
_ctrl.stop();
|
||||
_ctrl.value = 0;
|
||||
} else if (!_ctrl.isAnimating) {
|
||||
_ctrl.repeat();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ctrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = widget.color ?? ClideTheme.of(context).surface.globalForeground;
|
||||
// Tint every stroke of the multi-colour logo to one colour, keeping alpha.
|
||||
final mark = ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
|
||||
child: ClideSvgView.asset('assets/logo/logo.svg', width: widget.size, height: widget.size),
|
||||
);
|
||||
final child = _reducedMotion
|
||||
? mark
|
||||
: AnimatedBuilder(
|
||||
animation: _ctrl,
|
||||
child: mark,
|
||||
builder: (_, child) => Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.identity()
|
||||
..setEntry(3, 2, 0.0015) // perspective
|
||||
..rotateY(_ctrl.value * 2 * math.pi),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
return Semantics(
|
||||
label: widget.semanticLabel,
|
||||
excludeSemantics: widget.semanticLabel == null,
|
||||
child: SizedBox(width: widget.size, height: widget.size, child: child),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/// A self-contained run-status glyph: spinner while running, check on success,
|
||||
/// cross on failure (T-296).
|
||||
///
|
||||
/// Deliberately NOT built on ConversationCard's success/error mark — it owns its
|
||||
/// own states and rendering so the spinner→check / spinner→cross transition can
|
||||
/// grow richer (a morph/cross-fade) without being constrained by that card. A
|
||||
/// light [AnimatedSwitcher] cross-fade between states is wired now; the keyed
|
||||
/// children leave the seam for a fuller transition later.
|
||||
library;
|
||||
|
||||
import 'package:clide/kernel/src/theme/controller.dart';
|
||||
import 'package:clide/widgets/src/clide_icon.dart';
|
||||
import 'package:clide/widgets/src/clide_spinner.dart';
|
||||
import 'package:clide/widgets/src/icons/check.dart';
|
||||
import 'package:clide/widgets/src/icons/x.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
enum ClideRunStatus { running, success, error }
|
||||
|
||||
class ClideStatusIndicator extends StatelessWidget {
|
||||
const ClideStatusIndicator({super.key, required this.status, this.size = 14});
|
||||
|
||||
final ClideRunStatus status;
|
||||
final double size;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
final (Widget glyph, String label) = switch (status) {
|
||||
ClideRunStatus.running => (ClideSpinner(size: size, color: tokens.globalTextMuted, key: const ValueKey('running')), 'running'),
|
||||
ClideRunStatus.success => (ClideIcon(const CheckIcon(), size: size, color: tokens.statusSuccess, key: const ValueKey('success')), 'succeeded'),
|
||||
ClideRunStatus.error => (ClideIcon(const CloseIcon(), size: size, color: tokens.statusError, key: const ValueKey('error')), 'failed'),
|
||||
};
|
||||
return Semantics(
|
||||
label: label,
|
||||
container: true,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: glyph,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user