navii_flutter 0.2.1 copy "navii_flutter: ^0.2.1" to clipboard
navii_flutter: ^0.2.1 copied to clipboard

Deterministic mascot-style avatar widget for Flutter. Drop-in avatar generation — same seed = same mascot, every time. No database, no uploads, no state required.

example/lib/main.dart

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

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

class NaviiShowcaseApp extends StatelessWidget {
  const NaviiShowcaseApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'navii_flutter',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF6366F1),
          brightness: Brightness.dark,
        ),
        scaffoldBackgroundColor: const Color(0xFF0F172A),
        cardColor: const Color(0xFF1E293B),
        dividerColor: const Color(0xFF334155),
        textTheme: const TextTheme(
          bodyMedium: TextStyle(color: Color(0xFFCBD5E1)),
          bodySmall: TextStyle(color: Color(0xFF94A3B8)),
        ),
      ),
      home: const _Shell(),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Shell
// ─────────────────────────────────────────────────────────────────────────────

class _Shell extends StatefulWidget {
  const _Shell();
  @override
  State<_Shell> createState() => _ShellState();
}

class _ShellState extends State<_Shell> {
  int _tab = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _tab,
        children: const [
          UseCasesTab(),
          TryItTab(),
          PickerTab(),
          GalleryTab(),
          CustomizeTab(),
        ],
      ),
      bottomNavigationBar: NavigationBar(
        selectedIndex: _tab,
        onDestinationSelected: (i) => setState(() => _tab = i),
        backgroundColor: const Color(0xFF1E293B),
        indicatorColor: const Color(0xFF6366F1),
        destinations: const [
          NavigationDestination(
            icon: Icon(Icons.dashboard_outlined),
            selectedIcon: Icon(Icons.dashboard),
            label: 'Use Cases',
          ),
          NavigationDestination(
            icon: Icon(Icons.person_outlined),
            selectedIcon: Icon(Icons.person),
            label: 'Try It',
          ),
          NavigationDestination(
            icon: Icon(Icons.touch_app_outlined),
            selectedIcon: Icon(Icons.touch_app),
            label: 'Picker',
          ),
          NavigationDestination(
            icon: Icon(Icons.grid_view_outlined),
            selectedIcon: Icon(Icons.grid_view),
            label: 'Gallery',
          ),
          NavigationDestination(
            icon: Icon(Icons.tune_outlined),
            selectedIcon: Icon(Icons.tune),
            label: 'Customize',
          ),
        ],
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Tab 1 — Use Cases
// ─────────────────────────────────────────────────────────────────────────────

class UseCasesTab extends StatelessWidget {
  const UseCasesTab({super.key});

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 3,
      child: Scaffold(
        backgroundColor: const Color(0xFF0F172A),
        appBar: AppBar(
          backgroundColor: const Color(0xFF1E293B),
          title: RichText(
            text: const TextSpan(
              style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
              children: [
                TextSpan(
                  text: 'navii',
                  style: TextStyle(color: Color(0xFF818CF8)),
                ),
                TextSpan(
                  text: '_flutter',
                  style: TextStyle(color: Color(0xFFCBD5E1)),
                ),
                TextSpan(
                  text: '  use cases',
                  style: TextStyle(
                    color: Color(0xFF64748B),
                    fontWeight: FontWeight.normal,
                    fontSize: 14,
                  ),
                ),
              ],
            ),
          ),
          bottom: const TabBar(
            labelColor: Color(0xFF818CF8),
            unselectedLabelColor: Color(0xFF64748B),
            indicatorColor: Color(0xFF6366F1),
            tabs: [
              Tab(text: 'Chat'),
              Tab(text: 'Comments'),
              Tab(text: 'Contacts'),
            ],
          ),
        ),
        body: const TabBarView(
          children: [_ChatDemo(), _CommentsDemo(), _ContactsDemo()],
        ),
      ),
    );
  }
}

// ── Chat Demo ────────────────────────────────────────────────────────────────

const _chatConversations = [
  _Convo('Alice Mensah', 'Sounds good, see you then! 👋', '2m', true, 3),
  _Convo('Bob Asante', 'Did you get my last message?', '14m', false, 0),
  _Convo('Carol Adjei', 'The meeting is at 3pm tomorrow', '1h', true, 1),
  _Convo(
    'David Owusu',
    'haha yeah exactly what I was thinking',
    '2h',
    false,
    0,
  ),
  _Convo(
    'Eve Boateng',
    'Can you review the PR when you get a chance?',
    '3h',
    true,
    0,
  ),
  _Convo('Frank Darko', 'On my way!', '5h', false, 0),
  _Convo('Grace Amponsah', 'Thanks for the help earlier 🙏', 'Mon', true, 0),
  _Convo(
    'Henry Acheampong',
    'I\'ll send the files by end of day',
    'Mon',
    false,
    0,
  ),
  _Convo('Iris Opoku', 'Happy birthday!! 🎉🎂', 'Sun', true, 0),
  _Convo('Jack Frimpong', 'Let me know what you think', 'Sun', false, 0),
];

class _Convo {
  final String name;
  final String message;
  final String time;
  final bool online;
  final int unread;
  const _Convo(this.name, this.message, this.time, this.online, this.unread);
}

class _ChatDemo extends StatelessWidget {
  const _ChatDemo();

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        _SearchBar(hint: 'Search messages...'),
        Expanded(
          child: ListView.builder(
            itemCount: _chatConversations.length,
            itemBuilder: (context, i) {
              final c = _chatConversations[i];
              return _ChatTile(convo: c);
            },
          ),
        ),
      ],
    );
  }
}

class _ChatTile extends StatelessWidget {
  final _Convo convo;
  const _ChatTile({required this.convo});

  @override
  Widget build(BuildContext context) {
    return ListTile(
      contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
      leading: Stack(
        children: [
          Navii(seed: convo.name, size: 48),
          if (convo.online)
            Positioned(
              right: 0,
              bottom: 0,
              child: Container(
                width: 12,
                height: 12,
                decoration: BoxDecoration(
                  color: const Color(0xFF22C55E),
                  shape: BoxShape.circle,
                  border: Border.all(color: const Color(0xFF0F172A), width: 2),
                ),
              ),
            ),
        ],
      ),
      title: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(
            convo.name,
            style: const TextStyle(
              fontWeight: FontWeight.w600,
              color: Color(0xFFE2E8F0),
              fontSize: 15,
            ),
          ),
          Text(
            convo.time,
            style: TextStyle(
              fontSize: 12,
              color: convo.unread > 0
                  ? const Color(0xFF818CF8)
                  : const Color(0xFF64748B),
            ),
          ),
        ],
      ),
      subtitle: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Expanded(
            child: Text(
              convo.message,
              maxLines: 1,
              overflow: TextOverflow.ellipsis,
              style: TextStyle(
                fontSize: 13,
                color: convo.unread > 0
                    ? const Color(0xFFCBD5E1)
                    : const Color(0xFF64748B),
              ),
            ),
          ),
          if (convo.unread > 0)
            Container(
              margin: const EdgeInsets.only(left: 8),
              padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
              decoration: BoxDecoration(
                color: const Color(0xFF6366F1),
                borderRadius: BorderRadius.circular(10),
              ),
              child: Text(
                '${convo.unread}',
                style: const TextStyle(
                  fontSize: 11,
                  color: Colors.white,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),
        ],
      ),
    );
  }
}

// ── Comments Demo ────────────────────────────────────────────────────────────

const _comments = [
  _Comment(
    'Noah Tetteh',
    '2m ago',
    'This is exactly what I\'ve been looking for. The determinism aspect is really clever.',
    24,
  ),
  _Comment(
    'Mia Asare',
    '8m ago',
    'How does it handle unicode names like "김민준" or "محمد"?',
    7,
  ),
  _Comment(
    'Liam Antwi',
    '15m ago',
    'Replying to @Mia — yes it works! Just tested it. Every character maps correctly.',
    12,
  ),
  _Comment(
    'Olivia Mensah',
    '1h ago',
    'Just published a post about this on my blog 🎉',
    31,
  ),
  _Comment(
    'Peter Asante',
    '2h ago',
    'The offline-first approach is a big win for us. No more broken avatar images.',
    9,
  ),
  _Comment(
    'Quinn Osei',
    '3h ago',
    'Does it support custom palettes at runtime?',
    5,
  ),
];

class _Comment {
  final String name;
  final String time;
  final String text;
  final int likes;
  const _Comment(this.name, this.time, this.text, this.likes);
}

class _CommentsDemo extends StatelessWidget {
  const _CommentsDemo();

  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        // Post card
        Container(
          padding: const EdgeInsets.all(16),
          decoration: BoxDecoration(
            color: const Color(0xFF1E293B),
            borderRadius: BorderRadius.circular(12),
            border: Border.all(color: const Color(0xFF334155)),
          ),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Row(
                children: [
                  Navii(seed: 'Samuel Darko', size: 40),
                  const SizedBox(width: 10),
                  Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: const [
                      Text(
                        'Samuel Darko',
                        style: TextStyle(
                          fontWeight: FontWeight.bold,
                          color: Color(0xFFE2E8F0),
                        ),
                      ),
                      Text(
                        'Just shipped navii_flutter v0.1.0 🚀',
                        style: TextStyle(
                          fontSize: 12,
                          color: Color(0xFF64748B),
                        ),
                      ),
                    ],
                  ),
                ],
              ),
              const SizedBox(height: 12),
              const Text(
                'Drop-in deterministic mascot avatars for Flutter. '
                'No database, no uploads, works offline. '
                'Same user ID → same avatar, always. '
                'Check out the package on pub.dev 👇',
                style: TextStyle(color: Color(0xFFCBD5E1), height: 1.5),
              ),
              const SizedBox(height: 12),
              Row(
                children: [
                  const Icon(
                    Icons.favorite_border,
                    size: 18,
                    color: Color(0xFF64748B),
                  ),
                  const SizedBox(width: 4),
                  const Text(
                    '142',
                    style: TextStyle(fontSize: 13, color: Color(0xFF64748B)),
                  ),
                  const SizedBox(width: 16),
                  const Icon(
                    Icons.comment_outlined,
                    size: 18,
                    color: Color(0xFF64748B),
                  ),
                  const SizedBox(width: 4),
                  Text(
                    '${_comments.length}',
                    style: const TextStyle(
                      fontSize: 13,
                      color: Color(0xFF64748B),
                    ),
                  ),
                ],
              ),
            ],
          ),
        ),
        const SizedBox(height: 16),
        const Text(
          'Comments',
          style: TextStyle(
            fontWeight: FontWeight.bold,
            color: Color(0xFFE2E8F0),
            fontSize: 15,
          ),
        ),
        const SizedBox(height: 8),
        ..._comments.map((c) => _CommentTile(comment: c)),
      ],
    );
  }
}

class _CommentTile extends StatelessWidget {
  final _Comment comment;
  const _CommentTile({required this.comment});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 10),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Navii(seed: comment.name, size: 36),
          const SizedBox(width: 10),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Row(
                  children: [
                    Text(
                      comment.name,
                      style: const TextStyle(
                        fontWeight: FontWeight.w600,
                        color: Color(0xFFE2E8F0),
                        fontSize: 13,
                      ),
                    ),
                    const SizedBox(width: 8),
                    Text(
                      comment.time,
                      style: const TextStyle(
                        fontSize: 11,
                        color: Color(0xFF64748B),
                      ),
                    ),
                  ],
                ),
                const SizedBox(height: 4),
                Text(
                  comment.text,
                  style: const TextStyle(
                    fontSize: 13,
                    color: Color(0xFFCBD5E1),
                    height: 1.4,
                  ),
                ),
                const SizedBox(height: 6),
                Row(
                  children: [
                    const Icon(
                      Icons.favorite_border,
                      size: 14,
                      color: Color(0xFF64748B),
                    ),
                    const SizedBox(width: 4),
                    Text(
                      '${comment.likes}',
                      style: const TextStyle(
                        fontSize: 12,
                        color: Color(0xFF64748B),
                      ),
                    ),
                    const SizedBox(width: 16),
                    const Text(
                      'Reply',
                      style: TextStyle(fontSize: 12, color: Color(0xFF818CF8)),
                    ),
                  ],
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

// ── Contacts Demo ────────────────────────────────────────────────────────────

const _contacts = {
  'A': ['Alice Mensah', 'Ama Owusu', 'Asante Kwame'],
  'B': ['Bob Darko', 'Bright Frimpong'],
  'C': ['Carol Adjei', 'Charles Boateng', 'Cynthia Asare'],
  'D': ['David Antwi', 'Diana Tetteh'],
  'E': ['Emmanuel Osei', 'Eve Opoku'],
  'F': ['Frank Amponsah', 'Fatima Acheampong'],
  'G': ['Grace Asante', 'Gabriel Mensah'],
};

class _ContactsDemo extends StatelessWidget {
  const _ContactsDemo();

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        _SearchBar(hint: 'Search contacts...'),
        Expanded(
          child: ListView(
            children: _contacts.entries
                .expand(
                  (entry) => [
                    Padding(
                      padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
                      child: Text(
                        entry.key,
                        style: const TextStyle(
                          fontSize: 12,
                          fontWeight: FontWeight.bold,
                          color: Color(0xFF6366F1),
                          letterSpacing: 1,
                        ),
                      ),
                    ),
                    ...entry.value.map(
                      (name) => ListTile(
                        leading: Navii(seed: name, size: 44),
                        title: Text(
                          name,
                          style: const TextStyle(
                            color: Color(0xFFE2E8F0),
                            fontWeight: FontWeight.w500,
                          ),
                        ),
                        subtitle: const Text(
                          'Tap to view profile',
                          style: TextStyle(
                            fontSize: 12,
                            color: Color(0xFF64748B),
                          ),
                        ),
                        trailing: const Icon(
                          Icons.chevron_right,
                          color: Color(0xFF475569),
                        ),
                      ),
                    ),
                  ],
                )
                .toList(),
          ),
        ),
      ],
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Tab 2 — Try It (Auth Flow + Live Preview)
// ─────────────────────────────────────────────────────────────────────────────

class TryItTab extends StatefulWidget {
  const TryItTab({super.key});
  @override
  State<TryItTab> createState() => _TryItTabState();
}

class _TryItTabState extends State<TryItTab> {
  // Simple in-memory "users" store
  final Map<String, String> _users = {};
  String? _loggedInUser;
  String? _error;

  final _nameController = TextEditingController();
  final _liveController = TextEditingController();

  @override
  void dispose() {
    _nameController.dispose();
    _liveController.dispose();
    super.dispose();
  }

  void _signUp(String name) {
    final trimmed = name.trim();
    if (trimmed.isEmpty) return;
    if (_users.containsKey(trimmed.toLowerCase())) {
      setState(
        () => _error = 'User "$trimmed" already exists. Sign in instead.',
      );
      return;
    }
    setState(() {
      _users[trimmed.toLowerCase()] = trimmed;
      _loggedInUser = trimmed;
      _error = null;
    });
  }

  void _signIn(String name) {
    final trimmed = name.trim();
    if (trimmed.isEmpty) return;
    if (!_users.containsKey(trimmed.toLowerCase())) {
      setState(() => _error = 'No account for "$trimmed". Sign up first.');
      return;
    }
    setState(() {
      _loggedInUser = _users[trimmed.toLowerCase()];
      _error = null;
    });
  }

  void _signOut() => setState(() => _loggedInUser = null);

  @override
  Widget build(BuildContext context) {
    if (_loggedInUser != null) {
      return _ProfileScreen(
        user: _loggedInUser!,
        allUsers: _users,
        onSignOut: _signOut,
      );
    }
    return _AuthScreen(
      users: _users,
      error: _error,
      liveController: _liveController,
      nameController: _nameController,
      onSignUp: _signUp,
      onSignIn: _signIn,
    );
  }
}

class _AuthScreen extends StatefulWidget {
  final Map<String, String> users;
  final String? error;
  final TextEditingController liveController;
  final TextEditingController nameController;
  final void Function(String) onSignUp;
  final void Function(String) onSignIn;

  const _AuthScreen({
    required this.users,
    required this.error,
    required this.liveController,
    required this.nameController,
    required this.onSignUp,
    required this.onSignIn,
  });

  @override
  State<_AuthScreen> createState() => _AuthScreenState();
}

class _AuthScreenState extends State<_AuthScreen>
    with SingleTickerProviderStateMixin {
  late final TabController _tabController;
  String _liveSeed = '';

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 2, vsync: this);
    widget.liveController.addListener(() {
      setState(() => _liveSeed = widget.liveController.text.trim());
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFF0F172A),
      appBar: AppBar(
        backgroundColor: const Color(0xFF1E293B),
        title: const Text('navii_flutter — Try It'),
        bottom: TabBar(
          controller: _tabController,
          labelColor: const Color(0xFF818CF8),
          unselectedLabelColor: const Color(0xFF64748B),
          indicatorColor: const Color(0xFF6366F1),
          tabs: const [
            Tab(text: 'Sign Up'),
            Tab(text: 'Sign In'),
          ],
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        children: [_buildSignUp(), _buildSignIn()],
      ),
    );
  }

  Widget _buildSignUp() {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(24),
      child: Column(
        children: [
          const SizedBox(height: 16),
          // Live avatar preview
          AnimatedContainer(
            duration: const Duration(milliseconds: 300),
            curve: Curves.easeOut,
            child: _liveSeed.isNotEmpty
                ? Column(
                    children: [
                      Navii(seed: _liveSeed, size: 96),
                      const SizedBox(height: 8),
                      Text(
                        'This will be your avatar',
                        style: TextStyle(
                          fontSize: 13,
                          color: Colors.grey.shade500,
                        ),
                      ),
                    ],
                  )
                : Column(
                    children: [
                      Container(
                        width: 96,
                        height: 96,
                        decoration: BoxDecoration(
                          color: const Color(0xFF1E293B),
                          shape: BoxShape.circle,
                          border: Border.all(
                            color: const Color(0xFF334155),
                            width: 2,
                          ),
                        ),
                        child: const Icon(
                          Icons.person,
                          size: 40,
                          color: Color(0xFF475569),
                        ),
                      ),
                      const SizedBox(height: 8),
                      const Text(
                        'Enter your name to preview your avatar',
                        style: TextStyle(
                          fontSize: 13,
                          color: Color(0xFF64748B),
                        ),
                      ),
                    ],
                  ),
          ),
          const SizedBox(height: 32),
          TextField(
            controller: widget.liveController,
            style: const TextStyle(color: Color(0xFFE2E8F0)),
            decoration: _inputDecoration('Your name', Icons.person_outline),
            textCapitalization: TextCapitalization.words,
          ),
          const SizedBox(height: 12),
          if (widget.error != null) _ErrorBanner(widget.error!),
          const SizedBox(height: 12),
          SizedBox(
            width: double.infinity,
            child: FilledButton(
              onPressed: () => widget.onSignUp(widget.liveController.text),
              style: FilledButton.styleFrom(
                backgroundColor: const Color(0xFF6366F1),
                padding: const EdgeInsets.symmetric(vertical: 14),
              ),
              child: const Text(
                'Create Account',
                style: TextStyle(fontSize: 16),
              ),
            ),
          ),
          const SizedBox(height: 32),
          if (widget.users.isNotEmpty) ...[
            const _Divider('Existing accounts'),
            const SizedBox(height: 12),
            Wrap(
              spacing: 12,
              runSpacing: 12,
              children: widget.users.values
                  .map(
                    (name) => Column(
                      children: [
                        Navii(seed: name, size: 48),
                        const SizedBox(height: 4),
                        Text(
                          name.split(' ').first,
                          style: const TextStyle(
                            fontSize: 11,
                            color: Color(0xFF64748B),
                          ),
                        ),
                      ],
                    ),
                  )
                  .toList(),
            ),
          ],
          const SizedBox(height: 32),
          _DeterminismNote(),
        ],
      ),
    );
  }

  Widget _buildSignIn() {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(24),
      child: Column(
        children: [
          const SizedBox(height: 40),
          if (widget.users.isEmpty) ...[
            const Icon(
              Icons.person_add_outlined,
              size: 64,
              color: Color(0xFF334155),
            ),
            const SizedBox(height: 16),
            const Text(
              'No accounts yet.',
              style: TextStyle(color: Color(0xFF64748B)),
            ),
            const Text(
              'Create one in the Sign Up tab.',
              style: TextStyle(color: Color(0xFF475569), fontSize: 13),
            ),
            const SizedBox(height: 32),
          ] else ...[
            const Text(
              'Your accounts',
              style: TextStyle(
                fontWeight: FontWeight.bold,
                color: Color(0xFFE2E8F0),
              ),
            ),
            const SizedBox(height: 16),
            ...widget.users.values.map(
              (name) => ListTile(
                contentPadding: const EdgeInsets.symmetric(
                  horizontal: 0,
                  vertical: 4,
                ),
                leading: Navii(seed: name, size: 44),
                title: Text(
                  name,
                  style: const TextStyle(color: Color(0xFFE2E8F0)),
                ),
                subtitle: const Text(
                  'Tap to sign in',
                  style: TextStyle(fontSize: 12, color: Color(0xFF64748B)),
                ),
                trailing: const Icon(
                  Icons.arrow_forward_ios,
                  size: 14,
                  color: Color(0xFF475569),
                ),
                onTap: () => widget.onSignIn(name),
              ),
            ),
            const SizedBox(height: 16),
          ],
          TextField(
            controller: widget.nameController,
            style: const TextStyle(color: Color(0xFFE2E8F0)),
            decoration: _inputDecoration(
              'Or type your name',
              Icons.person_outline,
            ),
            textCapitalization: TextCapitalization.words,
          ),
          const SizedBox(height: 12),
          if (widget.error != null) _ErrorBanner(widget.error!),
          const SizedBox(height: 12),
          SizedBox(
            width: double.infinity,
            child: FilledButton(
              onPressed: () => widget.onSignIn(widget.nameController.text),
              style: FilledButton.styleFrom(
                backgroundColor: const Color(0xFF6366F1),
                padding: const EdgeInsets.symmetric(vertical: 14),
              ),
              child: const Text('Sign In', style: TextStyle(fontSize: 16)),
            ),
          ),
        ],
      ),
    );
  }
}

class _ProfileScreen extends StatelessWidget {
  final String user;
  final Map<String, String> allUsers;
  final VoidCallback onSignOut;

  const _ProfileScreen({
    required this.user,
    required this.allUsers,
    required this.onSignOut,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFF0F172A),
      appBar: AppBar(
        backgroundColor: const Color(0xFF1E293B),
        title: const Text('Profile'),
        actions: [
          TextButton(
            onPressed: onSignOut,
            child: const Text(
              'Sign Out',
              style: TextStyle(color: Color(0xFF818CF8)),
            ),
          ),
        ],
      ),
      body: SingleChildScrollView(
        child: Column(
          children: [
            // Profile header
            Container(
              width: double.infinity,
              padding: const EdgeInsets.symmetric(vertical: 40),
              color: const Color(0xFF1E293B),
              child: Column(
                children: [
                  Navii(seed: user, size: 100),
                  const SizedBox(height: 12),
                  Text(
                    user,
                    style: const TextStyle(
                      fontSize: 22,
                      fontWeight: FontWeight.bold,
                      color: Color(0xFFE2E8F0),
                    ),
                  ),
                  const SizedBox(height: 4),
                  Text(
                    'Seed: "$user"',
                    style: const TextStyle(
                      fontSize: 12,
                      color: Color(0xFF64748B),
                      fontFamily: 'monospace',
                    ),
                  ),
                  const SizedBox(height: 16),
                  // Show avatar is stable — same user, 3 renders
                  Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      const Text(
                        'Always the same:  ',
                        style: TextStyle(
                          fontSize: 12,
                          color: Color(0xFF64748B),
                        ),
                      ),
                      ...List.generate(
                        3,
                        (i) => Padding(
                          padding: const EdgeInsets.symmetric(horizontal: 3),
                          child: Navii(seed: user, size: 28),
                        ),
                      ),
                    ],
                  ),
                ],
              ),
            ),
            const SizedBox(height: 24),
            // Similar names — prove no collisions
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text(
                    'Similar names — all different avatars',
                    style: TextStyle(
                      fontWeight: FontWeight.bold,
                      color: Color(0xFFE2E8F0),
                    ),
                  ),
                  const SizedBox(height: 4),
                  const Text(
                    'Even one character difference produces a completely unique avatar.',
                    style: TextStyle(fontSize: 12, color: Color(0xFF94A3B8)),
                  ),
                  const SizedBox(height: 16),
                  ..._buildVariants(user),
                ],
              ),
            ),
            const SizedBox(height: 24),
            if (allUsers.length > 1) ...[
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Text(
                      'Other users',
                      style: TextStyle(
                        fontWeight: FontWeight.bold,
                        color: Color(0xFFE2E8F0),
                      ),
                    ),
                    const SizedBox(height: 12),
                    Wrap(
                      spacing: 16,
                      runSpacing: 16,
                      children: allUsers.values
                          .where((u) => u != user)
                          .map(
                            (u) => Column(
                              children: [
                                Navii(seed: u, size: 52),
                                const SizedBox(height: 4),
                                Text(
                                  u.split(' ').first,
                                  style: const TextStyle(
                                    fontSize: 11,
                                    color: Color(0xFF64748B),
                                  ),
                                ),
                              ],
                            ),
                          )
                          .toList(),
                    ),
                  ],
                ),
              ),
              const SizedBox(height: 24),
            ],
          ],
        ),
      ),
    );
  }

  List<Widget> _buildVariants(String name) {
    final base = name.trim();
    final variants = [
      base,
      '${base}1',
      '${base.split(' ').first} J.',
      base.toLowerCase(),
      base.toUpperCase(),
    ];
    return variants
        .map(
          (v) => Padding(
            padding: const EdgeInsets.symmetric(vertical: 6),
            child: Row(
              children: [
                Navii(seed: v, size: 40),
                const SizedBox(width: 12),
                Text(
                  '"$v"',
                  style: const TextStyle(
                    fontFamily: 'monospace',
                    fontSize: 13,
                    color: Color(0xFFCBD5E1),
                  ),
                ),
                if (v == base)
                  Container(
                    margin: const EdgeInsets.only(left: 8),
                    padding: const EdgeInsets.symmetric(
                      horizontal: 6,
                      vertical: 2,
                    ),
                    decoration: BoxDecoration(
                      color: const Color(0xFF1E3A8A),
                      borderRadius: BorderRadius.circular(4),
                    ),
                    child: const Text(
                      'you',
                      style: TextStyle(fontSize: 10, color: Color(0xFF93C5FD)),
                    ),
                  ),
              ],
            ),
          ),
        )
        .toList();
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Tab 3 — Picker
// ─────────────────────────────────────────────────────────────────────────────

class PickerTab extends StatefulWidget {
  const PickerTab({super.key});
  @override
  State<PickerTab> createState() => _PickerTabState();
}

class _PickerTabState extends State<PickerTab> {
  static const _baseSeed = 'showcase-profile';
  String _currentSeed = 'showcase-profile-navii-v0';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFF0F172A),
      appBar: AppBar(
        backgroundColor: const Color(0xFF1E293B),
        title: const Text('Avatar Picker'),
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // Current selection card
            _CurrentAvatarCard(seed: _currentSeed),
            const SizedBox(height: 28),

            // Inline picker
            const Text(
              'Inline Picker',
              style: TextStyle(
                fontSize: 15,
                fontWeight: FontWeight.bold,
                color: Color(0xFFE2E8F0),
              ),
            ),
            const SizedBox(height: 4),
            const Text(
              'NaviiPicker(baseSeed: ..., onSelected: ...)',
              style: TextStyle(
                fontFamily: 'monospace',
                fontSize: 11,
                color: Color(0xFF64748B),
              ),
            ),
            const SizedBox(height: 12),
            Container(
              padding: const EdgeInsets.all(16),
              decoration: BoxDecoration(
                color: const Color(0xFF1E293B),
                borderRadius: BorderRadius.circular(12),
                border: Border.all(color: const Color(0xFF334155)),
              ),
              child: NaviiPicker(
                baseSeed: _baseSeed,
                initialSeed: _currentSeed,
                confirmLabel: 'Use this avatar',
                onSelected: (seed) => setState(() => _currentSeed = seed),
              ),
            ),

            const SizedBox(height: 28),

            // Bottom sheet button
            const Text(
              'Bottom Sheet',
              style: TextStyle(
                fontSize: 15,
                fontWeight: FontWeight.bold,
                color: Color(0xFFE2E8F0),
              ),
            ),
            const SizedBox(height: 4),
            const Text(
              'showNaviiPickerSheet(context, baseSeed: ...)',
              style: TextStyle(
                fontFamily: 'monospace',
                fontSize: 11,
                color: Color(0xFF64748B),
              ),
            ),
            const SizedBox(height: 12),
            SizedBox(
              width: double.infinity,
              child: OutlinedButton.icon(
                onPressed: () async {
                  final seed = await showNaviiPickerSheet(
                    context,
                    baseSeed: _baseSeed,
                    initialSeed: _currentSeed,
                    title: 'Choose your avatar',
                    confirmLabel: 'Use this avatar',
                  );
                  if (seed != null) setState(() => _currentSeed = seed);
                },
                icon: const Icon(Icons.open_in_new, size: 16),
                label: const Text('Open picker in bottom sheet'),
                style: OutlinedButton.styleFrom(
                  foregroundColor: const Color(0xFF818CF8),
                  side: const BorderSide(color: Color(0xFF334155)),
                  padding: const EdgeInsets.symmetric(vertical: 14),
                ),
              ),
            ),

            const SizedBox(height: 28),

            // How it works note
            Container(
              padding: const EdgeInsets.all(14),
              decoration: BoxDecoration(
                color: const Color(0xFF1E293B),
                borderRadius: BorderRadius.circular(10),
                border: Border.all(color: const Color(0xFF334155)),
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: const [
                  Row(
                    children: [
                      Icon(
                        Icons.info_outline,
                        size: 14,
                        color: Color(0xFF818CF8),
                      ),
                      SizedBox(width: 6),
                      Text(
                        'Stable seeds',
                        style: TextStyle(
                          fontSize: 13,
                          fontWeight: FontWeight.bold,
                          color: Color(0xFFE2E8F0),
                        ),
                      ),
                    ],
                  ),
                  SizedBox(height: 6),
                  Text(
                    'Each variant seed is deterministic: '
                    '"showcase-profile-navii-v0", '
                    '"showcase-profile-navii-v1", etc. Store the chosen seed '
                    'in your database and render it anywhere with '
                    'Navii(seed: storedSeed).',
                    style: TextStyle(
                      fontSize: 12,
                      color: Color(0xFF94A3B8),
                      height: 1.5,
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class _CurrentAvatarCard extends StatelessWidget {
  final String seed;
  const _CurrentAvatarCard({required this.seed});

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.symmetric(vertical: 24),
      decoration: BoxDecoration(
        color: const Color(0xFF1E293B),
        borderRadius: BorderRadius.circular(16),
        border: Border.all(color: const Color(0xFF334155)),
      ),
      child: Column(
        children: [
          Navii(
            seed: seed,
            size: 80,
            borderColor: const Color(0xFF6366F1),
            borderWidth: 2.5,
          ),
          const SizedBox(height: 10),
          const Text(
            'Current avatar',
            style: TextStyle(
              fontSize: 13,
              fontWeight: FontWeight.bold,
              color: Color(0xFFE2E8F0),
            ),
          ),
          const SizedBox(height: 2),
          Text(
            seed,
            style: const TextStyle(
              fontFamily: 'monospace',
              fontSize: 11,
              color: Color(0xFF64748B),
            ),
            textAlign: TextAlign.center,
          ),
        ],
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Tab 4 — Gallery
// ─────────────────────────────────────────────────────────────────────────────

class GalleryTab extends StatelessWidget {
  const GalleryTab({super.key});

  static const _seeds = [
    'alice',
    'bob',
    'carol',
    'david',
    'eve',
    'frank',
    'grace',
    'henry',
    'iris',
    'jack',
    'kate',
    'liam',
    'mia',
    'noah',
    'olivia',
    'peter',
    'quinn',
    'rachel',
    'sam',
    'tina',
    'user-001',
    'user-002',
    'user-003',
    'user-004',
    'user-005',
    'user-006',
    'alpha',
    'beta',
    'gamma',
    'delta',
    'kofi',
    'ama',
    'kwame',
    'abena',
    'yaw',
    'akosua',
    'kojo',
    'adwoa',
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFF0F172A),
      appBar: AppBar(
        backgroundColor: const Color(0xFF1E293B),
        title: Text('Gallery  ·  ${_seeds.length} avatars'),
      ),
      body: GridView.builder(
        padding: const EdgeInsets.all(12),
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 5,
          mainAxisSpacing: 8,
          crossAxisSpacing: 8,
          childAspectRatio: 0.85,
        ),
        itemCount: _seeds.length,
        itemBuilder: (context, i) {
          final seed = _seeds[i];
          return Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Navii(seed: seed, size: 52),
              const SizedBox(height: 3),
              Text(
                seed,
                style: const TextStyle(fontSize: 9, color: Color(0xFF475569)),
                overflow: TextOverflow.ellipsis,
                maxLines: 1,
              ),
            ],
          );
        },
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Tab 5 — Customize
// ─────────────────────────────────────────────────────────────────────────────

class CustomizeTab extends StatelessWidget {
  const CustomizeTab({super.key});

  static const _palettes = [
    'indigo',
    'mint',
    'amber',
    'sky',
    'violet',
    'cyan',
    'rose',
    'lime',
    'peach',
    'teal',
    'sand',
    'plum',
    'coral',
    'forest',
    'slate',
    'fuchsia',
    'terracotta',
    'navy',
    'lavender',
    'charcoal',
    'butter',
    'aqua',
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFF0F172A),
      appBar: AppBar(
        backgroundColor: const Color(0xFF1E293B),
        title: const Text('Customize'),
      ),
      body: ListView(
        padding: const EdgeInsets.all(20),
        children: [
          // Sizes
          const Text(
            'Sizes',
            style: TextStyle(
              fontSize: 16,
              fontWeight: FontWeight.bold,
              color: Color(0xFFE2E8F0),
            ),
          ),
          const SizedBox(height: 4),
          const Text(
            'Navii(seed: ..., size: 64)',
            style: TextStyle(
              fontFamily: 'monospace',
              fontSize: 12,
              color: Color(0xFF64748B),
            ),
          ),
          const SizedBox(height: 16),
          ...[24.0, 32.0, 48.0, 64.0, 96.0, 128.0].map(
            (s) => Padding(
              padding: const EdgeInsets.only(bottom: 16),
              child: Row(
                children: [
                  Navii(seed: 'size-demo', size: s),
                  const SizedBox(width: 16),
                  Text(
                    '${s.toInt()}px',
                    style: const TextStyle(color: Color(0xFF94A3B8)),
                  ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 8),
          const Divider(color: Color(0xFF334155), height: 32),
          // Palettes
          const Text(
            '22 Palettes',
            style: TextStyle(
              fontSize: 16,
              fontWeight: FontWeight.bold,
              color: Color(0xFFE2E8F0),
            ),
          ),
          const SizedBox(height: 4),
          const Text(
            "AvatarOptions(paletteId: 'violet')",
            style: TextStyle(
              fontFamily: 'monospace',
              fontSize: 12,
              color: Color(0xFF64748B),
            ),
          ),
          const SizedBox(height: 16),
          GridView.builder(
            shrinkWrap: true,
            physics: const NeverScrollableScrollPhysics(),
            gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 4,
              mainAxisSpacing: 12,
              crossAxisSpacing: 12,
              childAspectRatio: 0.8,
            ),
            itemCount: _palettes.length,
            itemBuilder: (context, i) {
              final p = _palettes[i];
              return Column(
                children: [
                  Navii(
                    seed: 'palette-preview',
                    size: 56,
                    options: AvatarOptions(paletteId: p),
                  ),
                  const SizedBox(height: 4),
                  Text(
                    p,
                    style: const TextStyle(
                      fontSize: 9,
                      color: Color(0xFF475569),
                    ),
                    overflow: TextOverflow.ellipsis,
                  ),
                ],
              );
            },
          ),
          const Divider(color: Color(0xFF334155), height: 40),

          // Shapes
          const Text(
            'Shapes',
            style: TextStyle(
              fontSize: 16,
              fontWeight: FontWeight.bold,
              color: Color(0xFFE2E8F0),
            ),
          ),
          const SizedBox(height: 4),
          const Text(
            'Navii(seed: ..., shape: NaviiShape.circle)',
            style: TextStyle(
              fontFamily: 'monospace',
              fontSize: 12,
              color: Color(0xFF64748B),
            ),
          ),
          const SizedBox(height: 16),
          Row(
            children: [
              Column(
                children: [
                  Navii(seed: 'shape-demo', size: 64, shape: NaviiShape.circle),
                  const SizedBox(height: 4),
                  const Text(
                    'circle',
                    style: TextStyle(fontSize: 11, color: Color(0xFF64748B)),
                  ),
                ],
              ),
              const SizedBox(width: 24),
              Column(
                children: [
                  Navii(
                    seed: 'shape-demo',
                    size: 64,
                    shape: NaviiShape.rounded,
                  ),
                  const SizedBox(height: 4),
                  const Text(
                    'rounded',
                    style: TextStyle(fontSize: 11, color: Color(0xFF64748B)),
                  ),
                ],
              ),
              const SizedBox(width: 24),
              Column(
                children: [
                  Navii(seed: 'shape-demo', size: 64, shape: NaviiShape.square),
                  const SizedBox(height: 4),
                  const Text(
                    'square',
                    style: TextStyle(fontSize: 11, color: Color(0xFF64748B)),
                  ),
                ],
              ),
            ],
          ),
          const Divider(color: Color(0xFF334155), height: 40),

          // Status dots & borders
          const Text(
            'Status & Border',
            style: TextStyle(
              fontSize: 16,
              fontWeight: FontWeight.bold,
              color: Color(0xFFE2E8F0),
            ),
          ),
          const SizedBox(height: 4),
          const Text(
            'Navii(seed: ..., statusColor: ..., borderColor: ...)',
            style: TextStyle(
              fontFamily: 'monospace',
              fontSize: 12,
              color: Color(0xFF64748B),
            ),
          ),
          const SizedBox(height: 16),
          Wrap(
            spacing: 20,
            runSpacing: 16,
            children: [
              _LabelledAvatar(
                label: 'online',
                child: Navii(
                  seed: 'status-demo',
                  size: 56,
                  statusColor: const Color(0xFF22C55E),
                ),
              ),
              _LabelledAvatar(
                label: 'away',
                child: Navii(
                  seed: 'status-demo-2',
                  size: 56,
                  statusColor: const Color(0xFFF59E0B),
                  statusAlignment: Alignment.bottomRight,
                ),
              ),
              _LabelledAvatar(
                label: 'busy',
                child: Navii(
                  seed: 'status-demo-3',
                  size: 56,
                  statusColor: const Color(0xFFEF4444),
                ),
              ),
              _LabelledAvatar(
                label: 'border',
                child: Navii(
                  seed: 'border-demo',
                  size: 56,
                  borderColor: const Color(0xFF818CF8),
                  borderWidth: 3,
                ),
              ),
              _LabelledAvatar(
                label: 'both',
                child: Navii(
                  seed: 'border-demo-2',
                  size: 56,
                  borderColor: const Color(0xFF22C55E),
                  statusColor: const Color(0xFF22C55E),
                ),
              ),
            ],
          ),
          const Divider(color: Color(0xFF334155), height: 40),

          // Group avatars
          const Text(
            'Group Avatars',
            style: TextStyle(
              fontSize: 16,
              fontWeight: FontWeight.bold,
              color: Color(0xFFE2E8F0),
            ),
          ),
          const SizedBox(height: 4),
          const Text(
            'NaviiGroup(seeds: [...], size: 36, maxVisible: 3)',
            style: TextStyle(
              fontFamily: 'monospace',
              fontSize: 12,
              color: Color(0xFF64748B),
            ),
          ),
          const SizedBox(height: 16),
          NaviiGroup(
            seeds: const [
              'group-a',
              'group-b',
              'group-c',
              'group-d',
              'group-e',
              'group-f',
            ],
            size: 40,
            overlap: 12,
            maxVisible: 4,
            borderColor: const Color(0xFF0F172A),
          ),
          const SizedBox(height: 12),
          NaviiGroup(
            seeds: const ['team-1', 'team-2', 'team-3', 'team-4'],
            size: 32,
            overlap: 8,
            shape: NaviiShape.rounded,
            borderColor: const Color(0xFF0F172A),
          ),
          const SizedBox(height: 32),
        ],
      ),
    );
  }
}

class _LabelledAvatar extends StatelessWidget {
  final String label;
  final Widget child;
  const _LabelledAvatar({required this.label, required this.child});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        child,
        const SizedBox(height: 4),
        Text(
          label,
          style: const TextStyle(fontSize: 11, color: Color(0xFF64748B)),
        ),
      ],
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Shared widgets
// ─────────────────────────────────────────────────────────────────────────────

class _SearchBar extends StatelessWidget {
  final String hint;
  const _SearchBar({required this.hint});

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.fromLTRB(12, 10, 12, 4),
      padding: const EdgeInsets.symmetric(horizontal: 12),
      decoration: BoxDecoration(
        color: const Color(0xFF1E293B),
        borderRadius: BorderRadius.circular(10),
        border: Border.all(color: const Color(0xFF334155)),
      ),
      child: Row(
        children: [
          const Icon(Icons.search, color: Color(0xFF475569), size: 18),
          const SizedBox(width: 8),
          Expanded(
            child: TextField(
              style: const TextStyle(fontSize: 14, color: Color(0xFFCBD5E1)),
              decoration: InputDecoration(
                hintText: hint,
                hintStyle: const TextStyle(color: Color(0xFF475569)),
                border: InputBorder.none,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class _ErrorBanner extends StatelessWidget {
  final String message;
  const _ErrorBanner(this.message);

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.all(12),
      decoration: BoxDecoration(
        color: const Color(0xFF450A0A),
        borderRadius: BorderRadius.circular(8),
        border: Border.all(color: const Color(0xFF7F1D1D)),
      ),
      child: Text(
        message,
        style: const TextStyle(fontSize: 13, color: Color(0xFFFCA5A5)),
      ),
    );
  }
}

class _Divider extends StatelessWidget {
  final String label;
  const _Divider(this.label);

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        const Expanded(child: Divider(color: Color(0xFF334155))),
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 12),
          child: Text(
            label,
            style: const TextStyle(fontSize: 12, color: Color(0xFF64748B)),
          ),
        ),
        const Expanded(child: Divider(color: Color(0xFF334155))),
      ],
    );
  }
}

class _DeterminismNote extends StatelessWidget {
  const _DeterminismNote();

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: const Color(0xFF1E293B),
        borderRadius: BorderRadius.circular(12),
        border: Border.all(color: const Color(0xFF334155)),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: const [
          Row(
            children: [
              Icon(Icons.info_outline, size: 16, color: Color(0xFF818CF8)),
              SizedBox(width: 6),
              Text(
                'How it works',
                style: TextStyle(
                  fontWeight: FontWeight.bold,
                  color: Color(0xFFE2E8F0),
                  fontSize: 13,
                ),
              ),
            ],
          ),
          SizedBox(height: 8),
          Text(
            'Pass any string as the seed — your name, a UUID, or any other unique identifier. '
            'a random avatar is generated. Picks one of 22M+ '
            'combinations. Same seed = same avatar, '
            'works offline.',
            style: TextStyle(
              fontSize: 12,
              color: Color(0xFF94A3B8),
              height: 1.5,
            ),
          ),
        ],
      ),
    );
  }
}

InputDecoration _inputDecoration(String label, IconData icon) {
  return InputDecoration(
    labelText: label,
    labelStyle: const TextStyle(color: Color(0xFF64748B)),
    prefixIcon: Icon(icon, color: const Color(0xFF475569)),
    filled: true,
    fillColor: const Color(0xFF1E293B),
    enabledBorder: OutlineInputBorder(
      borderRadius: BorderRadius.circular(10),
      borderSide: const BorderSide(color: Color(0xFF334155)),
    ),
    focusedBorder: OutlineInputBorder(
      borderRadius: BorderRadius.circular(10),
      borderSide: const BorderSide(color: Color(0xFF6366F1), width: 2),
    ),
  );
}
0
likes
160
points
118
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Deterministic mascot-style avatar widget for Flutter. Drop-in avatar generation — same seed = same mascot, every time. No database, no uploads, no state required.

Homepage
Repository (GitHub)
View/report issues

Topics

#avatar #deterministic #mascot #offline #profile-picture

Funding

Consider supporting this project:

github.com

License

MIT (license)

Dependencies

flutter, flutter_svg, navii_dart

More

Packages that depend on navii_flutter