xtal_dashboard 0.1.0
xtal_dashboard: ^0.1.0 copied to clipboard
A grid-based dashboard layout library with drag-and-drop reordering, resizing, and physics-based animations.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:xtal_dashboard/xtal_dashboard.dart';
void main() {
runApp(const DemoApp());
}
class DemoApp extends StatelessWidget {
const DemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'xtal_dashboard Demo',
theme: ThemeData.light(useMaterial3: true),
debugShowCheckedModeBanner: false,
home: const DemoPage(),
);
}
}
class DemoPage extends StatefulWidget {
const DemoPage({super.key});
@override
State<DemoPage> createState() => _DemoPageState();
}
class _DemoPageState extends State<DemoPage> {
bool _collisionEnabled = true;
bool _shelfEnabled = true;
late List<DashboardItemData> _items;
List<DashboardItemData> _shelfItems = [];
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
_items = _createInitialItems();
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
// ---- Initial grid layout (4 columns, 8 items, mixed spans) ----
//
// Col: 0 1 2 3
// Row 0: [ A (2x2) ] [ B (2x1) ]
// Row 1: [ ] [ C ] [ D ]
// Row 2: [ E ] [ F (2x1) ] [ G ]
// Row 3: [ H (3x1) ] [ ]
static const _itemConfigs = <(String, int, int, int, int, int)>[
// (id, column, row, columnSpan, rowSpan, colorValue)
('A', 0, 0, 2, 2, 0xFF42A5F5), // Blue 400
('B', 2, 0, 2, 1, 0xFF26A69A), // Teal 400
('C', 2, 1, 1, 1, 0xFFAB47BC), // Purple 300
('D', 3, 1, 1, 1, 0xFFEC407A), // Pink 300
('E', 0, 2, 1, 1, 0xFFFFCA28), // Amber 400
('F', 1, 2, 2, 1, 0xFFFFA726), // Orange 400
('G', 3, 2, 1, 1, 0xFF66BB6A), // Green 400
('H', 0, 3, 3, 1, 0xFF5C6BC0), // Indigo 300
];
List<DashboardItemData> _createInitialItems() {
return _itemConfigs.map((config) {
final (id, col, row, colSpan, rowSpan, colorValue) = config;
final color = Color(colorValue);
return DashboardItemData(
gridItem: GridItem(
id: id,
column: col,
row: row,
columnSpan: colSpan,
rowSpan: rowSpan,
),
builder: (context, isBeingDragged) => _buildCard(color, id),
);
}).toList();
}
// ---- Card widget with visual patterns for FittedBox verification ----
static Widget _buildCard(Color color, String label) {
final darker = Color.lerp(color, const Color(0xFF000000), 0.3)!;
return DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [color, darker],
),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0x30FFFFFF), width: 1.5),
),
child: Stack(
children: [
// Corner dots — visible when layout is correct.
for (final alignment in [
Alignment.topLeft,
Alignment.topRight,
Alignment.bottomLeft,
Alignment.bottomRight,
])
Align(
alignment: alignment,
child: Padding(
padding: const EdgeInsets.all(6),
child: DecoratedBox(
decoration: BoxDecoration(
color: const Color(0x40FFFFFF),
borderRadius: BorderRadius.circular(3),
),
child: const SizedBox(width: 6, height: 6),
),
),
),
// Thin horizontal rule across the middle.
const Center(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: DecoratedBox(
decoration: BoxDecoration(color: Color(0x20FFFFFF)),
child: SizedBox(height: 1, width: double.infinity),
),
),
),
// Label + subtitle.
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
label,
style: const TextStyle(
color: Color(0xFFFFFFFF),
fontSize: 28,
fontWeight: FontWeight.bold,
decoration: TextDecoration.none,
),
),
const SizedBox(height: 2),
Text(
'Widget $label',
style: const TextStyle(
color: Color(0x99FFFFFF),
fontSize: 10,
fontWeight: FontWeight.w500,
decoration: TextDecoration.none,
),
),
],
),
),
],
),
);
}
// ---- Custom ghost: blue border + glow ----
static Widget _buildGhost(BuildContext context, Widget child, Size size) {
return DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF2196F3), width: 3),
boxShadow: const [
BoxShadow(color: Color(0x402196F3), blurRadius: 16, spreadRadius: 4),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Opacity(opacity: 0.85, child: child),
),
);
}
// ---- Build ----
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('xtal_dashboard (shelf: ${_shelfItems.length})'),
actions: [
Padding(
padding: const EdgeInsets.only(right: 8),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Shelf'),
Switch.adaptive(
value: _shelfEnabled,
onChanged: (value) {
setState(() => _shelfEnabled = value);
},
),
const SizedBox(width: 8),
const Text('Collision'),
Switch.adaptive(
value: _collisionEnabled,
onChanged: (value) {
setState(() => _collisionEnabled = value);
},
),
],
),
),
],
),
body: SafeArea(
child: SingleChildScrollView(
controller: _scrollController,
padding: const EdgeInsets.all(12),
child: XtalDashboard(
scrollController: _scrollController,
columns: 4,
items: _items,
gap: 12,
padding: const EdgeInsets.all(12),
collisionDetectionEnabled: _collisionEnabled,
animationConfig: const AnimationConfig(stiffness: 350, damping: 22),
gestureConfig: const GestureConfig(
resizeHandleSize: 28,
autoScrollConfig: AutoScrollConfig(),
),
resizeConstraints: const ResizeConstraints(
maxColumnSpan: 4,
maxRowSpan: 4,
),
shelfConfig: _shelfEnabled
? ShelfConfig(
position: ShelfPosition.right,
size: 100,
borderRadius: BorderRadius.circular(12),
backgroundColor: const Color(0x0D000000),
dropTargetColor: const Color(0x262196F3),
gapFromGrid: 16,
)
: null,
ghostBuilder: _buildGhost,
onItemsChanged: (items) {
setState(() => _items = items);
},
onShelfChanged: (shelfItems) {
setState(() => _shelfItems = shelfItems);
},
),
),
),
);
}
}