thinking_orbs 0.2.0 copy "thinking_orbs: ^0.2.0" to clipboard
thinking_orbs: ^0.2.0 copied to clipboard

Dotted thought-orb loading indicators for AI & agent UIs — nine tuned states, two sizes, auto dark/light, and custom colors. A Flutter port of Jakub Antalik's thinking-orbs.

example/lib/main.dart

// Playground for the thinking_orbs package — mirrors the original
// thinking-orbs demo: all nine states at both tuned sizes, with theme
// toggle, custom colors, speed slider and play/pause.

import 'package:flutter/material.dart';
import 'package:thinking_orbs/thinking_orbs.dart';

void main() {
  runApp(const OrbsPlaygroundApp());
}

class OrbsPlaygroundApp extends StatefulWidget {
  const OrbsPlaygroundApp({super.key});

  @override
  State<OrbsPlaygroundApp> createState() => _OrbsPlaygroundAppState();
}

class _OrbsPlaygroundAppState extends State<OrbsPlaygroundApp> {
  bool _dark = true;
  double _speed = 1;
  bool _paused = false;
  Color? _color = const Color(0xFF8B5CF6);

  static const _labels = {
    OrbState.working: 'working',
    OrbState.searching: 'searching',
    OrbState.solving: 'solving',
    OrbState.listening: 'listening',
    OrbState.connecting: 'connecting',
    OrbState.weaving: 'weaving',
    OrbState.composing: 'composing',
    OrbState.breathing: 'breathing',
    OrbState.shaping: 'shaping',
  };

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'thinking_orbs',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        brightness: _dark ? Brightness.dark : Brightness.light,
        scaffoldBackgroundColor:
            _dark ? const Color(0xFF09090B) : const Color(0xFFFAFAFA),
        useMaterial3: true,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('thinking_orbs'),
          backgroundColor: Colors.transparent,
          actions: [
            IconButton(
              tooltip: _paused ? 'Play' : 'Pause',
              icon: Icon(_paused ? Icons.play_arrow : Icons.pause),
              onPressed: () => setState(() => _paused = !_paused),
            ),
            IconButton(
              tooltip: _dark ? 'Light mode' : 'Dark mode',
              icon: Icon(_dark ? Icons.light_mode : Icons.dark_mode),
              onPressed: () => setState(() => _dark = !_dark),
            ),
          ],
        ),
        body: SafeArea(
          child: LayoutBuilder(
            builder: (context, constraints) {
              final horizontalPadding =
                  constraints.maxWidth < 600 ? 16.0 : 24.0;
              return Column(
                children: [
                  Center(
                    child: ConstrainedBox(
                      constraints: const BoxConstraints(maxWidth: 1120),
                      child: Padding(
                        padding:
                            EdgeInsets.symmetric(horizontal: horizontalPadding),
                        child: LayoutBuilder(
                          builder: (context, controlConstraints) {
                            final compact = controlConstraints.maxWidth < 720;
                            final speedControl = Row(
                              children: [
                                const Text('speed'),
                                Expanded(
                                  child: Slider(
                                    value: _speed,
                                    min: 0.25,
                                    max: 3,
                                    divisions: 11,
                                    label: '${_speed.toStringAsFixed(2)}x',
                                    onChanged: (v) =>
                                        setState(() => _speed = v),
                                  ),
                                ),
                                SizedBox(
                                  width: 52,
                                  child: Text(
                                    '${_speed.toStringAsFixed(2)}x',
                                    textAlign: TextAlign.end,
                                  ),
                                ),
                              ],
                            );
                            final colorPicker = _ColorPicker(
                              selected: _color,
                              onSelected: (color) =>
                                  setState(() => _color = color),
                            );

                            if (compact) {
                              return Column(
                                crossAxisAlignment: CrossAxisAlignment.start,
                                children: [
                                  speedControl,
                                  const SizedBox(height: 4),
                                  colorPicker,
                                ],
                              );
                            }
                            return Row(
                              children: [
                                Expanded(child: speedControl),
                                const SizedBox(width: 32),
                                colorPicker,
                              ],
                            );
                          },
                        ),
                      ),
                    ),
                  ),
                  Expanded(
                    child: Center(
                      child: ConstrainedBox(
                        constraints: const BoxConstraints(maxWidth: 1120),
                        child: GridView.builder(
                          padding: EdgeInsets.fromLTRB(
                            horizontalPadding,
                            16,
                            horizontalPadding,
                            24,
                          ),
                          gridDelegate:
                              const SliverGridDelegateWithMaxCrossAxisExtent(
                            maxCrossAxisExtent: 380,
                            mainAxisExtent: 220,
                            mainAxisSpacing: 16,
                            crossAxisSpacing: 16,
                          ),
                          itemCount: OrbState.values.length,
                          itemBuilder: (context, index) {
                            final state = OrbState.values[index];
                            return _OrbCard(
                              key: ValueKey('orb-card-${state.name}'),
                              state: state,
                              label: _labels[state]!,
                              theme: _dark ? OrbTheme.dark : OrbTheme.light,
                              color: _color,
                              speed: _speed,
                              paused: _paused,
                              dark: _dark,
                            );
                          },
                        ),
                      ),
                    ),
                  ),
                ],
              );
            },
          ),
        ),
      ),
    );
  }
}

class _ColorPicker extends StatelessWidget {
  const _ColorPicker({required this.selected, required this.onSelected});

  static const options = <({String key, String label, Color? color})>[
    (key: 'original', label: 'Original monochrome', color: null),
    (key: 'violet', label: 'Violet', color: Color(0xFF8B5CF6)),
    (key: 'sky', label: 'Sky blue', color: Color(0xFF38BDF8)),
    (key: 'emerald', label: 'Emerald', color: Color(0xFF34D399)),
    (key: 'amber', label: 'Amber', color: Color(0xFFF59E0B)),
  ];

  final Color? selected;
  final ValueChanged<Color?> onSelected;

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        const Text('color'),
        const SizedBox(width: 8),
        for (final option in options)
          SizedBox.square(
            dimension: 44,
            child: Tooltip(
              message: option.label,
              child: Semantics(
                button: true,
                selected: option.color == selected,
                label: option.label,
                child: InkWell(
                  key: ValueKey('color-${option.key}'),
                  customBorder: const CircleBorder(),
                  onTap: () => onSelected(option.color),
                  child: Center(
                    child: AnimatedContainer(
                      duration: const Duration(milliseconds: 160),
                      width: option.color == selected ? 28 : 24,
                      height: option.color == selected ? 28 : 24,
                      decoration: BoxDecoration(
                        color: option.color,
                        shape: BoxShape.circle,
                        border: Border.all(
                          color: option.color == selected
                              ? Theme.of(context).colorScheme.onSurface
                              : Theme.of(context).colorScheme.outline,
                          width: option.color == selected ? 2 : 1,
                        ),
                      ),
                      child: option.color == null
                          ? const Icon(Icons.contrast, size: 16)
                          : null,
                    ),
                  ),
                ),
              ),
            ),
          ),
      ],
    );
  }
}

class _OrbCard extends StatelessWidget {
  const _OrbCard({
    super.key,
    required this.state,
    required this.label,
    required this.theme,
    required this.color,
    required this.speed,
    required this.paused,
    required this.dark,
  });

  final OrbState state;
  final String label;
  final OrbTheme theme;
  final Color? color;
  final double speed;
  final bool paused;
  final bool dark;

  @override
  Widget build(BuildContext context) {
    return DecoratedBox(
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(16),
        border: Border.all(
          color: dark ? const Color(0xFF27272A) : const Color(0xFFE4E4E7),
        ),
      ),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              ThinkingOrb(
                state: state,
                size: OrbSize.size64,
                theme: theme,
                color: color,
                speed: speed,
                paused: paused,
              ),
              const SizedBox(width: 20),
              ThinkingOrb(
                state: state,
                size: OrbSize.size20,
                theme: theme,
                color: color,
                speed: speed,
                paused: paused,
              ),
            ],
          ),
          const SizedBox(height: 12),
          Text(
            label,
            style: TextStyle(
              fontSize: 13,
              color: dark ? const Color(0xFFA1A1AA) : const Color(0xFF71717A),
            ),
          ),
        ],
      ),
    );
  }
}
1
likes
160
points
769
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Dotted thought-orb loading indicators for AI & agent UIs — nine tuned states, two sizes, auto dark/light, and custom colors. A Flutter port of Jakub Antalik's thinking-orbs.

Repository (GitHub)
View/report issues
Contributing

License

MIT (license)

Dependencies

flutter

More

Packages that depend on thinking_orbs