flutter_thinking_orbs 0.1.0
flutter_thinking_orbs: ^0.1.0 copied to clipboard
Dotted thought-orb loading indicators for AI & agent UIs. Six hand-tuned animated states, monochrome and theme-aware, drawn with plain Canvas circles.
import 'package:flutter/material.dart';
import 'package:flutter_thinking_orbs/flutter_thinking_orbs.dart';
void main() => runApp(const ExampleApp());
/// Demo app for the flutter_thinking_orbs package.
class ExampleApp extends StatefulWidget {
/// Creates the demo app.
const ExampleApp({super.key});
@override
State<ExampleApp> createState() => _ExampleAppState();
}
class _ExampleAppState extends State<ExampleApp> {
Brightness _brightness = Brightness.dark;
double _size = 96;
double _speed = 1;
bool _paused = false;
@override
Widget build(BuildContext context) {
final dark = _brightness == Brightness.dark;
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(brightness: _brightness, useMaterial3: true),
home: Scaffold(
backgroundColor:
dark ? const Color(0xFF101013) : const Color(0xFFF5F5F4),
appBar: AppBar(
backgroundColor: Colors.transparent,
title: const Text('flutter_thinking_orbs'),
actions: [
IconButton(
tooltip: dark ? 'Switch to light' : 'Switch to dark',
icon: Icon(
dark ? Icons.light_mode_outlined : Icons.dark_mode_outlined,
),
onPressed: () => setState(
() => _brightness = dark ? Brightness.light : Brightness.dark,
),
),
],
),
body: Column(
children: [
Expanded(
child: GridView.count(
padding: const EdgeInsets.all(24),
crossAxisCount: MediaQuery.sizeOf(context).width > 640 ? 3 : 2,
mainAxisSpacing: 24,
crossAxisSpacing: 24,
children: [
for (final state in OrbState.values)
_OrbCell(
state: state,
size: _size,
speed: _speed,
paused: _paused,
dark: dark,
),
],
),
),
_Controls(
size: _size,
speed: _speed,
paused: _paused,
onSize: (v) => setState(() => _size = v),
onSpeed: (v) => setState(() => _speed = v),
onPaused: (v) => setState(() => _paused = v),
),
],
),
),
);
}
}
class _OrbCell extends StatelessWidget {
const _OrbCell({
required this.state,
required this.size,
required this.speed,
required this.paused,
required this.dark,
});
final OrbState state;
final double size;
final double speed;
final bool paused;
final bool dark;
@override
Widget build(BuildContext context) {
final ink = dark ? Colors.white70 : Colors.black87;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Center(
child: ThinkingOrb(
state: state,
size: size,
speed: speed,
paused: paused,
),
),
),
const SizedBox(height: 12),
Text(
state.name,
style: TextStyle(color: ink, fontSize: 13, letterSpacing: 0.2),
),
],
);
}
}
class _Controls extends StatelessWidget {
const _Controls({
required this.size,
required this.speed,
required this.paused,
required this.onSize,
required this.onSpeed,
required this.onPaused,
});
final double size;
final double speed;
final bool paused;
final ValueChanged<double> onSize;
final ValueChanged<double> onSpeed;
final ValueChanged<bool> onPaused;
@override
Widget build(BuildContext context) {
return SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 16),
child: Column(
children: [
_Slider(
label: 'size',
value: size,
min: 16,
max: 160,
display: size.round().toString(),
onChanged: onSize,
),
_Slider(
label: 'speed',
value: speed,
min: 0.1,
max: 3,
display: '${speed.toStringAsFixed(1)}×',
onChanged: onSpeed,
),
Row(
children: [
const SizedBox(width: 4),
const Text('paused'),
const Spacer(),
Switch(value: paused, onChanged: onPaused),
],
),
],
),
),
);
}
}
class _Slider extends StatelessWidget {
const _Slider({
required this.label,
required this.value,
required this.min,
required this.max,
required this.display,
required this.onChanged,
});
final String label;
final double value;
final double min;
final double max;
final String display;
final ValueChanged<double> onChanged;
@override
Widget build(BuildContext context) {
return Row(
children: [
SizedBox(width: 56, child: Text(label)),
Expanded(
child: Slider(value: value, min: min, max: max, onChanged: onChanged),
),
SizedBox(width: 44, child: Text(display, textAlign: TextAlign.end)),
],
);
}
}