xtal_dashboard 0.2.0
xtal_dashboard: ^0.2.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 _editingEnabled = true;
bool _shelfEnabled = true;
late List<DashboardItemData> _items;
late List<DashboardItemData> _shelfItems;
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
_items = _createInitialItems();
_shelfItems = _createInitialShelfItems();
}
// ---- Initial shelf items ----
static const _shelfItemConfigs = <(String, int, int, int, String)>[
// (id, columnSpan, rowSpan, colorValue, title)
('S1', 1, 1, 0xFFEF5350, 'CPU'), // Red 400
('S2', 2, 1, 0xFF7E57C2, 'Recent Activity Log'), // Deep Purple 400
('S3', 1, 1, 0xFF29B6F6, 'System Performance Monitor'), // Light Blue 400
];
List<DashboardItemData> _createInitialShelfItems() {
return _shelfItemConfigs.map((config) {
final (id, colSpan, rowSpan, colorValue, title) = config;
final color = Color(colorValue);
return DashboardItemData(
gridItem: GridItem(
id: id,
column: 0,
row: 0,
columnSpan: colSpan,
rowSpan: rowSpan,
),
builder: (context, isBeingDragged) => _buildCard(color, id),
title: title,
);
}).toList();
}
@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, String)>[
// (id, column, row, columnSpan, rowSpan, colorValue, title)
('A', 0, 0, 2, 2, 0xFF42A5F5, 'Overview Dashboard'), // Blue 400
('B', 2, 0, 2, 1, 0xFF26A69A, 'Network'), // Teal 400
('C', 2, 1, 1, 1, 0xFFAB47BC, 'Mem'), // Purple 300
('D', 3, 1, 1, 1, 0xFFEC407A, 'IO'), // Pink 300
('E', 0, 2, 1, 1, 0xFFFFCA28, 'Alerts'), // Amber 400
('F', 1, 2, 2, 1, 0xFFFFA726, 'Request Throughput'), // Orange 400
('G', 3, 2, 1, 1, 0xFF66BB6A, 'Up'), // Green 400
(
'H',
0,
3,
3,
1,
0xFF5C6BC0,
'Latency Distribution (p50 / p95 / p99)',
), // Indigo 300
];
List<DashboardItemData> _createInitialItems() {
return _itemConfigs.map((config) {
final (id, col, row, colSpan, rowSpan, colorValue, title) = 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),
title: title,
);
}).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('Edit'),
Switch.adaptive(
value: _editingEnabled,
onChanged: (value) {
setState(() => _editingEnabled = value);
},
),
const SizedBox(width: 8),
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,
editingEnabled: _editingEnabled,
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,
showTitles: true,
borderRadius: BorderRadius.circular(12),
backgroundColor: const Color(0x0D000000),
dropTargetColor: const Color(0x262196F3),
gapFromGrid: 16,
hoverOpenDelay: Duration.zero,
autoCloseDelay: const Duration(seconds: 3),
)
: null,
initialShelfItems: _shelfItems,
ghostBuilder: _buildGhost,
onItemsChanged: (items) {
setState(() => _items = items);
},
onShelfChanged: (shelfItems) {
setState(() => _shelfItems = shelfItems);
},
),
),
),
);
}
}