keyspot 0.3.1 copy "keyspot: ^0.3.1" to clipboard
keyspot: ^0.3.1 copied to clipboard

Interactive onboarding and guided tours for Flutter. Highlight widgets, teach gestures with an animated pointer, and track targets live so nothing drifts.

example/lib/main.dart

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

import 'demos/basic_spotlight.dart';
import 'demos/full_tour.dart';
import 'demos/gesture_teaching.dart';
import 'demos/pointer_playground.dart';
import 'demos/scrolling_list.dart';
import 'demos/shapes_gallery.dart';
import 'demos/theming.dart';

void main() => runApp(const KeyspotDemoApp());

/// The demo application.
class KeyspotDemoApp extends StatefulWidget {
  /// Creates the demo app.
  const KeyspotDemoApp({super.key});

  @override
  State<KeyspotDemoApp> createState() => _KeyspotDemoAppState();
}

class _KeyspotDemoAppState extends State<KeyspotDemoApp> {
  final KeyspotController _keyspot =
      KeyspotController(logger: KeyspotLoggers.debug);

  @override
  void dispose() {
    _keyspot.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Keyspot demos',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorSchemeSeed: const Color(0xFF00838F),
        brightness: Brightness.light,
        useMaterial3: true,
      ),
      // Mounting the scope in `builder` keeps Theme, Directionality and
      // localisations available to step content cards.
      builder: (BuildContext context, Widget? child) {
        return KeyspotScope(
          controller: _keyspot,
          child: child ?? const SizedBox.shrink(),
        );
      },
      home: HomePage(keyspot: _keyspot),
    );
  }
}

/// A demo entry in the home grid.
class Demo {
  /// Creates a demo entry.
  const Demo({
    required this.title,
    required this.blurb,
    required this.icon,
    required this.color,
    required this.builder,
  });

  /// Card title.
  final String title;

  /// One-line description.
  final String blurb;

  /// Card icon.
  final IconData icon;

  /// Accent colour for the card's icon chip.
  final Color color;

  /// Builds the demo page.
  final Widget Function(KeyspotController keyspot) builder;
}

/// The list of demos shown on the home screen.
List<Demo> buildDemos() => const <Demo>[
      Demo(
        title: 'Basic spotlight',
        blurb: 'Dim the screen, highlight a widget, choose how it dismisses.',
        icon: Icons.highlight_rounded,
        color: Color(0xFFF9A825),
        builder: _basicSpotlight,
      ),
      Demo(
        title: 'Shapes gallery',
        blurb: 'Auto-detected outlines, forced shapes, and custom paths.',
        icon: Icons.category_rounded,
        color: Color(0xFF7B1FA2),
        builder: _shapesGallery,
      ),
      Demo(
        title: 'Pointer playground',
        blurb: 'Drive the hand yourself — glide, rotate, arc and pulse.',
        icon: Icons.pan_tool_alt_rounded,
        color: Color(0xFF00838F),
        builder: _pointerPlayground,
      ),
      Demo(
        title: 'Scrolling list',
        blurb: 'Find a buried row, then watch the spotlight track your scroll.',
        icon: Icons.format_list_bulleted_rounded,
        color: Color(0xFF3949AB),
        builder: _scrollingList,
      ),
      Demo(
        title: 'Full tour',
        blurb: 'Seven guided steps with cards, back, skip and memory.',
        icon: Icons.route_rounded,
        color: Color(0xFF43A047),
        builder: _fullTour,
      ),
      Demo(
        title: 'Gesture teaching',
        blurb: 'Show a drag and a pinch with the animated hand.',
        icon: Icons.gesture_rounded,
        color: Color(0xFFF4511E),
        builder: _gestureTeaching,
      ),
      Demo(
        title: 'Theming',
        blurb: 'Neon rings, bare spotlights, and pointers as emoji, '
            'SVG or live animation.',
        icon: Icons.palette_rounded,
        color: Color(0xFF5E35B1),
        builder: _theming,
      ),
    ];

// Top-level builder functions keep the Demo list const-constructible.
Widget _basicSpotlight(KeyspotController k) => BasicSpotlightDemo(keyspot: k);
Widget _shapesGallery(KeyspotController k) => ShapesGalleryDemo(keyspot: k);
Widget _pointerPlayground(KeyspotController k) =>
    PointerPlaygroundDemo(keyspot: k);
Widget _scrollingList(KeyspotController k) => ScrollingListDemo(keyspot: k);
Widget _fullTour(KeyspotController k) => FullTourDemo(keyspot: k);
Widget _gestureTeaching(KeyspotController k) => GestureTeachingDemo(keyspot: k);
Widget _theming(KeyspotController k) => ThemingDemo(keyspot: k);

/// The home screen: a branded header plus the demo cards.
///
/// Lays out as a single-column list on phones and a grid on wider screens.
class HomePage extends StatelessWidget {
  /// Creates the home page.
  const HomePage({super.key, required this.keyspot});

  /// The shared controller.
  final KeyspotController keyspot;

  void _open(BuildContext context, Demo demo) {
    Navigator.of(context).push(
      MaterialPageRoute<void>(builder: (_) => demo.builder(keyspot)),
    );
  }

  @override
  Widget build(BuildContext context) {
    final List<Demo> demos = buildDemos();

    return Scaffold(
      body: SafeArea(
        child: LayoutBuilder(
          builder: (BuildContext context, BoxConstraints constraints) {
            final bool compact = constraints.maxWidth < 640.0;
            final EdgeInsets pagePadding = EdgeInsets.symmetric(
              horizontal: compact ? 16.0 : 32.0,
            );

            return CustomScrollView(
              slivers: <Widget>[
                SliverPadding(
                  padding: pagePadding.copyWith(top: compact ? 24.0 : 48.0),
                  sliver: SliverToBoxAdapter(child: _Header(compact: compact)),
                ),
                SliverPadding(
                  padding: pagePadding.copyWith(top: 24.0, bottom: 32.0),
                  sliver: compact
                      ? SliverList.separated(
                          itemCount: demos.length,
                          separatorBuilder: (_, int __) =>
                              const SizedBox(height: 10.0),
                          itemBuilder: (BuildContext context, int index) =>
                              _DemoCard(
                            demo: demos[index],
                            compact: true,
                            onTap: () => _open(context, demos[index]),
                          ),
                        )
                      : SliverGrid(
                          gridDelegate:
                              const SliverGridDelegateWithMaxCrossAxisExtent(
                            maxCrossAxisExtent: 320.0,
                            mainAxisExtent: 172.0,
                            mainAxisSpacing: 14.0,
                            crossAxisSpacing: 14.0,
                          ),
                          delegate: SliverChildBuilderDelegate(
                            (BuildContext context, int index) => _DemoCard(
                              demo: demos[index],
                              compact: false,
                              onTap: () => _open(context, demos[index]),
                            ),
                            childCount: demos.length,
                          ),
                        ),
                ),
              ],
            );
          },
        ),
      ),
    );
  }
}

class _Header extends StatelessWidget {
  const _Header({required this.compact});

  final bool compact;

  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
    final ColorScheme scheme = theme.colorScheme;

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Row(
          children: <Widget>[
            Container(
              width: 44.0,
              height: 44.0,
              decoration: BoxDecoration(
                color: scheme.primary,
                borderRadius: BorderRadius.circular(14.0),
              ),
              child: Icon(Icons.center_focus_strong_rounded,
                  color: scheme.onPrimary, size: 26.0),
            ),
            const SizedBox(width: 14.0),
            Text(
              'Keyspot',
              style: (compact
                      ? theme.textTheme.headlineMedium
                      : theme.textTheme.displaySmall)
                  ?.copyWith(fontWeight: FontWeight.w700),
            ),
          ],
        ),
        const SizedBox(height: 12.0),
        Text(
          'Spotlights and guided hand gestures for Flutter — anchored to a '
          'GlobalKey, tracked live, never drifting.',
          style: theme.textTheme.bodyLarge
              ?.copyWith(color: scheme.onSurfaceVariant),
        ),
        const SizedBox(height: 16.0),
        Wrap(
          spacing: 8.0,
          runSpacing: 8.0,
          children: const <Widget>[
            _FeatureChip(
                icon: Icons.my_location_rounded, label: 'Drift-free tracking'),
            _FeatureChip(
                icon: Icons.swipe_rounded, label: 'Gesture choreography'),
            _FeatureChip(
                icon: Icons.inventory_2_outlined, label: 'Zero dependencies'),
          ],
        ),
      ],
    );
  }
}

class _FeatureChip extends StatelessWidget {
  const _FeatureChip({required this.icon, required this.label});

  final IconData icon;
  final String label;

  @override
  Widget build(BuildContext context) {
    final ColorScheme scheme = Theme.of(context).colorScheme;
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0),
      decoration: BoxDecoration(
        color: scheme.secondaryContainer.withValues(alpha: 0.6),
        borderRadius: BorderRadius.circular(999.0),
      ),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          Icon(icon, size: 16.0, color: scheme.onSecondaryContainer),
          const SizedBox(width: 6.0),
          Text(
            label,
            style: Theme.of(context)
                .textTheme
                .labelMedium
                ?.copyWith(color: scheme.onSecondaryContainer),
          ),
        ],
      ),
    );
  }
}

class _DemoCard extends StatelessWidget {
  const _DemoCard({
    required this.demo,
    required this.compact,
    required this.onTap,
  });

  final Demo demo;
  final bool compact;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
    final ColorScheme scheme = theme.colorScheme;

    final Widget iconChip = Container(
      width: 44.0,
      height: 44.0,
      decoration: BoxDecoration(
        color: demo.color.withValues(alpha: 0.14),
        borderRadius: BorderRadius.circular(12.0),
      ),
      child: Icon(demo.icon, color: demo.color, size: 24.0),
    );

    final Widget title = Text(
      demo.title,
      style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600),
    );

    final Widget blurb = Text(
      demo.blurb,
      maxLines: 2,
      overflow: TextOverflow.ellipsis,
      style:
          theme.textTheme.bodySmall?.copyWith(color: scheme.onSurfaceVariant),
    );

    return Card(
      elevation: 0.0,
      clipBehavior: Clip.antiAlias,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16.0),
        side: BorderSide(color: scheme.outlineVariant),
      ),
      child: InkWell(
        onTap: onTap,
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: compact
              ? Row(
                  children: <Widget>[
                    iconChip,
                    const SizedBox(width: 14.0),
                    Expanded(
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: <Widget>[
                          title,
                          const SizedBox(height: 2.0),
                          blurb,
                        ],
                      ),
                    ),
                    Icon(Icons.chevron_right_rounded,
                        color: scheme.outline, size: 22.0),
                  ],
                )
              : Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Row(
                      children: <Widget>[
                        iconChip,
                        const Spacer(),
                        Icon(Icons.arrow_outward_rounded,
                            color: scheme.outline, size: 18.0),
                      ],
                    ),
                    const Spacer(),
                    title,
                    const SizedBox(height: 4.0),
                    blurb,
                  ],
                ),
        ),
      ),
    );
  }
}

/// Shared scaffold for the demo pages.
class DemoScaffold extends StatelessWidget {
  /// Creates a demo scaffold.
  const DemoScaffold({
    super.key,
    required this.title,
    required this.body,
    this.subtitle,
    this.actions = const <Widget>[],
  });

  /// App bar title.
  final String title;

  /// Optional one-line explanation shown under the app bar.
  final String? subtitle;

  /// Page body.
  final Widget body;

  /// Buttons rendered under the body.
  final List<Widget> actions;

  @override
  Widget build(BuildContext context) {
    final ColorScheme scheme = Theme.of(context).colorScheme;
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
        bottom: subtitle == null
            ? null
            : PreferredSize(
                preferredSize: const Size.fromHeight(28.0),
                child: Padding(
                  padding: const EdgeInsets.only(
                      left: 16.0, right: 16.0, bottom: 8.0),
                  child: Align(
                    alignment: Alignment.centerLeft,
                    child: Text(
                      subtitle!,
                      style: Theme.of(context)
                          .textTheme
                          .bodySmall
                          ?.copyWith(color: scheme.onSurfaceVariant),
                    ),
                  ),
                ),
              ),
      ),
      body: Column(
        children: <Widget>[
          Expanded(child: body),
          if (actions.isNotEmpty)
            DecoratedBox(
              decoration: BoxDecoration(
                color: scheme.surfaceContainerLow,
                border: Border(top: BorderSide(color: scheme.outlineVariant)),
              ),
              child: SafeArea(
                child: Padding(
                  padding: const EdgeInsets.all(12.0),
                  child: Wrap(
                    spacing: 8.0,
                    runSpacing: 8.0,
                    alignment: WrapAlignment.center,
                    children: actions,
                  ),
                ),
              ),
            ),
        ],
      ),
    );
  }
}
14
likes
160
points
87
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Interactive onboarding and guided tours for Flutter. Highlight widgets, teach gestures with an animated pointer, and track targets live so nothing drifts.

Homepage
Repository (GitHub)
View/report issues
Contributing

Topics

#onboarding #showcase #tutorial #spotlight #walkthrough

License

MIT (license)

Dependencies

flutter

More

Packages that depend on keyspot