// ChainWorkspaceSwitcher โ€” the AppBar workspace (project) control. // // One control, two mechanics (docs/architecture/projects.md, // ยง Studio): for open/protected projects the choice sets the page // filter AND the label new runs are stamped with. "All projects" // stays reachable โ€” a filter, not a jail. Sealed areas never // appear here (own hub instance; stage-3 connection switch). // // The project colour is a marking dot only โ€” Studio's blue stays // the app accent (registered design deviation). import 'package:flutter/material.dart'; import '../data/hub.dart'; import '../data/workspace.dart'; import '../l10n/app_localizations.dart'; import '../theme/tokens.dart'; class ChainWorkspaceSwitcher extends StatelessWidget { const ChainWorkspaceSwitcher({super.key}); @override Widget build(BuildContext context) { return ListenableBuilder( listenable: Workspace.instance, builder: (context, _) { final l = AppLocalizations.of(context)!; final theme = Theme.of(context); final ws = Workspace.instance; final active = ws.active; final label = ws.isAll ? l.workspaceAll : (active?.name ?? ws.activeSlug); return Tooltip( message: l.workspaceSwitcherTooltip, child: PopupMenuButton( onOpened: ws.refresh, onSelected: ws.setActive, itemBuilder: (context) => [ PopupMenuItem( value: '', child: Row( children: [ Icon( Icons.grid_view_outlined, size: 16, color: theme.colorScheme.onSurfaceVariant, ), const SizedBox(width: ChainSpace.sm), Text(l.workspaceAll), ], ), ), if (ws.projects.isNotEmpty) const PopupMenuDivider(), for (final p in ws.projects) PopupMenuItem( value: p.slug, child: Row( children: [ _ProjectDot(project: p), const SizedBox(width: ChainSpace.sm), Flexible( child: Text(p.name, overflow: TextOverflow.ellipsis), ), if (p.isProtected) ...[ const SizedBox(width: ChainSpace.sm), Tooltip( message: l.workspaceProtectedHint, child: Icon( Icons.shield_outlined, size: 14, color: theme.colorScheme.onSurfaceVariant, ), ), ], ], ), ), ], child: Container( padding: const EdgeInsets.symmetric( horizontal: ChainSpace.md, vertical: 5, ), decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(ChainRadius.sm), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (active != null) ...[ _ProjectDot(project: active), const SizedBox(width: 6), ] else ...[ Icon( Icons.grid_view_outlined, size: 14, color: theme.colorScheme.onSurfaceVariant, ), const SizedBox(width: 6), ], ConstrainedBox( constraints: const BoxConstraints(maxWidth: 160), child: Text( label, overflow: TextOverflow.ellipsis, style: theme.textTheme.labelMedium, ), ), if (active?.isProtected ?? false) ...[ const SizedBox(width: 4), Icon( Icons.shield_outlined, size: 13, color: theme.colorScheme.onSurfaceVariant, ), ], const SizedBox(width: 2), Icon( Icons.arrow_drop_down, size: 18, color: theme.colorScheme.onSurfaceVariant, ), ], ), ), ), ); }, ); } } /// The project's registry colour as a small marking dot. Falls /// back to the theme's outline colour when the project has none. class _ProjectDot extends StatelessWidget { final ProjectRef project; const _ProjectDot({required this.project}); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Container( width: 10, height: 10, decoration: BoxDecoration( color: _parseHex(project.color) ?? theme.colorScheme.outline, shape: BoxShape.circle, ), ); } static Color? _parseHex(String hex) { final h = hex.replaceFirst('#', ''); if (h.length != 6) return null; final v = int.tryParse(h, radix: 16); if (v == null) return null; return Color(0xFF000000 | v); } }