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

Solar icon kit for Flutter. 1,231 icons in 6 native styles — linear, outline, broken, bold, line-duotone, bold-duotone — behind a single SolarIcon widget.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:solar_iconkit/solar_iconkit.dart';

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

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

  @override
  State<SolarExampleApp> createState() => _SolarExampleAppState();
}

class _SolarExampleAppState extends State<SolarExampleApp> {
  ThemeMode _themeMode = ThemeMode.system;

  void _toggleTheme() {
    setState(() {
      _themeMode = switch (_themeMode) {
        ThemeMode.system => ThemeMode.light,
        ThemeMode.light => ThemeMode.dark,
        ThemeMode.dark => ThemeMode.system,
      };
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Solar Icon Kit',
      debugShowCheckedModeBanner: false,
      theme: _buildTheme(Brightness.light),
      darkTheme: _buildTheme(Brightness.dark),
      themeMode: _themeMode,
      home: HomePage(
        themeMode: _themeMode,
        onToggleTheme: _toggleTheme,
      ),
    );
  }

  ThemeData _buildTheme(Brightness brightness) {
    final scheme = ColorScheme.fromSeed(
      seedColor: const Color(0xFF6366F1),
      brightness: brightness,
    );
    return ThemeData(
      colorScheme: scheme,
      useMaterial3: true,
      scaffoldBackgroundColor: scheme.surface,
      appBarTheme: AppBarTheme(
        backgroundColor: scheme.surface,
        surfaceTintColor: Colors.transparent,
        scrolledUnderElevation: 0,
        elevation: 0,
        centerTitle: false,
      ),
      iconTheme: IconThemeData(color: scheme.onSurface, size: 22),
    );
  }
}

// ============================================================================
// HOME
// ============================================================================

class HomePage extends StatefulWidget {
  const HomePage({
    super.key,
    required this.themeMode,
    required this.onToggleTheme,
  });

  final ThemeMode themeMode;
  final VoidCallback onToggleTheme;

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final TextEditingController _searchController = TextEditingController();
  String _query = '';
  SolarIconStyle _style = SolarIconStyle.linear;

  static const double _iconSize = 40;

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

  List<String> get _filtered {
    final all = List<String>.from(SolarIcons.all);
    if (_query.trim().isEmpty) return all;
    final q = _query.trim().toLowerCase();
    return <String>[
      for (final n in all)
        if (n.contains(q)) n,
    ];
  }

  IconData get _themeIcon => switch (widget.themeMode) {
        ThemeMode.light => Icons.light_mode_outlined,
        ThemeMode.dark => Icons.dark_mode_outlined,
        ThemeMode.system => Icons.brightness_auto_outlined,
      };

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

    return Scaffold(
      body: SafeArea(
        child: CustomScrollView(
          slivers: [
            SliverToBoxAdapter(
              child: _Header(
                total: SolarIcons.all.length,
                filteredCount: icons.length,
                themeIcon: _themeIcon,
                onToggleTheme: widget.onToggleTheme,
              ),
            ),
            SliverPersistentHeader(
              pinned: true,
              delegate: _StickyControls(
                background: scheme.surface,
                border: scheme.outlineVariant.withOpacity(0.5),
                child: _ControlsPanel(
                  searchController: _searchController,
                  onSearchChanged: (v) => setState(() => _query = v),
                  style: _style,
                  onStyleChanged: (s) => setState(() => _style = s),
                ),
              ),
            ),
            if (icons.isEmpty)
              const SliverFillRemaining(
                hasScrollBody: false,
                child: _EmptyState(),
              )
            else
              SliverPadding(
                padding: const EdgeInsets.all(16),
                sliver: SliverGrid(
                  gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
                    maxCrossAxisExtent: 112,
                    mainAxisSpacing: 8,
                    crossAxisSpacing: 8,
                    childAspectRatio: 0.92,
                  ),
                  delegate: SliverChildBuilderDelegate(
                    (context, i) {
                      final name = icons[i];
                      return _IconCard(
                        name: name,
                        style: _style,
                        size: _iconSize,
                        onTap: () => _showDetails(name),
                      );
                    },
                    childCount: icons.length,
                  ),
                ),
              ),
          ],
        ),
      ),
    );
  }

  Future<void> _showDetails(String name) {
    return showModalBottomSheet<void>(
      context: context,
      isScrollControlled: true,
      showDragHandle: true,
      backgroundColor: Theme.of(context).colorScheme.surface,
      shape: const RoundedRectangleBorder(
        borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
      ),
      builder: (_) => IconDetailSheet(name: name, initialStyle: _style),
    );
  }
}

// ============================================================================
// HEADER
// ============================================================================

class _Header extends StatelessWidget {
  const _Header({
    required this.total,
    required this.filteredCount,
    required this.themeIcon,
    required this.onToggleTheme,
  });

  final int total;
  final int filteredCount;
  final IconData themeIcon;
  final VoidCallback onToggleTheme;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final scheme = theme.colorScheme;
    return Padding(
      padding: const EdgeInsets.fromLTRB(20, 16, 12, 8),
      child: Row(
        children: [
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisSize: MainAxisSize.min,
              children: [
                Text(
                  'Solar Icon Kit',
                  style: theme.textTheme.titleLarge?.copyWith(
                    fontWeight: FontWeight.w700,
                    letterSpacing: -0.3,
                  ),
                ),
                const SizedBox(height: 2),
                Text(
                  filteredCount == total
                      ? '${_formatNumber(total)} icons  ·  6 styles'
                      : '${_formatNumber(filteredCount)} of ${_formatNumber(total)} icons',
                  style: theme.textTheme.bodySmall?.copyWith(
                    color: scheme.onSurfaceVariant,
                  ),
                ),
              ],
            ),
          ),
          IconButton(
            onPressed: onToggleTheme,
            icon: Icon(themeIcon),
            tooltip: 'Toggle theme',
            style: IconButton.styleFrom(
              backgroundColor: scheme.surfaceContainerHighest,
              foregroundColor: scheme.onSurface,
            ),
          ),
        ],
      ),
    );
  }
}

String _formatNumber(int n) {
  final s = n.toString();
  final buf = StringBuffer();
  for (var i = 0; i < s.length; i++) {
    if (i > 0 && (s.length - i) % 3 == 0) buf.write(',');
    buf.write(s[i]);
  }
  return buf.toString();
}

// ============================================================================
// CONTROLS
// ============================================================================

class _ControlsPanel extends StatelessWidget {
  const _ControlsPanel({
    required this.searchController,
    required this.onSearchChanged,
    required this.style,
    required this.onStyleChanged,
  });

  final TextEditingController searchController;
  final ValueChanged<String> onSearchChanged;
  final SolarIconStyle style;
  final ValueChanged<SolarIconStyle> onStyleChanged;

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        mainAxisSize: MainAxisSize.min,
        children: [
          TextField(
            controller: searchController,
            onChanged: onSearchChanged,
            textInputAction: TextInputAction.search,
            decoration: InputDecoration(
              filled: true,
              fillColor: scheme.surfaceContainerHighest,
              hintText: 'Search 1,231 icons...',
              prefixIcon: const Padding(
                padding: EdgeInsets.symmetric(horizontal: 14),
                child: SolarIcon(SolarIcons.magnifer, size: 18),
              ),
              prefixIconConstraints:
                  const BoxConstraints(minWidth: 48, minHeight: 48),
              suffixIcon: ValueListenableBuilder<TextEditingValue>(
                valueListenable: searchController,
                builder: (context, value, _) {
                  if (value.text.isEmpty) return const SizedBox.shrink();
                  return IconButton(
                    icon: const SolarIcon(SolarIcons.closeCircle, size: 18),
                    onPressed: () {
                      searchController.clear();
                      onSearchChanged('');
                    },
                  );
                },
              ),
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(28),
                borderSide: BorderSide.none,
              ),
              enabledBorder: OutlineInputBorder(
                borderRadius: BorderRadius.circular(28),
                borderSide: BorderSide.none,
              ),
              focusedBorder: OutlineInputBorder(
                borderRadius: BorderRadius.circular(28),
                borderSide: BorderSide(color: scheme.primary, width: 1.5),
              ),
              contentPadding:
                  const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
            ),
          ),
          const SizedBox(height: 12),
          _StyleSelector(
            value: style,
            onChanged: onStyleChanged,
          ),
        ],
      ),
    );
  }
}

class _StyleSelector extends StatelessWidget {
  const _StyleSelector({required this.value, required this.onChanged});

  final SolarIconStyle value;
  final ValueChanged<SolarIconStyle> onChanged;

  static const _labels = {
    SolarIconStyle.linear: 'Linear',
    SolarIconStyle.outline: 'Outline',
    SolarIconStyle.broken: 'Broken',
    SolarIconStyle.bold: 'Bold',
    SolarIconStyle.lineDuotone: 'Line Duo',
    SolarIconStyle.boldDuotone: 'Bold Duo',
  };

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    return Container(
      height: 36,
      decoration: BoxDecoration(
        color: scheme.surfaceContainerHighest,
        borderRadius: BorderRadius.circular(999),
      ),
      child: ListView.separated(
        scrollDirection: Axis.horizontal,
        padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
        itemCount: SolarIconStyle.values.length,
        separatorBuilder: (_, __) => const SizedBox(width: 2),
        itemBuilder: (context, i) {
          final s = SolarIconStyle.values[i];
          final selected = s == value;
          return _SegmentChip(
            selected: selected,
            onTap: () => onChanged(s),
            child: Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                SolarIcon(SolarIcons.home2, style: s, size: 14),
                const SizedBox(width: 6),
                Text(_labels[s]!),
              ],
            ),
          );
        },
      ),
    );
  }
}

class _SegmentChip extends StatelessWidget {
  const _SegmentChip({
    required this.selected,
    required this.onTap,
    required this.child,
  });

  final bool selected;
  final VoidCallback onTap;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    return AnimatedContainer(
      duration: const Duration(milliseconds: 150),
      curve: Curves.easeOut,
      decoration: BoxDecoration(
        color: selected ? scheme.surface : Colors.transparent,
        borderRadius: BorderRadius.circular(999),
        boxShadow: selected
            ? [
                BoxShadow(
                  color: scheme.shadow.withOpacity(0.06),
                  blurRadius: 4,
                  offset: const Offset(0, 1),
                ),
              ]
            : null,
      ),
      child: Material(
        color: Colors.transparent,
        borderRadius: BorderRadius.circular(999),
        clipBehavior: Clip.antiAlias,
        child: InkWell(
          onTap: onTap,
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
            child: DefaultTextStyle.merge(
              style: TextStyle(
                fontSize: 12,
                fontWeight: FontWeight.w600,
                color: selected ? scheme.onSurface : scheme.onSurfaceVariant,
              ),
              child: IconTheme(
                data: IconThemeData(
                  size: 14,
                  color: selected
                      ? scheme.onSurface
                      : scheme.onSurfaceVariant,
                ),
                child: child,
              ),
            ),
          ),
        ),
      ),
    );
  }
}

// ============================================================================
// STICKY CONTROLS DELEGATE
// ============================================================================

class _StickyControls extends SliverPersistentHeaderDelegate {
  _StickyControls({
    required this.background,
    required this.border,
    required this.child,
  });

  final Color background;
  final Color border;
  final Widget child;

  @override
  double get minExtent => 128;

  @override
  double get maxExtent => 128;

  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    return Container(
      color: background,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Expanded(child: child),
          Divider(height: 1, color: border),
        ],
      ),
    );
  }

  @override
  bool shouldRebuild(_StickyControls old) =>
      old.background != background ||
      old.border != border ||
      old.child != child;
}

// ============================================================================
// ICON CARD
// ============================================================================

class _IconCard extends StatelessWidget {
  const _IconCard({
    required this.name,
    required this.style,
    required this.size,
    required this.onTap,
  });

  final String name;
  final SolarIconStyle style;
  final double size;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    return Material(
      color: scheme.surfaceContainerLowest,
      borderRadius: BorderRadius.circular(14),
      clipBehavior: Clip.antiAlias,
      child: InkWell(
        onTap: onTap,
        splashColor: scheme.primary.withOpacity(0.08),
        highlightColor: scheme.primary.withOpacity(0.04),
        child: Container(
          decoration: BoxDecoration(
            border: Border.all(
              color: scheme.outlineVariant.withOpacity(0.4),
            ),
            borderRadius: BorderRadius.circular(14),
          ),
          padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Expanded(
                child: Center(
                  child: SolarIcon(name, style: style, size: size),
                ),
              ),
              const SizedBox(height: 8),
              Text(
                name,
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
                textAlign: TextAlign.center,
                style: TextStyle(
                  fontFamily: 'monospace',
                  fontSize: 10,
                  color: scheme.onSurfaceVariant,
                  height: 1.2,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

// ============================================================================
// EMPTY STATE
// ============================================================================

class _EmptyState extends StatelessWidget {
  const _EmptyState();

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    return Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          SolarIcon(
            SolarIcons.magniferBug,
            style: SolarIconStyle.boldDuotone,
            size: 48,
            color: scheme.onSurfaceVariant,
          ),
          const SizedBox(height: 12),
          Text(
            'No icons match your search',
            style: TextStyle(color: scheme.onSurfaceVariant),
          ),
        ],
      ),
    );
  }
}

// ============================================================================
// DETAIL SHEET
// ============================================================================

class IconDetailSheet extends StatefulWidget {
  const IconDetailSheet({
    super.key,
    required this.name,
    required this.initialStyle,
  });

  final String name;
  final SolarIconStyle initialStyle;

  @override
  State<IconDetailSheet> createState() => _IconDetailSheetState();
}

class _IconDetailSheetState extends State<IconDetailSheet> {
  late SolarIconStyle _style = widget.initialStyle;

  static const _labels = {
    SolarIconStyle.linear: 'Linear',
    SolarIconStyle.outline: 'Outline',
    SolarIconStyle.broken: 'Broken',
    SolarIconStyle.bold: 'Bold',
    SolarIconStyle.lineDuotone: 'Line Duo',
    SolarIconStyle.boldDuotone: 'Bold Duo',
  };

  String get _dartIdentifier {
    final parts = widget.name.split(RegExp('[-_]+'));
    final head = parts.first.toLowerCase();
    final tail = parts.skip(1).map((p) {
      if (p.isEmpty) return '';
      return p[0].toUpperCase() + p.substring(1).toLowerCase();
    }).join();
    var ident = head + tail;
    if (ident.isNotEmpty && RegExp(r'^[0-9]').hasMatch(ident)) {
      ident = 'i$ident';
    }
    return ident;
  }

  String get _flutterCode =>
      'SolarIcon(SolarIcons.$_dartIdentifier, style: SolarIconStyle.${_style.name})';

  Future<void> _copy(String text, String label) async {
    await Clipboard.setData(ClipboardData(text: text));
    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text('Copied $label'),
        behavior: SnackBarBehavior.floating,
        duration: const Duration(seconds: 2),
      ),
    );
  }

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

    return DraggableScrollableSheet(
      expand: false,
      initialChildSize: 0.75,
      minChildSize: 0.4,
      maxChildSize: 0.95,
      builder: (context, controller) => SingleChildScrollView(
        controller: controller,
        padding: const EdgeInsets.fromLTRB(20, 8, 20, 32),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Row(
              children: [
                Expanded(
                  child: Text(
                    widget.name,
                    style: theme.textTheme.titleLarge?.copyWith(
                      fontWeight: FontWeight.w700,
                      fontFamily: 'monospace',
                    ),
                  ),
                ),
                Container(
                  padding: const EdgeInsets.symmetric(
                      horizontal: 10, vertical: 4),
                  decoration: BoxDecoration(
                    color: scheme.primaryContainer,
                    borderRadius: BorderRadius.circular(999),
                  ),
                  child: Text(
                    _labels[_style]!,
                    style: TextStyle(
                      color: scheme.onPrimaryContainer,
                      fontSize: 11,
                      fontWeight: FontWeight.w600,
                    ),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 20),
            Container(
              height: 200,
              decoration: BoxDecoration(
                color: scheme.surfaceContainerHighest,
                borderRadius: BorderRadius.circular(20),
                border: Border.all(
                  color: scheme.outlineVariant.withOpacity(0.5),
                ),
              ),
              alignment: Alignment.center,
              child: SolarIcon(
                widget.name,
                style: _style,
                size: 96,
                color: scheme.primary,
              ),
            ),
            const SizedBox(height: 20),
            Text(
              'All styles',
              style: theme.textTheme.labelLarge?.copyWith(
                fontWeight: FontWeight.w700,
              ),
            ),
            const SizedBox(height: 8),
            SizedBox(
              height: 92,
              child: ListView.separated(
                scrollDirection: Axis.horizontal,
                itemCount: SolarIconStyle.values.length,
                separatorBuilder: (_, __) => const SizedBox(width: 8),
                itemBuilder: (context, i) {
                  final s = SolarIconStyle.values[i];
                  final selected = s == _style;
                  return Material(
                    color: selected
                        ? scheme.primaryContainer
                        : scheme.surfaceContainerHighest,
                    borderRadius: BorderRadius.circular(14),
                    clipBehavior: Clip.antiAlias,
                    child: InkWell(
                      onTap: () => setState(() => _style = s),
                      child: Container(
                        width: 84,
                        padding: const EdgeInsets.all(12),
                        decoration: selected
                            ? null
                            : BoxDecoration(
                                borderRadius: BorderRadius.circular(14),
                                border: Border.all(
                                  color: scheme.outlineVariant
                                      .withOpacity(0.5),
                                ),
                              ),
                        child: Column(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            SolarIcon(
                              widget.name,
                              style: s,
                              size: 32,
                              color: selected
                                  ? scheme.onPrimaryContainer
                                  : scheme.onSurface,
                            ),
                            const SizedBox(height: 8),
                            Text(
                              _labels[s]!,
                              style: TextStyle(
                                fontSize: 10,
                                fontWeight: FontWeight.w600,
                                color: selected
                                    ? scheme.onPrimaryContainer
                                    : scheme.onSurfaceVariant,
                              ),
                            ),
                          ],
                        ),
                      ),
                    ),
                  );
                },
              ),
            ),
            const SizedBox(height: 20),
            Text(
              'Flutter usage',
              style: theme.textTheme.labelLarge?.copyWith(
                fontWeight: FontWeight.w700,
              ),
            ),
            const SizedBox(height: 8),
            Container(
              padding: const EdgeInsets.all(14),
              decoration: BoxDecoration(
                color: scheme.surfaceContainerHighest,
                borderRadius: BorderRadius.circular(14),
                border: Border.all(
                  color: scheme.outlineVariant.withOpacity(0.5),
                ),
              ),
              child: SelectableText(
                _flutterCode,
                style: TextStyle(
                  fontFamily: 'monospace',
                  fontSize: 12,
                  color: scheme.onSurface,
                  height: 1.5,
                ),
              ),
            ),
            const SizedBox(height: 12),
            Row(
              children: [
                Expanded(
                  child: FilledButton.icon(
                    onPressed: () => _copy(_flutterCode, 'Flutter code'),
                    icon: const SolarIcon(
                      SolarIcons.copy,
                      style: SolarIconStyle.linear,
                      size: 18,
                      color: Colors.white,
                    ),
                    label: const Text('Copy code'),
                    style: FilledButton.styleFrom(
                      minimumSize: const Size(0, 48),
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(14),
                      ),
                    ),
                  ),
                ),
                const SizedBox(width: 8),
                Expanded(
                  child: OutlinedButton.icon(
                    onPressed: () => _copy(widget.name, 'icon name'),
                    icon: const SolarIcon(SolarIcons.tag, size: 18),
                    label: const Text('Copy name'),
                    style: OutlinedButton.styleFrom(
                      minimumSize: const Size(0, 48),
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(14),
                      ),
                    ),
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
2
likes
0
points
0
downloads

Publisher

unverified uploader

Weekly Downloads

Solar icon kit for Flutter. 1,231 icons in 6 native styles — linear, outline, broken, bold, line-duotone, bold-duotone — behind a single SolarIcon widget.

Homepage
Repository (GitHub)
View/report issues

Topics

#icons #svg #ui #iconography #widget

Funding

Consider supporting this project:

ko-fi.com

License

unknown (license)

Dependencies

flutter, flutter_svg

More

Packages that depend on solar_iconkit