fix(studio): sidebar — fix icon-shift + use collapsed top space
Some checks failed
Security / Security check (push) Failing after 2s
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:
parent
0183771689
commit
4dbe7a28f6
2 changed files with 142 additions and 26 deletions
166
lib/main.dart
166
lib/main.dart
|
|
@ -27,7 +27,7 @@ import 'widgets/widgets.dart';
|
|||
/// Studio's own build version. Bump on every UI commit so the
|
||||
/// running app self-identifies — visible in the sidebar header
|
||||
/// 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 {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
|
@ -499,9 +499,7 @@ class _SidebarState extends State<_Sidebar> {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: FaiSpace.md),
|
||||
// Connection pill — only when expanded; the
|
||||
// status-dot is encoded in the delta-mark's mode
|
||||
// so collapsed users still see at-a-glance state.
|
||||
// Connection pill — full version when expanded.
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: FaiSpace.lg),
|
||||
child: _ConnectionPill(
|
||||
|
|
@ -529,6 +527,36 @@ class _SidebarState extends State<_Sidebar> {
|
|||
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 F∆I 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),
|
||||
// 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 {
|
||||
final bool? connected;
|
||||
final String label;
|
||||
|
|
@ -808,25 +921,28 @@ class _SidebarItemState extends State<_SidebarItem> {
|
|||
size: 18,
|
||||
color: color,
|
||||
);
|
||||
final Widget content = widget.expanded
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
icon,
|
||||
const SizedBox(width: FaiSpace.md),
|
||||
// Flexible so the label can ellipsize during the
|
||||
// collapse animation instead of overflowing.
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
style: theme.textTheme.labelMedium?.copyWith(color: color),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Center(child: icon);
|
||||
// Same Row + same outer padding in both states so the icon
|
||||
// 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,
|
||||
children: [
|
||||
icon,
|
||||
if (widget.expanded) ...[
|
||||
const SizedBox(width: FaiSpace.md),
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
style: theme.textTheme.labelMedium?.copyWith(color: color),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
final inner = Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
|
|
@ -839,8 +955,8 @@ class _SidebarItemState extends State<_SidebarItem> {
|
|||
onTap: widget.onTap,
|
||||
child: AnimatedContainer(
|
||||
duration: FaiMotion.fast,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: widget.expanded ? FaiSpace.md : FaiSpace.sm,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: FaiSpace.md,
|
||||
vertical: FaiSpace.md,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
name: fai_studio
|
||||
description: "F∆I Studio — desktop GUI for the F∆I hub"
|
||||
publish_to: 'none'
|
||||
version: 0.51.0
|
||||
version: 0.51.1
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.0-200.1.beta
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue