fix(studio): sidebar — fix icon-shift + use collapsed top space
Some checks failed
Security / Security check (push) Failing after 2s

Two UX bugs Stefan flagged on the v0.51.0 collapsible rail:

1. Icons re-positioned when the rail expanded. Operators
   already pointing at an icon had to chase it as the rail
   grew. Two causes: the _SidebarItem switched between
   `Center(icon)` (collapsed) and `Row(icon, gap, label)`
   (expanded), and the AnimatedContainer's horizontal padding
   switched between FaiSpace.sm (collapsed) and FaiSpace.md
   (expanded). Both flipped the icon's effective X-position.

   Now: same Row, same AnimatedContainer padding, in both
   states. The icon sits at the same X-pixel always; only
   the label slot toggles open beside it.

2. Top of the collapsed rail was wasted (only the brand-mark
   was visible; version + connection + channel disappeared
   completely). Use the same vertical real estate for
   glance-able compact equivalents:

   * tiny "v0.51" version text in mono below the brand
   * _CollapsedConnectionDot — 10 px circle, green/red/amber
     tonality matching FaiDeltaMark's mode states. Tooltip
     surfaces the full endpoint string.
   * _CollapsedChannelChip — 18 px circle with the channel's
     first letter (P / B / D / L) in the same accent tone
     the full pill uses, so "production" stays unmistakably
     red even at-a-glance.

When the rail expands, the full pills replace the compact
indicators as before. Tests still green (the existing widget
test booted without hover-expand and that path stays correct
either way).

Version: 0.51.0 → 0.51.1
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-05-30 14:53:18 +02:00
parent 0183771689
commit 4dbe7a28f6
2 changed files with 142 additions and 26 deletions

View file

@ -27,7 +27,7 @@ import 'widgets/widgets.dart';
/// Studio's own build version. Bump on every UI commit so the /// Studio's own build version. Bump on every UI commit so the
/// running app self-identifies visible in the sidebar header /// running app self-identifies visible in the sidebar header
/// and quick-glance proof that you're seeing the current build. /// and quick-glance proof that you're seeing the current build.
const String kStudioVersion = '0.51.0'; const String kStudioVersion = '0.51.1';
Future<void> main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -499,9 +499,7 @@ class _SidebarState extends State<_Sidebar> {
), ),
), ),
const SizedBox(height: FaiSpace.md), const SizedBox(height: FaiSpace.md),
// Connection pill only when expanded; the // Connection pill full version when expanded.
// status-dot is encoded in the delta-mark's mode
// so collapsed users still see at-a-glance state.
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: FaiSpace.lg), padding: const EdgeInsets.symmetric(horizontal: FaiSpace.lg),
child: _ConnectionPill( child: _ConnectionPill(
@ -529,6 +527,36 @@ class _SidebarState extends State<_Sidebar> {
child: _ChannelPill(channel: widget.activeChannel!), child: _ChannelPill(channel: widget.activeChannel!),
), ),
], ],
] else ...[
// Collapsed top: keep the brand mark and the
// most glance-able status indicators (version,
// connection dot, channel letter) so the rail
// still reads as FI Studio at-a-glance without
// hovering to expand.
const SizedBox(height: FaiSpace.sm),
Text(
'v$kStudioVersion'.replaceFirst(RegExp(r'-.*'), ''),
style: FaiTheme.mono(
size: 9,
color: theme.colorScheme.primary,
),
),
const SizedBox(height: FaiSpace.md),
Tooltip(
message: widget.connected == true
? 'Connected · ${widget.endpointLabel}'
: widget.connected == false
? 'Disconnected · ${widget.endpointLabel}'
: 'Connecting · ${widget.endpointLabel}',
child: _CollapsedConnectionDot(connected: widget.connected),
),
if (widget.activeChannel != null && widget.activeChannel!.isNotEmpty) ...[
const SizedBox(height: FaiSpace.sm),
Tooltip(
message: widget.activeChannel!,
child: _CollapsedChannelChip(channel: widget.activeChannel!),
),
],
], ],
const SizedBox(height: FaiSpace.xxl), const SizedBox(height: FaiSpace.xxl),
// Destinations. // Destinations.
@ -671,6 +699,91 @@ class _ChannelPill extends StatelessWidget {
} }
} }
/// Mini-version of [_ConnectionPill] used when the sidebar is
/// collapsed. A 10px dot in the same green/red/amber tonality
/// the FaiDeltaMark uses for live / idle / unknown.
class _CollapsedConnectionDot extends StatelessWidget {
final bool? connected;
const _CollapsedConnectionDot({required this.connected});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final color = connected == true
? FaiColors.success
: connected == false
? theme.colorScheme.error
: FaiColors.warning;
return Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: color.withValues(alpha: 0.35),
blurRadius: 4,
spreadRadius: 0.5,
),
],
),
);
}
}
/// Mini-version of [_ChannelPill] used when the sidebar is
/// collapsed. A 16px circular chip with the first letter of
/// the channel (P / B / D / L) in the same accent tone the
/// full pill uses, so production stays unmistakeable at a
/// glance.
class _CollapsedChannelChip extends StatelessWidget {
final String channel;
const _CollapsedChannelChip({required this.channel});
Color _accent(ColorScheme cs) {
switch (channel) {
case 'production':
return cs.error;
case 'beta':
return FaiColors.warning;
case 'dev':
return cs.primary;
case 'local':
default:
return cs.onSurfaceVariant;
}
}
String get _letter {
if (channel.isEmpty) return '·';
return channel.substring(0, 1).toUpperCase();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final accent = _accent(theme.colorScheme);
return Container(
width: 18,
height: 18,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: accent.withValues(alpha: 0.55)),
),
alignment: Alignment.center,
child: Text(
_letter,
style: FaiTheme.mono(
size: 10,
weight: FontWeight.w700,
color: accent,
),
),
);
}
}
class _ConnectionPill extends StatelessWidget { class _ConnectionPill extends StatelessWidget {
final bool? connected; final bool? connected;
final String label; final String label;
@ -808,14 +921,17 @@ class _SidebarItemState extends State<_SidebarItem> {
size: 18, size: 18,
color: color, color: color,
); );
final Widget content = widget.expanded // Same Row + same outer padding in both states so the icon
? Row( // stays at the exact same X-pixel before and after expand
// (operators don't have to re-aim at a destination they
// already pointed at). The label slot is conditional, the
// icon's position is not.
final content = Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
icon, icon,
if (widget.expanded) ...[
const SizedBox(width: FaiSpace.md), const SizedBox(width: FaiSpace.md),
// Flexible so the label can ellipsize during the
// collapse animation instead of overflowing.
Flexible( Flexible(
child: Text( child: Text(
label, label,
@ -825,8 +941,8 @@ class _SidebarItemState extends State<_SidebarItem> {
), ),
), ),
], ],
) ],
: Center(child: icon); );
final inner = Padding( final inner = Padding(
padding: const EdgeInsets.symmetric(vertical: 2), padding: const EdgeInsets.symmetric(vertical: 2),
@ -839,8 +955,8 @@ class _SidebarItemState extends State<_SidebarItem> {
onTap: widget.onTap, onTap: widget.onTap,
child: AnimatedContainer( child: AnimatedContainer(
duration: FaiMotion.fast, duration: FaiMotion.fast,
padding: EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: widget.expanded ? FaiSpace.md : FaiSpace.sm, horizontal: FaiSpace.md,
vertical: FaiSpace.md, vertical: FaiSpace.md,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(

View file

@ -1,7 +1,7 @@
name: fai_studio name: fai_studio
description: "F∆I Studio — desktop GUI for the F∆I hub" description: "F∆I Studio — desktop GUI for the F∆I hub"
publish_to: 'none' publish_to: 'none'
version: 0.51.0 version: 0.51.1
environment: environment:
sdk: ^3.11.0-200.1.beta sdk: ^3.11.0-200.1.beta