off_ide 0.1.3
off_ide: ^0.1.3 copied to clipboard
A high-performance, VS Code-like workspace shell widget for Flutter applications, featuring split editors, tab management, and a customizable sidebar.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:off_ide/off_ide.dart';
import 'package:path_provider/path_provider.dart';
// Theme Constants
const kPrimaryColor = Color.fromARGB(255, 70, 100, 64); // Slate
const kAccentColor = Color(0xFFEDF2F7); // Light Grey-Blue
const kSurfaceColor = Color(0xFFF7FAFC); // Off White
// Typography
const kFontHeader = TextStyle(
fontWeight: FontWeight.bold,
color: kPrimaryColor,
);
void main() async {
WidgetsFlutterBinding.ensureInitialized();
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: kIsWeb
? HydratedStorageDirectory.web
: HydratedStorageDirectory(
(await getApplicationDocumentsDirectory()).path,
),
);
runApp(const OffIdeExampleApp());
}
// -----------------------------------------------------------------------------
// DYNAMIC CRM COMPONENTS
// -----------------------------------------------------------------------------
class RoleCubit extends Cubit<String> {
RoleCubit() : super('admin');
void toggleRole() => emit(state == 'admin' ? 'user' : 'admin');
}
class SchemaMappers {
static IconData getIcon(String? iconName) {
if (iconName == null) return Icons.circle_outlined;
switch (iconName) {
case 'folder_outlined':
return Icons.folder_outlined;
case 'people_outline':
return Icons.people_outline;
case 'analytics_outlined':
return Icons.analytics_outlined;
case 'settings_outlined':
return Icons.settings_outlined;
case 'people':
return Icons.people;
case 'dashboard':
return Icons.dashboard;
case 'map':
return Icons.map;
case 'view_kanban':
return Icons.view_kanban;
case 'storage':
return Icons.storage;
default:
return Icons.circle_outlined;
}
}
static Widget? getIconWidget(String? iconName) {
if (iconName == 'flutter_logo') {
return const FlutterLogo(size: 24);
} else if (iconName == 'custom_image') {
return const Icon(Icons.star, color: Colors.amber, size: 24);
}
return null;
}
static Map<String, Widget Function(BuildContext, Map<String, dynamic>?)>
getPageRegistry(String userRole) {
final Map<String, Widget Function(BuildContext, Map<String, dynamic>?)>
registry = {
'team-directory': (context, args) => const TeamDirectoryPage(),
'member-profile': (context, args) => MemberProfilePage(args: args ?? {}),
'project-overview': (context, args) => const ProjectOverviewPage(),
'office-layout': (context, args) => const OfficeLayoutPage(),
};
if (userRole == 'admin') {
registry['sprint-board'] = (context, args) => const SprintBoardPage();
registry['engineering-dash'] = (context, args) => Container(
color: Colors.orange.shade50,
child: const Center(
child: Text(
'Engineering Dashboard\n(Actionable Group Demo)',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
),
);
registry['resources'] = (context, args) => Container(
color: Colors.blue.shade50,
child: const Center(child: Text('Resources Configuration')),
);
}
return registry;
}
}
class SidebarParser {
static List<ActivityBarItem> parseActivityBar(
List<dynamic> schema,
String userRole,
) {
return schema
.where((activity) => _hasAccess(activity['allowedRoles'], userRole))
.map(
(activity) => ActivityBarItem(
id: activity['id'],
icon: SchemaMappers.getIcon(activity['icon']),
iconWidget: SchemaMappers.getIconWidget(activity['icon']),
label: activity['label'],
tooltip: activity['tooltip'],
),
)
.toList();
}
static Map<String, SidebarView> parseSidebarViews(
List<dynamic> schema,
String userRole,
) {
Map<String, SidebarView> views = {};
for (var activity in schema) {
if (!_hasAccess(activity['allowedRoles'], userRole)) continue;
if (activity['childBuilder'] != null) {
WidgetBuilder? builder;
if (activity['childBuilder'] == 'teamSidebar') {
builder = OffIdeExampleApp.buildTeamSidebar;
} else if (activity['childBuilder'] == 'settingsSidebar') {
builder = OffIdeExampleApp.buildSettingsSidebar;
}
views[activity['id']] = SidebarView(
id: activity['id'],
title:
activity['title'] ?? activity['label'].toString().toUpperCase(),
groups: const [],
childBuilder: builder,
);
continue;
}
List<MenuGroup> parsedGroups = [];
for (var group in (activity['groups'] ?? [])) {
if (!_hasAccess(group['allowedRoles'], userRole)) continue;
List<MenuSubGroup> parsedSubGroups = [];
for (var subGroup in (group['subGroups'] ?? [])) {
if (!_hasAccess(subGroup['allowedRoles'], userRole)) continue;
List<MenuItem> parsedItems = [];
for (var item in (subGroup['items'] ?? [])) {
if (!_hasAccess(item['allowedRoles'], userRole)) continue;
parsedItems.add(
MenuItem(
id: item['id'],
label: item['label'],
pageId: item['pageId'],
icon: SchemaMappers.getIcon(item['icon']),
iconWidget: SchemaMappers.getIconWidget(item['icon']),
),
);
}
parsedSubGroups.add(
MenuSubGroup(
id: subGroup['id'],
label: subGroup['label'],
icon: SchemaMappers.getIcon(subGroup['icon']),
iconWidget: SchemaMappers.getIconWidget(subGroup['icon']),
isExpanded: subGroup['isExpanded'] ?? false,
pageId: subGroup['pageId'],
items: parsedItems,
),
);
}
parsedGroups.add(
MenuGroup(
id: group['id'],
label: group['label'],
icon: SchemaMappers.getIcon(group['icon']),
iconWidget: SchemaMappers.getIconWidget(group['icon']),
isExpanded: group['isExpanded'] ?? false,
subGroups: parsedSubGroups,
),
);
}
views[activity['id']] = SidebarView(
id: activity['id'],
title: activity['title'] ?? activity['label'].toString().toUpperCase(),
groups: parsedGroups,
);
}
return views;
}
static bool _hasAccess(List<dynamic>? allowedRoles, String userRole) {
if (allowedRoles == null || allowedRoles.isEmpty) return true;
if (userRole == 'admin') return true;
return allowedRoles.cast<String>().contains(userRole);
}
}
final List<Map<String, dynamic>> crmSidebarSchema = [
{
"id": "explorer",
"icon": "flutter_logo",
"label": "Explorer",
"tooltip": "Project Explorer",
"title": "EXPLORER",
"allowedRoles": ["admin", "user"],
"groups": [
{
"id": "acme_corp",
"label": "Acme_Corp",
"isExpanded": true,
"allowedRoles": ["admin", "user"],
"subGroups": [
{
"id": "marketing",
"label": "Marketing",
"isExpanded": true,
"allowedRoles": ["admin", "user"],
"items": [
{
"id": "team_directory",
"label": "Team_Directory.list",
"pageId": "team-directory",
"icon": "people",
"allowedRoles": ["admin", "user"],
},
{
"id": "project_overview",
"label": "Project_Overview.dash",
"pageId": "project-overview",
"icon": "dashboard",
"allowedRoles": ["admin", "user"],
},
{
"id": "office_layout",
"label": "Office_Layout.map",
"pageId": "office-layout",
"icon": "map",
"allowedRoles": ["admin", "user"],
},
],
},
{
"id": "engineering",
"label": "Engineering",
"isExpanded": false,
"pageId": "engineering-dash",
"allowedRoles": ["admin"], // ONLY ADMIN SEES THIS
"items": [
{
"id": "sprint_board",
"label": "Sprint_Board.task",
"pageId": "sprint-board",
"icon": "view_kanban",
"allowedRoles": ["admin"],
},
{
"id": "resources",
"label": "Resources.cfg",
"pageId": "resources",
"icon": "storage",
"allowedRoles": ["admin"],
},
],
},
],
},
],
},
{
"id": "team",
"icon": "people_outline",
"label": "Team",
"tooltip": "Team Members",
"title": "TEAM",
"allowedRoles": ["admin", "user"],
"childBuilder": "teamSidebar",
},
{
"id": "analytics",
"icon": "analytics_outlined",
"label": "Analytics",
"tooltip": "Reports & Metrics (Admin Only)",
"title": "ANALYTICS",
"allowedRoles": ["admin"], // ONLY ADMIN SEES ANALYTICS
"childBuilder": "teamSidebar",
},
{
"id": "settings",
"icon": "settings_outlined",
"label": "Settings",
"tooltip": "Configuration",
"title": "SETTINGS",
"allowedRoles": ["admin", "user"],
"childBuilder": "settingsSidebar",
},
];
// -----------------------------------------------------------------------------
// APP WIDGET
// -----------------------------------------------------------------------------
class OffIdeExampleApp extends StatelessWidget {
const OffIdeExampleApp({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => RoleCubit(),
child: BlocBuilder<RoleCubit, String>(
builder: (context, currentRole) {
return MaterialApp(
title: 'Off IDE Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: kPrimaryColor,
surface: kSurfaceColor,
primary: kPrimaryColor,
),
useMaterial3: true,
cardTheme: CardThemeData(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: Colors.grey.shade200),
),
color: Colors.white,
),
),
home: WorkspaceShell(
config: WorkspaceConfig(
maxTabs: 10,
activityBarItems: SidebarParser.parseActivityBar(
crmSidebarSchema,
currentRole,
),
sidebarViews: SidebarParser.parseSidebarViews(
crmSidebarSchema,
currentRole,
),
pageRegistry: SchemaMappers.getPageRegistry(currentRole),
),
),
);
},
),
);
}
static Widget buildTeamSidebar(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(8),
children: _teamMembers.map((person) {
return ListTile(
leading: CircleAvatar(child: Text(person['initials']!)),
title: Text(person['name']!),
subtitle: Text(person['role']!),
onTap: () {
context.read<WorkspaceBloc>().add(
OpenTab(
pageId: 'member-profile',
title: person['name']!,
icon: Icons.person,
pageArgs: {
'name': person['name'],
'initials': person['initials'],
'role': person['role'],
'department': person['department'],
'status': person['status'],
},
),
);
},
);
}).toList(),
);
}
static Widget buildSettingsSidebar(BuildContext context) {
return BlocBuilder<RoleCubit, String>(
builder: (context, role) {
final isAdmin = role == 'admin';
return ListView(
padding: const EdgeInsets.all(16),
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isAdmin ? Colors.blue.shade50 : Colors.grey.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isAdmin ? Colors.blue.shade200 : Colors.grey.shade200,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Access Control',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: 8),
const Text(
'Toggle your role to see dynamic routing in action. Changes apply instantly.',
style: TextStyle(color: Colors.grey, fontSize: 13),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Admin Mode',
style: TextStyle(
fontWeight: FontWeight.w600,
color: kPrimaryColor,
),
),
Switch(
value: isAdmin,
onChanged: (val) {
context.read<RoleCubit>().toggleRole();
},
),
],
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
isAdmin
? '✅ Analytics & Engineering visible.'
: '❌ Analytics & Engineering hidden.',
style: TextStyle(
color: isAdmin ? Colors.green[700] : Colors.red[700],
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
],
);
},
);
}
static const List<Map<String, String>> _teamMembers = [
{
'name': 'Emily Richardson',
'initials': 'ER',
'role': 'Product Manager',
'department': 'Marketing',
'status': 'Active',
},
{
'name': 'Marcus Vane',
'initials': 'MV',
'role': 'Frontend Developer',
'department': 'Engineering',
'status': 'Active',
},
{
'name': 'Sarah Jenkins',
'initials': 'SJ',
'role': 'UX Designer',
'department': 'Design',
'status': 'Away',
},
{
'name': 'Alan Grant',
'initials': 'AG',
'role': 'Team Lead',
'department': 'Engineering',
'status': 'Active',
},
];
}
// -----------------------------------------------------------------------------
// 1. Team Directory Page (Rich List)
// -----------------------------------------------------------------------------
class TeamDirectoryPage extends StatelessWidget {
const TeamDirectoryPage({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Team Directory',
style: kFontHeader.copyWith(fontSize: 32),
),
const SizedBox(height: 8),
Text(
'Showing all active members across departments',
style: TextStyle(color: Colors.grey[600], fontSize: 16),
),
],
),
Row(
children: [
OutlinedButton(
onPressed: () {},
style: OutlinedButton.styleFrom(
foregroundColor: kPrimaryColor,
side: const BorderSide(color: kPrimaryColor),
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
),
child: const Text('Export CSV'),
),
const SizedBox(width: 16),
FilledButton.icon(
onPressed: () {},
style: FilledButton.styleFrom(
backgroundColor: kPrimaryColor,
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
),
icon: const Icon(Icons.add),
label: const Text('Add Member'),
),
],
),
],
),
const SizedBox(height: 40),
// Rich Table
Expanded(
child: Card(
child: ListView.separated(
padding: const EdgeInsets.all(0),
itemCount: _memberList.length + 1, // +1 for header
separatorBuilder: (context, index) => const Divider(height: 1),
itemBuilder: (context, index) {
if (index == 0) return _buildTableHeader();
return _buildMemberRow(context, _memberList[index - 1]);
},
),
),
),
],
),
);
}
Widget _buildTableHeader() {
return const Padding(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Row(
children: [
Expanded(
flex: 3,
child: Text('NAME', style: TextStyle(fontWeight: FontWeight.bold)),
),
Expanded(
flex: 2,
child: Text('ROLE', style: TextStyle(fontWeight: FontWeight.bold)),
),
Expanded(
flex: 2,
child: Text(
'DEPARTMENT',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
Expanded(
flex: 1,
child: Text(
'STATUS',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
],
),
);
}
Widget _buildMemberRow(BuildContext context, Map<String, dynamic> member) {
return InkWell(
onTap: () {
context.read<WorkspaceBloc>().add(
OpenTab(
pageId: 'member-profile',
title: member['name'] as String,
icon: Icons.person,
pageArgs: {
'name': member['name'],
'initials': member['initials'],
'role': member['role'],
'department': member['department'],
'status': member['status'],
},
),
);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Row(
children: [
// Name with Avatar
Expanded(
flex: 3,
child: Row(
children: [
CircleAvatar(
backgroundColor: member['avatarColor'] as Color,
foregroundColor: kPrimaryColor,
child: Text(member['initials'] as String),
),
const SizedBox(width: 12),
Text(
member['name'] as String,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
),
// Role
Expanded(
flex: 2,
child: Text(
member['role'] as String,
style: const TextStyle(
fontStyle: FontStyle.italic,
color: Colors.grey,
),
),
),
// Department
Expanded(flex: 2, child: Text(member['department'] as String)),
// Status Badge
Expanded(
flex: 1,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: (member['active'] as bool)
? Colors.green.withValues(alpha: 0.1)
: Colors.grey.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20),
),
child: Text(
(member['status'] as String).toUpperCase(),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: (member['active'] as bool)
? Colors.green[800]
: Colors.grey[800],
),
textAlign: TextAlign.center,
),
),
),
],
),
),
);
}
static final List<Map<String, dynamic>> _memberList = [
{
'name': 'Emily Richardson',
'initials': 'ER',
'avatarColor': const Color(0xFFE0F2F1),
'role': 'Product Manager',
'department': 'Marketing',
'status': 'Active',
'active': true,
},
{
'name': 'Marcus Vane',
'initials': 'MV',
'avatarColor': const Color(0xFFFFF3E0),
'role': 'Frontend Developer',
'department': 'Engineering',
'status': 'Active',
'active': true,
},
{
'name': 'Sarah Jenkins',
'initials': 'SJ',
'avatarColor': const Color(0xFFE3F2FD),
'role': 'UX Designer',
'department': 'Design',
'status': 'Away',
'active': false,
},
{
'name': 'Alan Grant',
'initials': 'AG',
'avatarColor': const Color(0xFFF3E5F5),
'role': 'Team Lead',
'department': 'Engineering',
'status': 'Active',
'active': true,
},
];
}
// -----------------------------------------------------------------------------
// 2. Project Overview Page (Dashboard with KPIs)
// -----------------------------------------------------------------------------
class ProjectOverviewPage extends StatelessWidget {
const ProjectOverviewPage({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Project Overview', style: kFontHeader.copyWith(fontSize: 32)),
const SizedBox(height: 24),
// KPI Cards
Row(
children: [
_buildKPICard(
'Active Projects',
'24',
'+3 this month',
Icons.work_outline,
Colors.blue,
),
const SizedBox(width: 24),
_buildKPICard(
'Completion Rate',
'87%',
'4 due this week',
Icons.check_circle_outline,
Colors.green,
),
const SizedBox(width: 24),
_buildKPICard(
'Open Issues',
'18',
'5 critical',
Icons.bug_report_outlined,
Colors.red,
),
],
),
const SizedBox(height: 32),
// Filters
Row(
children: [
Expanded(
child: TextField(
decoration: InputDecoration(
hintText: 'Search projects...',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Colors.grey),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
),
),
),
const SizedBox(width: 16),
const DropdownMenu<String>(
initialSelection: 'All Teams',
dropdownMenuEntries: [
DropdownMenuEntry(value: 'All Teams', label: 'All Teams'),
DropdownMenuEntry(value: 'Marketing', label: 'Marketing'),
DropdownMenuEntry(value: 'Engineering', label: 'Engineering'),
],
),
],
),
const SizedBox(height: 24),
// Grid
Expanded(
child: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 1.8,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
),
itemCount: 9,
itemBuilder: (context, index) {
return _buildProjectCard(index);
},
),
),
],
),
);
}
Widget _buildKPICard(
String label,
String value,
String subtext,
IconData icon,
Color color,
) {
return Expanded(
child: Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: const TextStyle(
color: Colors.grey,
fontWeight: FontWeight.w600,
),
),
Icon(icon, color: color),
],
),
const SizedBox(height: 12),
Text(
value,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
subtext,
style: TextStyle(color: color, fontWeight: FontWeight.w500),
),
],
),
),
),
);
}
Widget _buildProjectCard(int index) {
final status = index % 3 == 0
? 'In Progress'
: (index % 2 == 0 ? 'On Hold' : 'Review');
final color = index % 3 == 0
? Colors.blue
: (index % 2 == 0 ? Colors.orange : Colors.green);
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(
backgroundColor: Colors.grey[200],
child: Text('P$index'),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Project #${100 + index}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
Text(
'Sprint ${index + 1}',
style: const TextStyle(color: Colors.grey),
),
],
),
],
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
status,
style: TextStyle(
color: color,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
);
}
}
// -----------------------------------------------------------------------------
// 3. Office Layout Page (Canvas Diagram)
// -----------------------------------------------------------------------------
class OfficeLayoutPage extends StatelessWidget {
const OfficeLayoutPage({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Office Layout — Floor 3',
style: kFontHeader.copyWith(fontSize: 32),
),
Row(
children: [
_buildLegendItem('Occupied', Colors.green),
const SizedBox(width: 16),
_buildLegendItem('Available', Colors.grey),
const SizedBox(width: 16),
_buildLegendItem('Reserved', Colors.blue),
],
),
],
),
const SizedBox(height: 24),
Expanded(
child: Card(
color: const Color(0xFFF9F9F9),
child: Center(
child: CustomPaint(
size: const Size(600, 400),
painter: _LayoutPainter(),
),
),
),
),
],
),
);
}
Widget _buildLegendItem(String label, Color color) {
return Row(
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
),
const SizedBox(width: 8),
Text(label),
],
);
}
}
class _LayoutPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..style = PaintingStyle.fill
..strokeWidth = 2;
// Hallway
paint.color = Colors.grey[300]!;
canvas.drawRect(
Rect.fromLTWH(0, size.height / 2 - 40, size.width, 80),
paint,
);
// Offices Top
paint.color = Colors.green[100]!;
for (int i = 0; i < 5; i++) {
canvas.drawRect(Rect.fromLTWH(i * 120 + 10.0, 10.0, 100, 120), paint);
canvas.drawLine(
Offset(i * 120 + 60.0, 130.0),
Offset(i * 120 + 60.0, 150.0),
Paint()
..color = Colors.black
..strokeWidth = 2,
);
}
// Meeting Rooms Bottom
paint.color = Colors.blue[100]!;
for (int i = 0; i < 5; i++) {
if (i == 2) paint.color = Colors.grey[200]!; // Available
canvas.drawRect(
Rect.fromLTWH(i * 120 + 10.0, size.height - 130, 100, 120),
paint,
);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
// -----------------------------------------------------------------------------
// 4. Sprint Board Page (Kanban-style)
// -----------------------------------------------------------------------------
class SprintBoardPage extends StatelessWidget {
const SprintBoardPage({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Sprint Board', style: kFontHeader.copyWith(fontSize: 32)),
SegmentedButton<String>(
segments: const [
ButtonSegment(value: 'board', label: Text('Board')),
ButtonSegment(value: 'list', label: Text('List')),
ButtonSegment(value: 'timeline', label: Text('Timeline')),
],
selected: const {'board'},
onSelectionChanged: (_) {},
),
],
),
const SizedBox(height: 24),
Expanded(
child: Row(
children: [
_buildColumn('To Do', Colors.grey, [
'Setup CI pipeline',
'Write API docs',
'Design login page',
]),
const SizedBox(width: 16),
_buildColumn('In Progress', Colors.blue, [
'Build dashboard',
'Integrate payments',
]),
const SizedBox(width: 16),
_buildColumn('Review', Colors.orange, [
'Auth module',
'User settings',
]),
const SizedBox(width: 16),
_buildColumn('Done', Colors.green, [
'Project setup',
'DB schema',
'Landing page',
'Email templates',
]),
],
),
),
],
),
);
}
Widget _buildColumn(String title, Color color, List<String> items) {
return Expanded(
child: Card(
child: Column(
children: [
// Column Header
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: Colors.grey[300]!)),
),
child: Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Text(
title,
style: const TextStyle(fontWeight: FontWeight.bold),
),
const Spacer(),
Text(
'${items.length}',
style: TextStyle(color: Colors.grey[500], fontSize: 12),
),
],
),
),
// Items
Expanded(
child: ListView.builder(
padding: const EdgeInsets.all(12),
itemCount: items.length,
itemBuilder: (context, index) {
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.05),
border: Border(left: BorderSide(color: color, width: 3)),
borderRadius: BorderRadius.circular(4),
),
child: Text(
items[index],
style: const TextStyle(fontWeight: FontWeight.w500),
),
);
},
),
),
],
),
),
);
}
}
// -----------------------------------------------------------------------------
// 5. Member Profile Page (Opens per member)
// -----------------------------------------------------------------------------
class MemberProfilePage extends StatelessWidget {
const MemberProfilePage({super.key, required this.args});
final Map<String, dynamic> args;
@override
Widget build(BuildContext context) {
final name = args['name'] as String? ?? 'Unknown';
final initials = args['initials'] as String? ?? '??';
final role = args['role'] as String? ?? 'N/A';
final department = args['department'] as String? ?? 'N/A';
final status = args['status'] as String? ?? 'N/A';
final isActive = status == 'Active';
return Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
children: [
CircleAvatar(
radius: 36,
backgroundColor: kPrimaryColor.withValues(alpha: .1),
child: Text(
initials,
style: kFontHeader.copyWith(fontSize: 24),
),
),
const SizedBox(width: 24),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(name, style: kFontHeader.copyWith(fontSize: 28)),
const SizedBox(height: 4),
Text(
role,
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
fontStyle: FontStyle.italic,
),
),
],
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
color: isActive
? Colors.green.withValues(alpha: .1)
: Colors.grey.withValues(alpha: .1),
borderRadius: BorderRadius.circular(20),
),
child: Text(
status.toUpperCase(),
style: TextStyle(
fontWeight: FontWeight.bold,
color: isActive ? Colors.green[800] : Colors.grey[600],
),
),
),
],
),
const SizedBox(height: 32),
const Divider(),
const SizedBox(height: 24),
// Info Cards
Row(
children: [
_infoCard('Department', department, Icons.business),
const SizedBox(width: 16),
_infoCard('Location', 'Floor 3, Desk 12', Icons.location_on),
const SizedBox(width: 16),
_infoCard('Joined', 'Jan 2023', Icons.calendar_today),
],
),
const SizedBox(height: 32),
// Activity log
Text('Recent Activity', style: kFontHeader.copyWith(fontSize: 20)),
const SizedBox(height: 16),
Expanded(
child: Card(
child: ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: 5,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
final actions = [
'Pushed 3 commits to main',
'Reviewed PR #142 — Dashboard layout',
'Updated project timeline',
'Completed onboarding checklist',
'Added comments on design spec',
];
return ListTile(
leading: Icon(
Icons.circle,
size: 8,
color: Colors.green[400],
),
title: Text(actions[index]),
trailing: Text(
'${index + 1}h ago',
style: TextStyle(color: Colors.grey[500], fontSize: 12),
),
);
},
),
),
),
],
),
);
}
Widget _infoCard(String label, String value, IconData icon) {
return Expanded(
child: Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Row(
children: [
Icon(icon, color: kPrimaryColor, size: 28),
const SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
value,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
),
),
);
}
}