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

Plug-and-play multi-board drag-and-drop for Flutter. Mechanism-first physics with optional default UI, 1..N boards, cross-board moves, WIP, and undo — flutter-only, state-management agnostic.

example/lib/main.dart

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

// Bright, airy palette — cool sky into soft mint (not purple / cream kitsch).
const _sky = Color(0xFFE7F3FB);
const _mist = Color(0xFFF3FBF0);
const _foam = Color(0xFFFFFFFF);
const _ink = Color(0xFF143042);
const _muted = Color(0xFF5B7384);
const _accent = Color(0xFF0F8A7A);
const _accentSoft = Color(0xFFD7F3EE);
const _line = Color(0xFFD5E4EE);
const _high = Color(0xFFE4572E);
const _mid = Color(0xFFD4A017);
const _low = Color(0xFF2A9D8F);

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  SystemChrome.setSystemUIOverlayStyle(
    const SystemUiOverlayStyle(
      statusBarColor: Colors.transparent,
      statusBarIconBrightness: Brightness.dark,
    ),
  );
  runApp(const FlexiBoardExampleApp());
}

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

  @override
  Widget build(BuildContext context) {
    final base = ThemeData(
      useMaterial3: true,
      brightness: Brightness.light,
      fontFamily: 'PlusJakartaSans',
    );
    final textTheme = base.textTheme.apply(
      bodyColor: _ink,
      displayColor: _ink,
      fontFamily: 'PlusJakartaSans',
    );

    return MaterialApp(
      title: 'FlexiBoard',
      debugShowCheckedModeBanner: false,
      theme: base.copyWith(
        scaffoldBackgroundColor: _sky,
        colorScheme: const ColorScheme.light(
          primary: _accent,
          onPrimary: _foam,
          secondary: _accentSoft,
          onSecondary: _ink,
          surface: _foam,
          onSurface: _ink,
          outline: _line,
        ),
        textTheme: textTheme,
        appBarTheme: const AppBarTheme(
          backgroundColor: Colors.transparent,
          elevation: 0,
          foregroundColor: _ink,
          titleTextStyle: TextStyle(
            fontFamily: 'Fraunces',
            fontSize: 22,
            fontWeight: FontWeight.w600,
            color: _ink,
            height: 1.1,
          ),
        ),
      ),
      home: const ExampleHome(),
    );
  }
}

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

  @override
  State<ExampleHome> createState() => _ExampleHomeState();
}

class _ExampleHomeState extends State<ExampleHome>
    with SingleTickerProviderStateMixin {
  late final FlexiBoardController<Task> _controller;
  late final AnimationController _intro;
  FlexiBoardLayout _layout = FlexiBoardLayout.tabs;
  bool _useCustomCards = true;

  @override
  void initState() {
    super.initState();
    _controller = FlexiBoardController<Task>(initial: _buildWorkspace());
    _intro = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 700),
    )..forward();
  }

  @override
  void dispose() {
    _intro.dispose();
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final fade = CurvedAnimation(parent: _intro, curve: Curves.easeOutCubic);
    final slide = Tween<Offset>(
      begin: const Offset(0, 0.04),
      end: Offset.zero,
    ).animate(fade);

    return Scaffold(
      body: DecoratedBox(
        decoration: const BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topLeft,
            end: Alignment.bottomRight,
            colors: [_sky, Color(0xFFEEF8F4), _mist],
          ),
        ),
        child: SafeArea(
          child: FadeTransition(
            opacity: fade,
            child: SlideTransition(
              position: slide,
              child: ListenableBuilder(
                listenable: _controller,
                builder: (context, _) {
                  return Column(
                    crossAxisAlignment: CrossAxisAlignment.stretch,
                    children: [
                      _TopBar(
                        canUndo: _controller.canUndo,
                        canRedo: _controller.canRedo,
                        onUndo: _controller.undo,
                        onRedo: _controller.redo,
                        customCards: _useCustomCards,
                        onToggleCards: () {
                          setState(() => _useCustomCards = !_useCustomCards);
                        },
                      ),
                      Padding(
                        padding: const EdgeInsets.fromLTRB(20, 4, 20, 12),
                        child: Row(
                          children: [
                            Expanded(
                              child: Text(
                                'Drag cards across boards. Long-press to lift.',
                                style: Theme.of(context)
                                    .textTheme
                                    .bodyMedium
                                    ?.copyWith(
                                      color: _muted,
                                      height: 1.35,
                                    ),
                              ),
                            ),
                            _LayoutSwitcher(
                              layout: _layout,
                              onChanged: (layout) =>
                                  setState(() => _layout = layout),
                            ),
                          ],
                        ),
                      ),
                      Expanded(
                        child: Padding(
                          padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
                          child: DecoratedBox(
                            decoration: BoxDecoration(
                              color: _foam.withValues(alpha: 0.72),
                              borderRadius: BorderRadius.circular(24),
                              border: Border.all(
                                color: _line.withValues(alpha: 0.8),
                              ),
                              boxShadow: [
                                BoxShadow(
                                  color: const Color(0xFF143042)
                                      .withValues(alpha: 0.06),
                                  blurRadius: 28,
                                  offset: const Offset(0, 12),
                                ),
                              ],
                            ),
                            child: ClipRRect(
                              borderRadius: BorderRadius.circular(24),
                              child: FlexiBoard<Task>(
                                controller: _controller,
                                layout: _layout,
                                physics: FlexiBoardPhysics.snappy,
                                theme: _boardTheme,
                                cardBuilder:
                                    _useCustomCards ? _buildCustomCard : null,
                                columnHeaderBuilder: _buildColumnHeader,
                                columnFooterBuilder: _buildColumnFooter,
                                boardTabBuilder: _buildTab,
                                emptyColumnBuilder: _buildEmptyColumn,
                                onDrag: (details) {
                                  debugPrint(
                                    'drag ${details.phase.name} '
                                    '${details.card.data.title}',
                                  );
                                },
                                onDrop: (details) {
                                  debugPrint(
                                    'drop accepted=${details.accepted} '
                                    '${details.card.data.title}',
                                  );
                                },
                              ),
                            ),
                          ),
                        ),
                      ),
                    ],
                  );
                },
              ),
            ),
          ),
        ),
      ),
    );
  }

  FlexiBoardTheme get _boardTheme => const FlexiBoardTheme(
        boardBackgroundColor: Color(0x00FFFFFF),
        columnBackgroundColor: Color(0xFFF7FBFD),
        columnHeaderColor: Color(0xFFF0F7FA),
        cardBackgroundColor: _foam,
        cardBorderColor: _line,
        placeholderColor: Color(0x330F8A7A),
        rejectedColor: Color(0x33E4572E),
        wipWarningColor: _mid,
        wipExceededColor: _high,
        tabSelectedColor: _accentSoft,
        tabUnselectedColor: Color(0xFFEAF2F7),
        columnBorderRadius: 16,
        cardBorderRadius: 14,
        cardElevation: 0,
      );

  Widget _buildTab(
    BuildContext context,
    FlexiBoardBoard<Task> board,
    bool selected,
    bool dragHover,
  ) {
    return AnimatedContainer(
      duration: const Duration(milliseconds: 200),
      margin: const EdgeInsets.only(right: 8),
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
      decoration: BoxDecoration(
        color: dragHover
            ? _accent.withValues(alpha: 0.16)
            : selected
                ? _accentSoft
                : const Color(0xFFEAF2F7),
        borderRadius: BorderRadius.circular(12),
        border: Border.all(
          color: selected || dragHover ? _accent.withValues(alpha: 0.35) : _line,
        ),
      ),
      child: Text(
        board.title,
        style: TextStyle(
                  fontFamily: 'PlusJakartaSans', 
          fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
          color: _ink,
          fontSize: 13.5,
        ),
      ),
    );
  }

  Widget _buildColumnHeader(BuildContext context, FlexiBoardColumn<Task> column) {
    final limit = column.wipLimit;
    return Padding(
      padding: const EdgeInsets.fromLTRB(14, 14, 14, 8),
      child: Row(
        children: [
          Expanded(
            child: Text(
              column.title,
              style: TextStyle(
                  fontFamily: 'PlusJakartaSans', 
                fontWeight: FontWeight.w700,
                fontSize: 14,
                color: _ink,
              ),
            ),
          ),
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
            decoration: BoxDecoration(
              color: column.isAtWipLimit
                  ? _high.withValues(alpha: 0.12)
                  : const Color(0xFFEAF2F7),
              borderRadius: BorderRadius.circular(8),
            ),
            child: Text(
              limit == null
                  ? '${column.cardCount}'
                  : '${column.cardCount}/$limit',
              style: TextStyle(
                  fontFamily: 'PlusJakartaSans', 
                fontSize: 11.5,
                fontWeight: FontWeight.w600,
                color: column.isAtWipLimit ? _high : _muted,
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildColumnFooter(BuildContext context, FlexiBoardColumn<Task> column) {
    return Padding(
      padding: const EdgeInsets.fromLTRB(10, 0, 10, 10),
      child: TextButton.icon(
        style: TextButton.styleFrom(
          foregroundColor: _accent,
          alignment: Alignment.centerLeft,
        ),
        onPressed: () {
          _controller.addCard(
            boardId: _controller.activeBoardId,
            columnId: column.id,
            card: FlexiBoardCard(
              id: 'new_${DateTime.now().millisecondsSinceEpoch}',
              data: Task(
                title: 'New task',
                priority: Priority.medium,
                tag: 'Fresh',
              ),
            ),
          );
          setState(() {});
        },
        icon: const Icon(Icons.add_rounded, size: 18),
        label: const Text('Add card'),
      ),
    );
  }

  Widget _buildEmptyColumn(BuildContext context, FlexiBoardColumn<Task> column) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 28),
      child: Text(
        'Drop here',
        textAlign: TextAlign.center,
        style: TextStyle(
                  fontFamily: 'PlusJakartaSans', 
          color: _muted.withValues(alpha: 0.8),
          fontSize: 13,
        ),
      ),
    );
  }

  Widget _buildCustomCard(
    BuildContext context,
    FlexiBoardCard<Task> card,
    FlexiBoardCardDragDetails details,
  ) {
    final priority = card.data.priority;
    final accent = switch (priority) {
      Priority.high => _high,
      Priority.medium => _mid,
      Priority.low => _low,
    };

    return AnimatedContainer(
      duration: const Duration(milliseconds: 180),
      width: double.infinity,
      padding: const EdgeInsets.fromLTRB(12, 12, 12, 12),
      decoration: BoxDecoration(
        color: _foam,
        borderRadius: BorderRadius.circular(14),
        border: Border.all(
          color: details.isDragging
              ? _accent.withValues(alpha: 0.45)
              : _line,
        ),
        boxShadow: [
          BoxShadow(
            color: _ink.withValues(alpha: details.isDragging ? 0.12 : 0.04),
            blurRadius: details.isDragging ? 18 : 10,
            offset: Offset(0, details.isDragging ? 8 : 3),
          ),
        ],
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              Container(
                width: 8,
                height: 8,
                decoration: BoxDecoration(
                  color: accent,
                  shape: BoxShape.circle,
                ),
              ),
              const SizedBox(width: 8),
              Text(
                priority.label,
                style: TextStyle(
                  fontFamily: 'PlusJakartaSans', 
                  fontSize: 11,
                  fontWeight: FontWeight.w600,
                  color: accent,
                  letterSpacing: 0.2,
                ),
              ),
            ],
          ),
          const SizedBox(height: 8),
          Text(
            card.data.title,
            style: TextStyle(
                  fontFamily: 'PlusJakartaSans', 
              fontSize: 14,
              fontWeight: FontWeight.w600,
              color: _ink,
              height: 1.3,
            ),
          ),
          if (card.data.tag != null) ...[
            const SizedBox(height: 10),
            Text(
              card.data.tag!,
              style: TextStyle(
                  fontFamily: 'PlusJakartaSans', 
                fontSize: 11.5,
                color: _muted,
              ),
            ),
          ],
        ],
      ),
    );
  }
}

class _TopBar extends StatelessWidget {
  const _TopBar({
    required this.canUndo,
    required this.canRedo,
    required this.onUndo,
    required this.onRedo,
    required this.customCards,
    required this.onToggleCards,
  });

  final bool canUndo;
  final bool canRedo;
  final VoidCallback onUndo;
  final VoidCallback onRedo;
  final bool customCards;
  final VoidCallback onToggleCards;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.fromLTRB(20, 12, 12, 8),
      child: Row(
        children: [
          Container(
            width: 36,
            height: 36,
            decoration: BoxDecoration(
              gradient: const LinearGradient(
                colors: [Color(0xFF19A394), Color(0xFF3DB8D0)],
              ),
              borderRadius: BorderRadius.circular(11),
            ),
            child: const Icon(Icons.view_kanban_rounded, color: _foam, size: 20),
          ),
          const SizedBox(width: 12),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  'FlexiBoard',
                  style: TextStyle(
                    fontFamily: 'Fraunces', 
                    fontSize: 24,
                    fontWeight: FontWeight.w600,
                    color: _ink,
                    height: 1.05,
                  ),
                ),
                Text(
                  'Multi-board DnD demo',
                  style: TextStyle(
                  fontFamily: 'PlusJakartaSans', 
                    fontSize: 12.5,
                    color: _muted,
                  ),
                ),
              ],
            ),
          ),
          _IconAction(
            tooltip: 'Undo',
            icon: Icons.undo_rounded,
            onPressed: canUndo ? onUndo : null,
          ),
          _IconAction(
            tooltip: 'Redo',
            icon: Icons.redo_rounded,
            onPressed: canRedo ? onRedo : null,
          ),
          _IconAction(
            tooltip: customCards ? 'Default cards' : 'Custom cards',
            icon: customCards
                ? Icons.auto_awesome_rounded
                : Icons.crop_square_rounded,
            onPressed: onToggleCards,
            active: customCards,
          ),
        ],
      ),
    );
  }
}

class _IconAction extends StatelessWidget {
  const _IconAction({
    required this.tooltip,
    required this.icon,
    required this.onPressed,
    this.active = false,
  });

  final String tooltip;
  final IconData icon;
  final VoidCallback? onPressed;
  final bool active;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(left: 4),
      child: Tooltip(
        message: tooltip,
        child: Material(
          color: active ? _accentSoft : Colors.transparent,
          borderRadius: BorderRadius.circular(12),
          child: InkWell(
            onTap: onPressed,
            borderRadius: BorderRadius.circular(12),
            child: SizedBox(
              width: 40,
              height: 40,
              child: Icon(
                icon,
                size: 20,
                color: onPressed == null
                    ? _muted.withValues(alpha: 0.35)
                    : active
                        ? _accent
                        : _ink,
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class _LayoutSwitcher extends StatelessWidget {
  const _LayoutSwitcher({
    required this.layout,
    required this.onChanged,
  });

  final FlexiBoardLayout layout;
  final ValueChanged<FlexiBoardLayout> onChanged;

  @override
  Widget build(BuildContext context) {
    final items = <(FlexiBoardLayout, String, IconData)>[
      (FlexiBoardLayout.single, 'One', Icons.crop_portrait_rounded),
      (FlexiBoardLayout.tabs, 'Tabs', Icons.tab_rounded),
      (FlexiBoardLayout.sideBySide, 'Split', Icons.view_column_rounded),
      (FlexiBoardLayout.paged, 'Paged', Icons.view_carousel_rounded),
    ];

    return Container(
      padding: const EdgeInsets.all(3),
      decoration: BoxDecoration(
        color: _foam.withValues(alpha: 0.9),
        borderRadius: BorderRadius.circular(12),
        border: Border.all(color: _line),
      ),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          for (final item in items)
            _LayoutChip(
              label: item.$2,
              icon: item.$3,
              selected: layout == item.$1,
              onTap: () => onChanged(item.$1),
            ),
        ],
      ),
    );
  }
}

class _LayoutChip extends StatelessWidget {
  const _LayoutChip({
    required this.label,
    required this.icon,
    required this.selected,
    required this.onTap,
  });

  final String label;
  final IconData icon;
  final bool selected;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    return Material(
      color: selected ? _accentSoft : Colors.transparent,
      borderRadius: BorderRadius.circular(9),
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(9),
        child: AnimatedContainer(
          duration: const Duration(milliseconds: 180),
          padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
          child: Row(
            children: [
              Icon(icon, size: 15, color: selected ? _accent : _muted),
              const SizedBox(width: 5),
              Text(
                label,
                style: TextStyle(
                  fontFamily: 'PlusJakartaSans', 
                  fontSize: 12,
                  fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
                  color: selected ? _ink : _muted,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

enum Priority { low, medium, high }

extension on Priority {
  String get label => switch (this) {
        Priority.high => 'High',
        Priority.medium => 'Medium',
        Priority.low => 'Low',
      };
}

class Task {
  Task({
    required this.title,
    required this.priority,
    this.tag,
  });

  final String title;
  final Priority priority;
  final String? tag;

  @override
  String toString() => title;
}

FlexiBoardWorkspace<Task> _buildWorkspace() {
  return FlexiBoardWorkspace<Task>(
    activeBoardId: 'product',
    boards: [
      FlexiBoardBoard<Task>(
        id: 'product',
        title: 'Product',
        columns: [
          FlexiBoardColumn<Task>(
            id: 'backlog',
            title: 'Backlog',
            cards: [
              FlexiBoardCard(
                id: 'p1',
                data: Task(
                  title: 'Research competitors',
                  priority: Priority.low,
                  tag: 'Discovery',
                ),
              ),
              FlexiBoardCard(
                id: 'p2',
                data: Task(
                  title: 'Draft product brief',
                  priority: Priority.medium,
                  tag: 'Writing',
                ),
              ),
            ],
          ),
          FlexiBoardColumn<Task>(
            id: 'doing',
            title: 'In Progress',
            wipLimit: 2,
            cards: [
              FlexiBoardCard(
                id: 'p3',
                data: Task(
                  title: 'Prototype drag physics',
                  priority: Priority.high,
                  tag: 'Engineering',
                ),
              ),
            ],
          ),
          const FlexiBoardColumn<Task>(
            id: 'done',
            title: 'Done',
          ),
        ],
      ),
      FlexiBoardBoard<Task>(
        id: 'marketing',
        title: 'Marketing',
        columns: [
          FlexiBoardColumn<Task>(
            id: 'ideas',
            title: 'Ideas',
            cards: [
              FlexiBoardCard(
                id: 'm1',
                data: Task(
                  title: 'Launch announcement',
                  priority: Priority.medium,
                  tag: 'Social',
                ),
              ),
            ],
          ),
          const FlexiBoardColumn<Task>(
            id: 'scheduled',
            title: 'Scheduled',
            wipLimit: 3,
          ),
        ],
      ),
    ],
  );
}
0
likes
0
points
183
downloads

Publisher

verified publisheranantatama.com

Weekly Downloads

Plug-and-play multi-board drag-and-drop for Flutter. Mechanism-first physics with optional default UI, 1..N boards, cross-board moves, WIP, and undo — flutter-only, state-management agnostic.

Repository (GitHub)
View/report issues

Topics

#flutter #drag-and-drop #kanban #board #ui

License

unknown (license)

Dependencies

flutter

More

Packages that depend on flexi_board