off_ide 0.1.3 copy "off_ide: ^0.1.3" to clipboard
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.

example/lib/main.dart

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,
                    ),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}
2
likes
160
points
174
downloads

Publisher

unverified uploader

Weekly Downloads

A high-performance, VS Code-like workspace shell widget for Flutter applications, featuring split editors, tab management, and a customizable sidebar.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

equatable, flutter, flutter_bloc, hydrated_bloc, path_provider, uuid

More

Packages that depend on off_ide