sliver_dashboard 2.3.1 copy "sliver_dashboard: ^2.3.1" to clipboard
sliver_dashboard: ^2.3.1 copied to clipboard

Smooth, sliver-based drag & resize grid engine. Supports nested grids, cross-sliver DND, responsive layouts. Native Flutter alternative to GridStack & react-grid-layout.

example/lib/main.dart

import 'dart:convert';
import 'dart:math';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:sliver_dashboard/sliver_dashboard.dart';

import 'multi_sliver_crossdrag_example.dart' show MultiSliverExamplePage;
import 'nested_example.dart' show NestedExamplePage;

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sliver Dashboard Playground',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.indigo,
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
      ),
      home: const ExampleHome(),
    );
  }
}

/// Launcher for the three dashboard demo examples.
class ExampleHome extends StatelessWidget {
  const ExampleHome({super.key});

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

    return Scaffold(
      appBar: AppBar(title: const Text('Sliver Dashboard — Examples')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          Card(
            color: theme.colorScheme.surfaceContainer,
            child: ListTile(
              leading: Icon(Icons.dashboard, color: theme.colorScheme.primary),
              title: const Text('Playground'),
              subtitle: const Text(
                'Single grid: drag, resize, custom columns (up to 60+), slot tap, maxRows, extra metadata…',
              ),
              trailing: const Icon(Icons.chevron_right),
              onTap: () => Navigator.of(context).push(
                MaterialPageRoute<void>(builder: (_) => const DashboardPage()),
              ),
            ),
          ),
          Card(
            color: theme.colorScheme.surfaceContainer,
            child: ListTile(
              leading: Icon(
                Icons.grid_view,
                color: theme.colorScheme.secondary,
              ),
              title: const Text('Nested grids'),
              subtitle: const Text(
                'Grids inside items, cross-grid drag & drop, sizeToContent, '
                'save/load of the whole tree.',
              ),
              trailing: const Icon(Icons.chevron_right),
              onTap: () => Navigator.of(context).push(
                MaterialPageRoute<void>(
                  builder: (_) => const NestedExamplePage(),
                ),
              ),
            ),
          ),
          Card(
            color: theme.colorScheme.surfaceContainer,
            child: ListTile(
              leading: Icon(Icons.layers, color: theme.colorScheme.tertiary),
              title: const Text('Multi-Sliver Drag & Drop'),
              subtitle: const Text(
                'Asymmetric sliver grids, physical coordinate matrix translation, '
                'custom proportional scaling projection policies.',
              ),
              trailing: const Icon(Icons.chevron_right),
              onTap: () => Navigator.of(context).push(
                MaterialPageRoute<void>(
                  builder: (_) => const MultiSliverExamplePage(),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

/// A strict policy to isolate layout regions and block specific collisions.
class CustomDashboardPolicy extends DashboardPolicy {
  const CustomDashboardPolicy({required this.blockSectionCollision});

  final bool blockSectionCollision;

  @override
  bool canCollide(LayoutItem itemA, LayoutItem itemB) {
    if (blockSectionCollision && itemB.isSectionBarrier) {
      return false;
    }
    return true;
  }

  @override
  bool canMoveTo(
    LayoutItem item,
    int targetX,
    int targetY,
    List<LayoutItem> currentLayout,
  ) {
    return true;
  }
}

/// Main playground viewport combining custom configurations and responsive grid views.
class DashboardPage extends StatefulWidget {
  const DashboardPage({super.key});

  @override
  State<DashboardPage> createState() => _DashboardPageState();
}

class _DashboardPageState extends State<DashboardPage> {
  var initialSlots = 8;

  late final DashboardController controller;

  final standardScrollController = ScrollController();
  final sliverScrollController = ScrollController();
  final jsonController = TextEditingController();

  final showGenerateButton = ValueNotifier<bool>(false);
  final isEditing = ValueNotifier(false);
  final showMinimap = ValueNotifier(true);
  final useSliverDemo = ValueNotifier(false);
  final useDragHandlesOnly = ValueNotifier(false);
  final blockSectionCollision = ValueNotifier(true);
  final animateReflow = ValueNotifier(false);
  final autoShrink = ValueNotifier(false);
  final maxRows = ValueNotifier<int?>(null);
  final enableSlotTapToAdd = ValueNotifier<bool>(false);

  // null = Use Responsive Breakpoints. Non-null = Override with custom fixed slot count.
  final customSlotCount = ValueNotifier<int?>(null);

  final compactionType = ValueNotifier<CompactType>(CompactType.vertical);
  final resizeBehavior = ValueNotifier<ResizeBehavior>(ResizeBehavior.push);
  final placementStrategy = ValueNotifier<AutoPlacementStrategy>(
    AutoPlacementStrategy.firstFit,
  );

  final random = Random();

  final _cardColors = <String, ({Color cardColor, Color textColor})>{};

  ({Color cardColor, Color textColor}) _generateColor(
    String id,
    ColorScheme colorScheme,
  ) {
    final int hash = id.hashCode;
    final double hue = (hash.abs() % 360).toDouble();
    final bool isDark = colorScheme.brightness == Brightness.dark;

    final bgColor = HSLColor.fromAHSL(
      1.0,
      hue,
      isDark ? 0.35 : 0.65,
      isDark ? 0.25 : 0.85,
    ).toColor();

    final textColor = bgColor.computeLuminance() > 0.5
        ? Colors.black87
        : Colors.white;

    return (cardColor: bgColor, textColor: textColor);
  }

  ({Color cardColor, Color textColor}) getColorsForItem(
    String id,
    ColorScheme colorScheme,
  ) {
    return _cardColors[id] ??= _generateColor(id, colorScheme);
  }

  @override
  void initState() {
    super.initState();
    controller = DashboardController(
      initialSlotCount: initialSlots,
      onLayoutChanged: (items, bkSlotCount) {
        _syncJsonField();
      },
      initialLayout: [
        // Section 1 Barrier
        const LayoutItem(
          id: 'sec_sys',
          x: 0,
          y: 0,
          w: 8,
          h: 1,
          isSectionBarrier: true,
          sectionTitle: '📌 System Diagnostics (Section 1)',
        ),
        const LayoutItem(
          id: 'sys_cpu',
          x: 0,
          y: 1,
          w: 2,
          h: 2,
          minW: 1,
          minH: 1,
          extra: {'category': 'CPU', 'tag': 'Hardware'},
        ),
        const LayoutItem(
          id: 'sys_mem',
          x: 2,
          y: 1,
          w: 2,
          h: 2,
          extra: {'category': 'RAM', 'tag': 'Hardware'},
        ),
        // Section 2 Barrier
        const LayoutItem(
          id: 'sec_usr',
          x: 0,
          y: 3,
          w: 8,
          h: 1,
          isSectionBarrier: true,
          sectionTitle: '📊 Custom Widgets & Analytics (Section 2)',
        ),
        const LayoutItem(
          id: 'chart_sales',
          x: 0,
          y: 4,
          w: 4,
          h: 2,
          isResizable: true,
          extra: {'category': 'Sales', 'tag': 'Analytics'},
        ),
        const LayoutItem(
          id: 'chart_geo',
          x: 4,
          y: 4,
          w: 2,
          h: 2,
          extra: {'category': 'Map', 'tag': 'Analytics'},
        ),
        const LayoutItem(
          id: 'table_logs',
          x: 6,
          y: 4,
          w: 2,
          h: 3,
          extra: {'category': 'Logs', 'tag': 'System'},
        ),
      ],
    );

    controller.setEditMode(isEditing.value);
    controller.setAllowAutoShrink(allow: autoShrink.value);
    _updatePolicy();
    _syncJsonField();

    controller.shortcuts = DashboardShortcuts(
      moveUp: {const SingleActivator(LogicalKeyboardKey.keyW)},
      moveLeft: {const SingleActivator(LogicalKeyboardKey.keyA)},
      moveDown: {const SingleActivator(LogicalKeyboardKey.keyS)},
      moveRight: {const SingleActivator(LogicalKeyboardKey.keyD)},
      grab: DashboardShortcuts.defaultShortcuts.grab,
      drop: DashboardShortcuts.defaultShortcuts.drop,
      cancel: DashboardShortcuts.defaultShortcuts.cancel,
    );

    blockSectionCollision.addListener(_updatePolicy);
  }

  void _updatePolicy() {
    controller.policy = CustomDashboardPolicy(
      blockSectionCollision: blockSectionCollision.value,
    );
  }

  void _syncJsonField() {
    final list = controller.exportLayout();
    if (list.length > 30) {
      jsonController.text =
          '// Auto-serialization paused for layouts with > 25 items.\n'
          '// Total items: ${list.length}\n'
          '//\n'
          '// Rendering 15,000+ lines of raw text in a TextField slows down perf.\n'
          '// Click "GENERATE JSON" below to export the schema manually at any time.';
      showGenerateButton.value = true;
      return;
    }
    jsonController.text = const JsonEncoder.withIndent('  ').convert(list);
    showGenerateButton.value = false;
  }

  void _forceGenerateJson() {
    final list = controller.exportLayout();
    jsonController.text = const JsonEncoder.withIndent('  ').convert(list);
    showMinimap.value = false;
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(
          'JSON Schema generated successfully for ${list.length} items!',
        ),
        backgroundColor: Theme.of(context).colorScheme.primary,
      ),
    );
  }

  void _importJson() {
    try {
      final decoded = jsonDecode(jsonController.text);
      if (decoded is List) {
        controller.importLayout(decoded);
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: const Text('Layout imported successfully!'),
            backgroundColor: Theme.of(context).colorScheme.primary,
          ),
        );
      }
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('Invalid JSON: $e'),
          backgroundColor: Theme.of(context).colorScheme.error,
        ),
      );
    }
  }

  void _addNewItem() {
    final activeCols = controller.slotCount.value;
    // Adapt default new item size for fine grids (e.g. 48 or 60 cols)
    final scale = max(1, activeCols ~/ 8);
    final w = (random.nextInt(2) + 1) * scale;
    final h = (random.nextInt(2) + 1) * scale;

    final newItem = LayoutItem(
      id: 'widget_${DateTime.now().millisecondsSinceEpoch % 10000}',
      x: -1,
      y: -1,
      w: w,
      h: h,
      extra: const {'category': 'Dynamic', 'created': 'FAB'},
    );
    controller.addItem(newItem, strategy: placementStrategy.value);
  }

  void _addStressTestItems(int count) {
    final list = <LayoutItem>[];
    final timestamp = DateTime.now().millisecondsSinceEpoch;
    final activeCols = controller.slotCount.value;
    final scale = max(1, activeCols ~/ 8);

    for (var i = 0; i < count; i++) {
      list.add(
        LayoutItem(
          id: 's_${timestamp}_$i',
          x: -1,
          y: -1,
          w: (random.nextInt(2) + 1) * scale,
          h: (random.nextInt(2) + 1) * scale,
          extra: const {'category': 'StressTest'},
        ),
      );
    }
    controller.addItems(list, strategy: placementStrategy.value);
  }

  void _handleSlotTap(int x, int y) {
    final activeCols = controller.slotCount.value;
    final scale = max(1, activeCols ~/ 8);
    final w = 2 * scale;
    final h = 2 * scale;
    final targetX = min(x, max(0, activeCols - w));

    controller.addItem(
      LayoutItem(
        id: 'tap_${targetX}_$y',
        x: targetX,
        y: y,
        w: w,
        h: h,
        extra: {'category': 'Created', 'createdVia': 'Slot Tap ($x,$y)'},
      ),
    );

    if (mounted) {
      ScaffoldMessenger.of(context).hideCurrentSnackBar();
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('Added new ${w}x$h item at slot ($targetX, $y)'),
          duration: const Duration(seconds: 2),
          backgroundColor: Theme.of(context).colorScheme.primary,
        ),
      );
    }
  }

  void _handleSlotLongPress(int x, int y) {
    if (mounted) {
      ScaffoldMessenger.of(context).hideCurrentSnackBar();
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('Long-pressed empty slot at Column $x, Row $y'),
          duration: const Duration(seconds: 2),
          backgroundColor: Theme.of(context).colorScheme.tertiary,
        ),
      );
    }
  }

  Future<bool> _confirmDeletion(List<LayoutItem> items) async {
    final confirm = await showDialog<bool>(
      context: context,
      builder: (ctx) => AlertDialog(
        title: const Text('Delete ?'),
        content: Text(
          items.length == 1
              ? 'Do you want to remove item ${items.first.id}?'
              : 'Do you want to remove items ${items.map((e) => e.id).join(', ')}?',
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(ctx, false),
            child: const Text('No'),
          ),
          TextButton(
            onPressed: () => Navigator.pop(ctx, true),
            child: const Text('Yes'),
          ),
        ],
      ),
    );
    return confirm ?? false;
  }

  Future<void> _confirmAndDelete(LayoutItem item) async {
    final confirm = await _confirmDeletion([item]);
    if (confirm) {
      controller.removeItem(item.id);
      if (mounted) {
        ScaffoldMessenger.of(
          context,
        ).showSnackBar(SnackBar(content: Text('Item ${item.id} deleted')));
      }
    }
  }

  @override
  void dispose() {
    blockSectionCollision.removeListener(_updatePolicy);
    showGenerateButton.dispose();
    isEditing.dispose();
    showMinimap.dispose();
    useSliverDemo.dispose();
    useDragHandlesOnly.dispose();
    blockSectionCollision.dispose();
    animateReflow.dispose();
    autoShrink.dispose();
    maxRows.dispose();
    enableSlotTapToAdd.dispose();
    customSlotCount.dispose();
    compactionType.dispose();
    resizeBehavior.dispose();
    placementStrategy.dispose();
    jsonController.dispose();
    controller.dispose();
    standardScrollController.dispose();
    sliverScrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final isDesktop = MediaQuery.of(context).size.width >= 950;

    final configPanel = _ConfigPanel(
      controller: controller,
      jsonController: jsonController,
      isEditing: isEditing,
      showMinimap: showMinimap,
      useSliverDemo: useSliverDemo,
      useDragHandlesOnly: useDragHandlesOnly,
      blockSectionCollision: blockSectionCollision,
      animateReflow: animateReflow,
      autoShrink: autoShrink,
      maxRows: maxRows,
      enableSlotTapToAdd: enableSlotTapToAdd,
      customSlotCount: customSlotCount,
      compactionType: compactionType,
      resizeBehavior: resizeBehavior,
      placementStrategy: placementStrategy,
      showGenerateButton: showGenerateButton,
      onForceGenerate: _forceGenerateJson,
      onImportJson: _importJson,
      onStressTest: _addStressTestItems,
    );

    return Scaffold(
      appBar: AppBar(
        title: const Text('Sliver Dashboard Playground'),
        elevation: 2,
        actions: [
          if (!isDesktop)
            Builder(
              builder: (context) {
                return IconButton(
                  icon: const Icon(Icons.tune),
                  tooltip: 'Open Config Panel',
                  onPressed: () => Scaffold.of(context).openEndDrawer(),
                );
              },
            ),
        ],
      ),
      endDrawer: isDesktop ? null : Drawer(child: SafeArea(child: configPanel)),
      body: Row(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Expanded(
            child: ValueListenableBuilder2<bool, int?>(
              useSliverDemo,
              customSlotCount,
              builder: (context, sliverMode, _, _) {
                return sliverMode
                    ? _buildSliverDemoView()
                    : _buildStandardDemoView();
              },
            ),
          ),
          if (isDesktop)
            Container(
              width: 320,
              decoration: BoxDecoration(
                border: Border(
                  left: BorderSide(
                    color: theme.colorScheme.outlineVariant,
                    width: 1,
                  ),
                ),
              ),
              child: Material(
                color: theme.colorScheme.surfaceContainerLow,
                child: configPanel,
              ),
            ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _addNewItem,
        tooltip: 'Add Auto-Placed Item (-1,-1)',
        child: const Icon(Icons.add),
      ),
    );
  }

  Widget _buildCard(BuildContext context, LayoutItem item) {
    final theme = Theme.of(context);
    final colors = getColorsForItem(item.id, theme.colorScheme);

    return _DashboardCard(
      key: ValueKey(item.id),
      item: item,
      isEditing: isEditing.value,
      useDragHandlesOnly: useDragHandlesOnly.value,
      cardColor: colors.cardColor,
      textColor: colors.textColor,
      theme: theme,
      onDelete: () => _confirmAndDelete(item),
    );
  }

  Widget _buildStandardDemoView() {
    final theme = Theme.of(context);
    return ValueListenableBuilder<int?>(
      valueListenable: customSlotCount,
      builder: (context, overrideSlots, _) {
        return ValueListenableBuilder<bool>(
          valueListenable: enableSlotTapToAdd,
          builder: (context, slotTapEnabled, _) {
            return ValueListenableBuilder3(
              useDragHandlesOnly,
              showMinimap,
              animateReflow,
              builder: (context, handlesOnly, minimap, reflow, _) {
                return Stack(
                  children: [
                    Dashboard<String>(
                      controller: controller,
                      scrollController: standardScrollController,
                      scrollDirection: controller.scrollDirection.value,
                      animateReflow: reflow,
                      slotAspectRatio: 1.0,
                      mainAxisSpacing: 8.0,
                      crossAxisSpacing: 8.0,
                      padding: const EdgeInsets.all(8.0),
                      onSlotTap: slotTapEnabled ? _handleSlotTap : null,
                      onSlotLongPress: _handleSlotLongPress,
                      dragStartGesture: handlesOnly
                          ? DragStartGesture.none
                          : DragStartGesture.longPress,
                      // Use responsive breakpoints when overrideSlots is null,
                      // otherwise null disables responsive breakpoints to respect manual slotCount
                      breakpoints: overrideSlots != null
                          ? null
                          : {0: 4, 600: 6, 900: 8},
                      itemBuilder: _buildCard,
                      onWillDelete: _confirmDeletion,
                      gridStyle: GridStyle(
                        fillColor: theme.colorScheme.onSurface.withValues(
                          alpha: 0.03,
                        ),
                        handleColor: theme.colorScheme.primary,
                        lineColor: theme.colorScheme.onSurface.withValues(
                          alpha: 0.08,
                        ),
                        lineWidth: 1,
                      ),
                      itemStyle: DashboardItemStyle(
                        focusColor: theme.colorScheme.primary,
                        activeColor: theme.colorScheme.secondary,
                        borderRadius: BorderRadius.circular(12),
                      ),
                      trashLayout: const TrashLayout(
                        visible: TrashPosition(
                          bottom: 20,
                          left: 100,
                          right: 100,
                        ),
                        hidden: TrashPosition(
                          bottom: -100,
                          left: 100,
                          right: 100,
                        ),
                      ),
                      trashBuilder: _buildTrashBin,
                    ),
                    if (minimap) _buildMinimapOverlay(),
                  ],
                );
              },
            );
          },
        );
      },
    );
  }

  Widget _buildSliverDemoView() {
    final theme = Theme.of(context);
    return ValueListenableBuilder<bool>(
      valueListenable: enableSlotTapToAdd,
      builder: (context, slotTapEnabled, _) {
        return ValueListenableBuilder3(
          useDragHandlesOnly,
          showMinimap,
          animateReflow,
          builder: (context, handlesOnly, minimap, reflow, _) {
            final overrideSlots = customSlotCount.value;

            return Stack(
              children: [
                DashboardOverlay<String>(
                  controller: controller,
                  scrollController: sliverScrollController,
                  dragStartGesture: handlesOnly
                      ? DragStartGesture.none
                      : DragStartGesture.longPress,
                  onSlotTap: slotTapEnabled ? _handleSlotTap : null,
                  onSlotLongPress: _handleSlotLongPress,
                  gridStyle: GridStyle(
                    fillColor: theme.colorScheme.onSurface.withValues(
                      alpha: 0.03,
                    ),
                    handleColor: theme.colorScheme.primary,
                    lineColor: theme.colorScheme.onSurface.withValues(
                      alpha: 0.08,
                    ),
                    lineWidth: 1,
                  ),
                  padding: const EdgeInsets.all(8.0),
                  fillViewport: true,
                  itemBuilder: _buildCard,
                  onWillDelete: _confirmDeletion,
                  trashLayout: const TrashLayout(
                    visible: TrashPosition(bottom: 20, left: 100, right: 100),
                    hidden: TrashPosition(bottom: -100, left: 100, right: 100),
                  ),
                  trashBuilder: _buildTrashBin,
                  child: CustomScrollView(
                    controller: sliverScrollController,
                    slivers: [
                      SliverAppBar(
                        automaticallyImplyLeading: false,
                        pinned: true,
                        expandedHeight: 120,
                        backgroundColor:
                            theme.colorScheme.surfaceContainerHighest,
                        flexibleSpace: FlexibleSpaceBar(
                          title: Text(
                            'Sliver direct composition',
                            style: TextStyle(
                              color: theme.colorScheme.onSurface,
                            ),
                          ),
                          centerTitle: false,
                        ),
                      ),
                      SliverPadding(
                        padding: const EdgeInsets.all(8.0),
                        sliver: SliverDashboard(
                          animateReflow: reflow,
                          breakpoints: overrideSlots != null
                              ? null
                              : {0: 4, 600: 6, 900: 8},
                          itemBuilder: _buildCard,
                        ),
                      ),
                      SliverList(
                        delegate: SliverChildBuilderDelegate(
                          (context, index) => ListTile(
                            leading: CircleAvatar(
                              backgroundColor:
                                  theme.colorScheme.primaryContainer,
                              child: Text(
                                '$index',
                                style: TextStyle(
                                  color: theme.colorScheme.onPrimaryContainer,
                                ),
                              ),
                            ),
                            title: Text('Subsequent List Item $index'),
                            subtitle: const Text(
                              'Rendered natively alongside the grid sliver',
                            ),
                          ),
                          childCount: 15,
                        ),
                      ),
                    ],
                  ),
                ),
                if (minimap) _buildMinimapOverlay(),
              ],
            );
          },
        );
      },
    );
  }

  Widget _buildTrashBin(
    BuildContext context,
    bool hovered,
    bool active,
    String? activeItemId,
  ) {
    final theme = Theme.of(context);
    final activeBg = active
        ? theme.colorScheme.error
        : (hovered
              ? theme.colorScheme.errorContainer
              : theme.colorScheme.onErrorContainer);
    final activeFg = active
        ? theme.colorScheme.onError
        : (hovered
              ? theme.colorScheme.onErrorContainer
              : theme.colorScheme.error);

    return Align(
      alignment: Alignment.bottomCenter,
      child: Container(
        height: 60,
        margin: const EdgeInsets.all(20.0),
        decoration: BoxDecoration(
          color: activeBg,
          borderRadius: BorderRadius.circular(30),
          boxShadow: const [BoxShadow(blurRadius: 10, color: Colors.black54)],
          border: hovered
              ? Border.all(color: theme.colorScheme.onError, width: 2)
              : null,
        ),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(active ? Icons.delete_forever : Icons.delete, color: activeFg),
            const SizedBox(width: 10),
            Text(
              active ? 'Release to Delete!' : 'Drop here to Delete',
              style: TextStyle(color: activeFg, fontWeight: FontWeight.bold),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildMinimapOverlay() {
    final theme = Theme.of(context);
    return ValueListenableBuilder<bool>(
      valueListenable: useSliverDemo,
      builder: (context, sliverMode, _) {
        final activeScrollController = sliverMode
            ? sliverScrollController
            : standardScrollController;

        return Positioned(
          left: 16,
          bottom: 16,
          child: Material(
            elevation: 6,
            borderRadius: BorderRadius.circular(8),
            clipBehavior: Clip.antiAlias,
            child: Container(
              width: 120,
              decoration: BoxDecoration(
                border: Border.all(color: theme.colorScheme.outlineVariant),
                borderRadius: BorderRadius.circular(8),
              ),
              child: DashboardMinimap(
                controller: controller,
                scrollController: activeScrollController,
                width: 120,
                slotAspectRatio: 1.0,
                mainAxisSpacing: 8.0,
                crossAxisSpacing: 8.0,
                padding: const EdgeInsets.all(8.0),
                style: MinimapStyle(
                  backgroundColor: theme.colorScheme.surfaceContainerHighest
                      .withValues(alpha: 0.8),
                  itemColor: theme.colorScheme.primary,
                  staticItemColor: theme.colorScheme.outline,
                  viewportColor: theme.colorScheme.primary.withValues(
                    alpha: 0.2,
                  ),
                ),
                onItemTap: (item) => controller.scrollToItem(item.id),
                markers: const [
                  MinimapMarker(
                    itemId: 'sec_sys',
                    color: Colors.red,
                    size: 10,
                    shape: MinimapMarkerShape.circle,
                    alignment: Alignment.centerRight,
                  ),
                  MinimapMarker(
                    itemId: 'sec_usr',
                    color: Colors.amber,
                    size: 12,
                    shape: MinimapMarkerShape.triangle,
                    alignment: Alignment.centerLeft,
                  ),
                ],
              ),
            ),
          ),
        );
      },
    );
  }
}

/// Utility builder combining two ValueListenables.
class ValueListenableBuilder2<A, B> extends StatelessWidget {
  const ValueListenableBuilder2(
    this.first,
    this.second, {
    required this.builder,
    super.key,
  });

  final ValueListenable<A> first;
  final ValueListenable<B> second;
  final Widget Function(BuildContext context, A a, B b, Widget? child) builder;

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<A>(
      valueListenable: first,
      builder: (context, a, _) {
        return ValueListenableBuilder<B>(
          valueListenable: second,
          builder: (context, b, _) {
            return builder(context, a, b, null);
          },
        );
      },
    );
  }
}

/// Utility builder combining three ValueListenables inside standard build context trees.
class ValueListenableBuilder3<A, B, C> extends StatelessWidget {
  const ValueListenableBuilder3(
    this.first,
    this.second,
    this.third, {
    required this.builder,
    super.key,
  });

  final ValueListenable<A> first;
  final ValueListenable<B> second;
  final ValueListenable<C> third;
  final Widget Function(BuildContext context, A a, B b, C c, Widget? child)
  builder;

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<A>(
      valueListenable: first,
      builder: (context, a, _) {
        return ValueListenableBuilder<B>(
          valueListenable: second,
          builder: (context, b, _) {
            return ValueListenableBuilder<C>(
              valueListenable: third,
              builder: (context, c, _) {
                return builder(context, a, b, c, null);
              },
            );
          },
        );
      },
    );
  }
}

class _ConfigPanel extends StatelessWidget {
  const _ConfigPanel({
    required this.controller,
    required this.jsonController,
    required this.isEditing,
    required this.showMinimap,
    required this.useSliverDemo,
    required this.useDragHandlesOnly,
    required this.blockSectionCollision,
    required this.animateReflow,
    required this.autoShrink,
    required this.maxRows,
    required this.enableSlotTapToAdd,
    required this.customSlotCount,
    required this.compactionType,
    required this.resizeBehavior,
    required this.placementStrategy,
    required this.showGenerateButton,
    required this.onForceGenerate,
    required this.onImportJson,
    required this.onStressTest,
  });

  final DashboardController controller;
  final TextEditingController jsonController;
  final ValueNotifier<bool> isEditing;
  final ValueNotifier<bool> showMinimap;
  final ValueNotifier<bool> useSliverDemo;
  final ValueNotifier<bool> useDragHandlesOnly;
  final ValueNotifier<bool> blockSectionCollision;
  final ValueNotifier<bool> animateReflow;
  final ValueNotifier<bool> autoShrink;
  final ValueNotifier<bool> enableSlotTapToAdd;
  final ValueNotifier<int?> maxRows;
  final ValueNotifier<int?> customSlotCount;
  final ValueNotifier<CompactType> compactionType;
  final ValueNotifier<ResizeBehavior> resizeBehavior;
  final ValueNotifier<AutoPlacementStrategy> placementStrategy;
  final ValueNotifier<bool> showGenerateButton;
  final VoidCallback onForceGenerate;
  final VoidCallback onImportJson;
  final void Function(int) onStressTest;

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

    return SingleChildScrollView(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            'CONFIGURATION PANEL',
            style: theme.textTheme.titleMedium?.copyWith(
              fontWeight: FontWeight.bold,
              color: theme.colorScheme.primary,
            ),
          ),
          const Divider(),
          const _SectionTitle('Visual Modes & Structures'),
          _SwitchTile(
            title: 'Edit Mode (Draggable/Resizable)',
            notifier: isEditing,
            onChanged: (val) => controller.setEditMode(val),
          ),
          _SwitchTile(
            title: 'Tap empty slot to add item (onSlotTap)',
            notifier: enableSlotTapToAdd,
          ),
          _SwitchTile(
            title: 'Use Custom Drag Handles only',
            notifier: useDragHandlesOnly,
          ),
          _SwitchTile(
            title: 'Native Sliver Direct Composition',
            notifier: useSliverDemo,
          ),
          _SwitchTile(
            title: 'Render Interactive Mini-Map',
            notifier: showMinimap,
          ),
          const SizedBox(height: 10),
          Text(
            'Grid Granularity / Columns (slotCount)',
            style: TextStyle(
              fontSize: 12,
              fontWeight: FontWeight.bold,
              color: theme.colorScheme.primary,
            ),
          ),
          const SizedBox(height: 4),
          ValueListenableBuilder<int?>(
            valueListenable: customSlotCount,
            builder: (context, value, _) {
              return DropdownButton<int?>(
                isExpanded: true,
                dropdownColor: theme.colorScheme.surfaceContainerHigh,
                value: value,
                items: const [
                  DropdownMenuItem(
                    value: null,
                    child: Text('RESPONSIVE BREAKPOINTS (AUTO)'),
                  ),
                  DropdownMenuItem(value: 4, child: Text('4 COLUMNS (COARSE)')),
                  DropdownMenuItem(
                    value: 8,
                    child: Text('8 COLUMNS (STANDARD)'),
                  ),
                  DropdownMenuItem(
                    value: 12,
                    child: Text('12 COLUMNS (GRID SYSTEM)'),
                  ),
                  DropdownMenuItem(
                    value: 16,
                    child: Text('16 COLUMNS (DENSE)'),
                  ),
                  DropdownMenuItem(value: 24, child: Text('24 COLUMNS (FINE)')),
                  DropdownMenuItem(
                    value: 32,
                    child: Text('32 COLUMNS (HIGH GRANULARITY)'),
                  ),
                  DropdownMenuItem(
                    value: 48,
                    child: Text('48 COLUMNS (ULTRA-FINE)'),
                  ),
                  DropdownMenuItem(
                    value: 60,
                    child: Text('60 COLUMNS (MICRO-GRID)'),
                  ),
                ],
                onChanged: (v) {
                  customSlotCount.value = v;
                  if (v != null) {
                    controller.setSlotCount(v);
                  }
                },
              );
            },
          ),
          const SizedBox(height: 16),
          const _SectionTitle('Collision & Layout Rules'),
          _SwitchTile(
            title: 'Block Section Header Collisions',
            notifier: blockSectionCollision,
          ),
          _SwitchTile(
            title: 'Auto-Shrink neighbors on Drag',
            notifier: autoShrink,
            onChanged: (val) => controller.setAllowAutoShrink(allow: val),
          ),
          _SwitchTile(
            title: 'Enable Reflow Animations',
            notifier: animateReflow,
          ),
          const SizedBox(height: 10),
          Text(
            'Max Rows Limit (Main Axis Cap)',
            style: TextStyle(
              fontSize: 12,
              color: theme.colorScheme.onSurfaceVariant,
            ),
          ),
          ValueListenableBuilder<int?>(
            valueListenable: maxRows,
            builder: (context, value, _) {
              return DropdownButton<int?>(
                isExpanded: true,
                dropdownColor: theme.colorScheme.surfaceContainerHigh,
                value: value,
                items: const [
                  DropdownMenuItem(
                    value: null,
                    child: Text('UNLIMITED (DEFAULT)'),
                  ),
                  DropdownMenuItem(value: 6, child: Text('6 ROWS MAX')),
                  DropdownMenuItem(value: 8, child: Text('8 ROWS MAX')),
                  DropdownMenuItem(value: 12, child: Text('12 ROWS MAX')),
                ],
                onChanged: (v) {
                  maxRows.value = v;
                  controller.setMaxRows(v);
                },
              );
            },
          ),
          const SizedBox(height: 10),
          Text(
            'Compaction Type',
            style: TextStyle(
              fontSize: 12,
              color: theme.colorScheme.onSurfaceVariant,
            ),
          ),
          ValueListenableBuilder(
            valueListenable: compactionType,
            builder: (context, value, _) {
              return DropdownButton<CompactType>(
                isExpanded: true,
                dropdownColor: theme.colorScheme.surfaceContainerHigh,
                value: value,
                items: CompactType.values
                    .map(
                      (v) => DropdownMenuItem(
                        value: v,
                        child: Text(v.name.toUpperCase()),
                      ),
                    )
                    .toList(),
                onChanged: (v) {
                  if (v != null) {
                    compactionType.value = v;
                    controller.setCompactionType(v);
                  }
                },
              );
            },
          ),
          const SizedBox(height: 10),
          Text(
            'Resize Behavior',
            style: TextStyle(
              fontSize: 12,
              color: theme.colorScheme.onSurfaceVariant,
            ),
          ),
          ValueListenableBuilder<ResizeBehavior>(
            valueListenable: resizeBehavior,
            builder: (context, value, _) {
              return DropdownButton<ResizeBehavior>(
                isExpanded: true,
                dropdownColor: theme.colorScheme.surfaceContainerHigh,
                value: value,
                items: ResizeBehavior.values
                    .map(
                      (v) => DropdownMenuItem(
                        value: v,
                        child: Text(v.name.toUpperCase()),
                      ),
                    )
                    .toList(),
                onChanged: (v) {
                  if (v != null) {
                    resizeBehavior.value = v;
                    controller.setResizeBehavior(v);
                  }
                },
              );
            },
          ),
          const SizedBox(height: 10),
          Text(
            'Auto-placement Strategy',
            style: TextStyle(
              fontSize: 12,
              color: theme.colorScheme.onSurfaceVariant,
            ),
          ),
          ValueListenableBuilder(
            valueListenable: placementStrategy,
            builder: (context, value, _) {
              return DropdownButton<AutoPlacementStrategy>(
                isExpanded: true,
                dropdownColor: theme.colorScheme.surfaceContainerHigh,
                value: value,
                items: AutoPlacementStrategy.values
                    .map(
                      (v) => DropdownMenuItem(
                        value: v,
                        child: Text(v.name.toUpperCase()),
                      ),
                    )
                    .toList(),
                onChanged: (v) {
                  if (v != null) {
                    placementStrategy.value = v;
                  }
                },
              );
            },
          ),
          const SizedBox(height: 16),
          const _SectionTitle('Stress Tests & Bulk actions'),
          Wrap(
            spacing: 8,
            runSpacing: 8,
            children: [
              ElevatedButton(
                onPressed: () => onStressTest(20),
                child: const Text('+20 Items'),
              ),
              ElevatedButton(
                onPressed: () => onStressTest(100),
                child: const Text('+100 Items'),
              ),
              OutlinedButton(
                onPressed: () => controller.layout.value = [],
                child: const Text('Clear Grid'),
              ),
            ],
          ),
          const SizedBox(height: 16),
          const _SectionTitle('JSON Schema Import/Export'),
          ValueListenableBuilder<bool>(
            valueListenable: showGenerateButton,
            builder: (context, show, _) {
              if (!show) return const SizedBox.shrink();
              return Padding(
                padding: const EdgeInsets.only(bottom: 8.0),
                child: SizedBox(
                  width: double.infinity,
                  child: FilledButton.icon(
                    onPressed: onForceGenerate,
                    icon: const Icon(Icons.bolt),
                    label: const Text('Generate JSON Schema'),
                    style: FilledButton.styleFrom(
                      backgroundColor: theme.colorScheme.primary,
                      foregroundColor: theme.colorScheme.onPrimary,
                    ),
                  ),
                ),
              );
            },
          ),
          const SizedBox(height: 8),
          TextField(
            controller: jsonController,
            maxLines: 6,
            style: const TextStyle(fontFamily: 'monospace', fontSize: 11),
            decoration: const InputDecoration(
              border: OutlineInputBorder(),
              labelText: 'JSON Layout',
            ),
          ),
          const SizedBox(height: 8),
          SizedBox(
            width: double.infinity,
            child: ElevatedButton.icon(
              onPressed: onImportJson,
              icon: const Icon(Icons.download),
              label: const Text('Import Layout from JSON'),
            ),
          ),
        ],
      ),
    );
  }
}

class _SectionTitle extends StatelessWidget {
  const _SectionTitle(this.title);
  final String title;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8.0),
      child: Text(
        title,
        style: TextStyle(
          fontWeight: FontWeight.bold,
          fontSize: 13,
          color: theme.colorScheme.onSurfaceVariant,
        ),
      ),
    );
  }
}

class _SwitchTile extends StatelessWidget {
  const _SwitchTile({
    required this.title,
    required this.notifier,
    this.onChanged,
  });

  final String title;
  final ValueNotifier<bool> notifier;
  final ValueChanged<bool>? onChanged;

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<bool>(
      valueListenable: notifier,
      builder: (context, value, _) {
        return SwitchListTile(
          contentPadding: EdgeInsets.zero,
          title: Text(title, style: const TextStyle(fontSize: 12)),
          value: value,
          onChanged: (val) {
            notifier.value = val;
            onChanged?.call(val);
          },
        );
      },
    );
  }
}

class _DashboardCard extends StatelessWidget {
  const _DashboardCard({
    required this.item,
    required this.isEditing,
    required this.useDragHandlesOnly,
    required this.cardColor,
    required this.textColor,
    required this.theme,
    required this.onDelete,
    super.key,
  });

  final LayoutItem item;
  final bool isEditing;
  final bool useDragHandlesOnly;
  final Color cardColor;
  final Color textColor;
  final ThemeData theme;
  final VoidCallback onDelete;

  @override
  Widget build(BuildContext context) {
    final category = item.extra?['category'] as String?;

    return Card(
      elevation: 3,
      color: cardColor,
      shape: const RoundedRectangleBorder(
        borderRadius: BorderRadius.all(Radius.circular(12)),
      ),
      clipBehavior: Clip.none,
      child: Stack(
        children: [
          Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(
                  item.id.startsWith('s_')
                      ? 'Item ${item.id.substring(2)}'
                      : item.id,
                  style: theme.textTheme.titleMedium?.copyWith(
                    fontWeight: FontWeight.bold,
                    color: textColor,
                  ),
                  overflow: TextOverflow.ellipsis,
                ),
                Text(
                  '(${item.w}x${item.h})',
                  style: TextStyle(
                    fontSize: 12,
                    color: textColor.withValues(alpha: 0.7),
                  ),
                ),
                if (category != null)
                  Container(
                    margin: const EdgeInsets.only(top: 4),
                    padding: const EdgeInsets.symmetric(
                      horizontal: 6,
                      vertical: 2,
                    ),
                    decoration: BoxDecoration(
                      color: textColor.withValues(alpha: 0.15),
                      borderRadius: BorderRadius.circular(4),
                    ),
                    child: Text(
                      category.toUpperCase(),
                      style: TextStyle(
                        fontSize: 9,
                        fontWeight: FontWeight.bold,
                        color: textColor,
                      ),
                    ),
                  ),
              ],
            ),
          ),
          if (isEditing && useDragHandlesOnly)
            Positioned(
              left: 4,
              top: 4,
              child: DashboardDragStartListener(
                itemId: item.id,
                child: Container(
                  padding: const EdgeInsets.all(4),
                  decoration: BoxDecoration(
                    color: Colors.black26,
                    borderRadius: BorderRadius.circular(6),
                  ),
                  child: const Icon(
                    Icons.drag_handle,
                    size: 16,
                    color: Colors.white,
                  ),
                ),
              ),
            ),
          if (isEditing)
            Positioned(
              top: 4,
              right: 4,
              child: GestureDetector(
                onTap: onDelete,
                child: Container(
                  padding: const EdgeInsets.all(4),
                  decoration: const BoxDecoration(
                    color: Colors.black38,
                    shape: BoxShape.circle,
                  ),
                  child: Icon(
                    Icons.close,
                    size: 14,
                    color: theme.colorScheme.error,
                  ),
                ),
              ),
            ),
        ],
      ),
    );
  }
}
18
likes
160
points
3.96k
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Smooth, sliver-based drag & resize grid engine. Supports nested grids, cross-sliver DND, responsive layouts. Native Flutter alternative to GridStack & react-grid-layout.

Repository (GitHub)
View/report issues

Topics

#dashboard #grid #layout #draggable #resizable

License

Apache-2.0 (license)

Dependencies

flutter, meta, state_beacon

More

Packages that depend on sliver_dashboard