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
|
/// 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 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),
|
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,25 +921,28 @@ 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
|
||||||
mainAxisSize: MainAxisSize.min,
|
// (operators don't have to re-aim at a destination they
|
||||||
children: [
|
// already pointed at). The label slot is conditional, the
|
||||||
icon,
|
// icon's position is not.
|
||||||
const SizedBox(width: FaiSpace.md),
|
final content = Row(
|
||||||
// Flexible so the label can ellipsize during the
|
mainAxisSize: MainAxisSize.min,
|
||||||
// collapse animation instead of overflowing.
|
children: [
|
||||||
Flexible(
|
icon,
|
||||||
child: Text(
|
if (widget.expanded) ...[
|
||||||
label,
|
const SizedBox(width: FaiSpace.md),
|
||||||
overflow: TextOverflow.fade,
|
Flexible(
|
||||||
softWrap: false,
|
child: Text(
|
||||||
style: theme.textTheme.labelMedium?.copyWith(color: color),
|
label,
|
||||||
),
|
overflow: TextOverflow.fade,
|
||||||
),
|
softWrap: false,
|
||||||
],
|
style: theme.textTheme.labelMedium?.copyWith(color: color),
|
||||||
)
|
),
|
||||||
: 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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue