feat(nav): discoverable sidebar — instant tooltips, shortcuts made visible, pinnable rail
Some checks are pending
Security / Security check (push) Waiting to run
Some checks are pending
Security / Security check (push) Waiting to run
The icon-only rail forced first-time users to guess (usertest: Senior, a11y, UX personas). Three changes: - Nav tooltips appear instantly and carry the page shortcut (Cmd+1..9, Ctrl on non-mac — Ctrl activators added); expanded labels show the same hint. Explicit button semantics for screen readers on every destination. - A visible 'Search & commands' row above the footer opens the existing Cmd+K palette, which nothing in the UI advertised. - Settings -> Appearance gains 'Keep the navigation expanded': pins the rail with permanent labels (persisted preference). Footer strip and pillar toggle made overflow-safe for the animating rail; responsive test scrolls the by-design scrollable destinations list. Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
ca01fd2ec0
commit
c436e12601
11 changed files with 364 additions and 30 deletions
23
lib/data/sidebar_prefs.dart
Normal file
23
lib/data/sidebar_prefs.dart
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// Operator preference: keep the sidebar permanently expanded so
|
||||
// every destination shows its text label without hovering. Off by
|
||||
// default (hover-expand rail); a11y/senior operators flip it in
|
||||
// Settings → Appearance.
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class SidebarPrefs {
|
||||
static const _kPinnedKey = 'sidebar.pinned';
|
||||
static final ValueNotifier<bool> pinned = ValueNotifier(false);
|
||||
|
||||
static Future<void> load() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
pinned.value = prefs.getBool(_kPinnedKey) ?? false;
|
||||
}
|
||||
|
||||
static Future<void> setPinned(bool value) async {
|
||||
pinned.value = value;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_kPinnedKey, value);
|
||||
}
|
||||
}
|
||||
|
|
@ -1549,6 +1549,9 @@
|
|||
"doctorPathStudioErrors": "Studio-Fehler",
|
||||
"settingsCategoryGeneral": "Allgemein",
|
||||
"settingsCategoryAppearance": "Darstellung",
|
||||
"sidebarSearchLabel": "Suchen & Befehle",
|
||||
"settingsSidebarPinnedTitle": "Navigation immer ausgeklappt",
|
||||
"settingsSidebarPinnedBody": "Zeigt die Beschriftungen der Seitenleiste dauerhaft an, statt sie nur beim Überfahren mit der Maus einzublenden.",
|
||||
"settingsCategoryAi": "System-KI",
|
||||
"settingsCategoryIntegrations": "Integrationen",
|
||||
"settingsCategorySecurity": "Sicherheit",
|
||||
|
|
|
|||
|
|
@ -1573,6 +1573,9 @@
|
|||
"doctorPathStudioErrors": "Studio errors",
|
||||
"settingsCategoryGeneral": "General",
|
||||
"settingsCategoryAppearance": "Appearance",
|
||||
"sidebarSearchLabel": "Search & commands",
|
||||
"settingsSidebarPinnedTitle": "Keep the navigation expanded",
|
||||
"settingsSidebarPinnedBody": "Shows the sidebar labels permanently instead of only while hovering with the mouse.",
|
||||
"settingsCategoryAi": "System AI",
|
||||
"settingsCategoryIntegrations": "Integrations",
|
||||
"settingsCategorySecurity": "Security",
|
||||
|
|
|
|||
|
|
@ -4544,6 +4544,24 @@ abstract class AppLocalizations {
|
|||
/// **'Appearance'**
|
||||
String get settingsCategoryAppearance;
|
||||
|
||||
/// No description provided for @sidebarSearchLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Search & commands'**
|
||||
String get sidebarSearchLabel;
|
||||
|
||||
/// No description provided for @settingsSidebarPinnedTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Keep the navigation expanded'**
|
||||
String get settingsSidebarPinnedTitle;
|
||||
|
||||
/// No description provided for @settingsSidebarPinnedBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Shows the sidebar labels permanently instead of only while hovering with the mouse.'**
|
||||
String get settingsSidebarPinnedBody;
|
||||
|
||||
/// No description provided for @settingsCategoryAi.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -2666,6 +2666,16 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get settingsCategoryAppearance => 'Darstellung';
|
||||
|
||||
@override
|
||||
String get sidebarSearchLabel => 'Suchen & Befehle';
|
||||
|
||||
@override
|
||||
String get settingsSidebarPinnedTitle => 'Navigation immer ausgeklappt';
|
||||
|
||||
@override
|
||||
String get settingsSidebarPinnedBody =>
|
||||
'Zeigt die Beschriftungen der Seitenleiste dauerhaft an, statt sie nur beim Überfahren mit der Maus einzublenden.';
|
||||
|
||||
@override
|
||||
String get settingsCategoryAi => 'System-KI';
|
||||
|
||||
|
|
|
|||
|
|
@ -2668,6 +2668,16 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get settingsCategoryAppearance => 'Appearance';
|
||||
|
||||
@override
|
||||
String get sidebarSearchLabel => 'Search & commands';
|
||||
|
||||
@override
|
||||
String get settingsSidebarPinnedTitle => 'Keep the navigation expanded';
|
||||
|
||||
@override
|
||||
String get settingsSidebarPinnedBody =>
|
||||
'Shows the sidebar labels permanently instead of only while hovering with the mouse.';
|
||||
|
||||
@override
|
||||
String get settingsCategoryAi => 'System AI';
|
||||
|
||||
|
|
|
|||
288
lib/main.dart
288
lib/main.dart
|
|
@ -6,12 +6,14 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:chain_client_sdk/chain_client_sdk.dart';
|
||||
import 'package:flutter/foundation.dart' show defaultTargetPlatform;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'data/chain_log.dart';
|
||||
import 'data/error_presentation.dart';
|
||||
import 'data/hub.dart';
|
||||
import 'data/sidebar_prefs.dart';
|
||||
import 'data/workspace.dart';
|
||||
import 'data/system_actions.dart';
|
||||
import 'data/theme_plugin.dart';
|
||||
|
|
@ -74,6 +76,10 @@ Future<void> main() async {
|
|||
() => HubService.instance.loadLocale(),
|
||||
);
|
||||
final themePlugin = await _restoreOr(null, loadActiveThemePlugin);
|
||||
await _restoreOr(null, () async {
|
||||
await SidebarPrefs.load();
|
||||
return null;
|
||||
});
|
||||
runApp(
|
||||
StudioApp(
|
||||
initialThemeMode: themeMode,
|
||||
|
|
@ -608,8 +614,9 @@ class StudioShellState extends State<StudioShell> {
|
|||
final theme = Theme.of(context);
|
||||
return Shortcuts(
|
||||
shortcuts: <ShortcutActivator, Intent>{
|
||||
// Cmd+1..6 jumps to the matching destination. Numbered
|
||||
// 1-based to match the visual order in the sidebar.
|
||||
// Cmd+1..9 (Ctrl on non-mac) jumps to the matching
|
||||
// destination. Numbered 1-based to match the visual
|
||||
// order in the sidebar.
|
||||
for (var i = 0; i < _pages.length; i++)
|
||||
SingleActivator(
|
||||
LogicalKeyboardKey(LogicalKeyboardKey.digit1.keyId + i),
|
||||
|
|
@ -617,6 +624,13 @@ class StudioShellState extends State<StudioShell> {
|
|||
): _GoToPageIntent(
|
||||
i,
|
||||
),
|
||||
for (var i = 0; i < _pages.length; i++)
|
||||
SingleActivator(
|
||||
LogicalKeyboardKey(LogicalKeyboardKey.digit1.keyId + i),
|
||||
control: true,
|
||||
): _GoToPageIntent(
|
||||
i,
|
||||
),
|
||||
// Settings dialog. macOS reserves Cmd+, for the native
|
||||
// app-Preferences menu item, which Flutter Desktop
|
||||
// doesn't wire up — the keystroke is captured by the OS
|
||||
|
|
@ -661,15 +675,19 @@ class StudioShellState extends State<StudioShell> {
|
|||
child: Scaffold(
|
||||
body: Row(
|
||||
children: [
|
||||
_Sidebar(
|
||||
selectedIndex: _selectedIndex,
|
||||
onSelect: (i) => setState(() => _selectedIndex = i),
|
||||
pages: _pages,
|
||||
connected: _connected,
|
||||
endpointLabel: HubService.instance.endpointLabel,
|
||||
activeChannel: _activeChannel,
|
||||
pendingApprovals: _pendingApprovals,
|
||||
forceExpanded: widget.startSidebarExpanded,
|
||||
ValueListenableBuilder<bool>(
|
||||
valueListenable: SidebarPrefs.pinned,
|
||||
builder: (context, pinned, _) => _Sidebar(
|
||||
selectedIndex: _selectedIndex,
|
||||
onSelect: (i) => setState(() => _selectedIndex = i),
|
||||
pages: _pages,
|
||||
connected: _connected,
|
||||
endpointLabel: HubService.instance.endpointLabel,
|
||||
activeChannel: _activeChannel,
|
||||
pendingApprovals: _pendingApprovals,
|
||||
forceExpanded: widget.startSidebarExpanded || pinned,
|
||||
onOpenSearch: _openSearchPalette,
|
||||
),
|
||||
),
|
||||
Container(width: 1, color: theme.colorScheme.outlineVariant),
|
||||
Expanded(
|
||||
|
|
@ -787,6 +805,11 @@ class _OpenSearchIntent extends Intent {
|
|||
const _OpenSearchIntent();
|
||||
}
|
||||
|
||||
/// Platform-truthful label for the app's primary-modifier
|
||||
/// shortcuts (the activators bind ⌘ on macOS and Ctrl elsewhere).
|
||||
String _metaShortcut(String key) =>
|
||||
defaultTargetPlatform == TargetPlatform.macOS ? '⌘$key' : 'Ctrl+$key';
|
||||
|
||||
class _Sidebar extends StatefulWidget {
|
||||
final int selectedIndex;
|
||||
final ValueChanged<int> onSelect;
|
||||
|
|
@ -804,11 +827,17 @@ class _Sidebar extends StatefulWidget {
|
|||
/// without polling the page.
|
||||
final int pendingApprovals;
|
||||
|
||||
/// Test-only: start fully expanded (controller at 1.0) and disable
|
||||
/// hover, so a widget test can measure the expanded layout without a
|
||||
/// hover gesture. Always false in production.
|
||||
/// Keep the rail fully expanded and disable hover-collapse.
|
||||
/// True when the operator pins the sidebar (Settings →
|
||||
/// Appearance) and in widget tests measuring the expanded
|
||||
/// layout.
|
||||
final bool forceExpanded;
|
||||
|
||||
/// Opens the Cmd+K command palette — the sidebar carries a
|
||||
/// visible search row so the palette is discoverable without
|
||||
/// knowing the shortcut.
|
||||
final VoidCallback? onOpenSearch;
|
||||
|
||||
const _Sidebar({
|
||||
required this.selectedIndex,
|
||||
required this.onSelect,
|
||||
|
|
@ -818,6 +847,7 @@ class _Sidebar extends StatefulWidget {
|
|||
required this.activeChannel,
|
||||
this.pendingApprovals = 0,
|
||||
this.forceExpanded = false,
|
||||
this.onOpenSearch,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -863,6 +893,16 @@ class _SidebarState extends State<_Sidebar>
|
|||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _Sidebar old) {
|
||||
super.didUpdateWidget(old);
|
||||
// The pin toggle flips at runtime (Settings → Appearance):
|
||||
// animate to the new resting state instead of jumping.
|
||||
if (widget.forceExpanded != old.forceExpanded) {
|
||||
widget.forceExpanded ? _ctrl.forward() : _ctrl.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ctrl.dispose();
|
||||
|
|
@ -1026,11 +1066,33 @@ class _SidebarState extends State<_Sidebar>
|
|||
widget.pendingApprovals > 0
|
||||
? widget.pendingApprovals
|
||||
: null,
|
||||
// Cmd+1..9 jump to the destination; the
|
||||
// hint rides in tooltip + expanded label
|
||||
// so the shortcut is discoverable.
|
||||
shortcutHint: i < 9 ? _metaShortcut('${i + 1}') : null,
|
||||
onTap: () => widget.onSelect(i),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Visible entry point for the Cmd+K palette — the
|
||||
// shortcut exists since 0.3x but nothing in the UI
|
||||
// said so (usertest: "no shortcuts anywhere").
|
||||
if (widget.onOpenSearch != null)
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final l = AppLocalizations.of(context)!;
|
||||
return _SidebarActionRow(
|
||||
icon: Icons.search,
|
||||
label: l.sidebarSearchLabel,
|
||||
shortcutHint: _metaShortcut('K'),
|
||||
t: t,
|
||||
labelsInteractive: labelsInteractive,
|
||||
iconColumnWidth: _collapsedWidth,
|
||||
onTap: widget.onOpenSearch!,
|
||||
);
|
||||
},
|
||||
),
|
||||
_Footer(
|
||||
t: t,
|
||||
labelsInteractive: labelsInteractive,
|
||||
|
|
@ -1535,6 +1597,11 @@ class _SidebarItem extends StatefulWidget {
|
|||
/// Approvals item for pending count.
|
||||
final int? badge;
|
||||
|
||||
/// Keyboard shortcut ('⌘2') shown in the tooltip and, subtly,
|
||||
/// next to the expanded label — discoverability for the
|
||||
/// existing Cmd+1..9 navigation.
|
||||
final String? shortcutHint;
|
||||
|
||||
const _SidebarItem({
|
||||
super.key,
|
||||
required this.page,
|
||||
|
|
@ -1544,6 +1611,7 @@ class _SidebarItem extends StatefulWidget {
|
|||
required this.iconColumnWidth,
|
||||
required this.onTap,
|
||||
this.badge,
|
||||
this.shortcutHint,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -1627,13 +1695,33 @@ class _SidebarItemState extends State<_SidebarItem> {
|
|||
opacity: widget.t,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: ChainSpace.md),
|
||||
child: Text(
|
||||
label,
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
color: color,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.shortcutHint != null)
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.shortcutHint!,
|
||||
overflow: TextOverflow.clip,
|
||||
softWrap: false,
|
||||
maxLines: 1,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant
|
||||
.withValues(alpha: 0.7),
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -1674,10 +1762,156 @@ class _SidebarItemState extends State<_SidebarItem> {
|
|||
),
|
||||
);
|
||||
|
||||
// Explicit button semantics: the collapsed rail is icon-only,
|
||||
// so a screen reader needs the destination name + role from
|
||||
// here, not from a hover-only tooltip.
|
||||
final semantic = Semantics(
|
||||
button: true,
|
||||
selected: widget.selected,
|
||||
label: widget.shortcutHint == null
|
||||
? label
|
||||
: '$label (${widget.shortcutHint})',
|
||||
child: ExcludeSemantics(child: inner),
|
||||
);
|
||||
|
||||
// Tooltip only while the label is hidden — once the label
|
||||
// is visible inline the tooltip becomes redundant noise.
|
||||
if (widget.t > 0.5) return inner;
|
||||
return Tooltip(message: label, preferBelow: false, child: inner);
|
||||
// waitDuration zero: a first-time user hunting through
|
||||
// unlabeled icons should not have to linger per icon.
|
||||
if (widget.t > 0.5) return semantic;
|
||||
return Tooltip(
|
||||
message: widget.shortcutHint == null
|
||||
? label
|
||||
: '$label · ${widget.shortcutHint}',
|
||||
preferBelow: false,
|
||||
waitDuration: Duration.zero,
|
||||
child: semantic,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sidebar utility row (search/palette) — same geometry and
|
||||
/// hover behaviour as a destination item, minus selection state.
|
||||
class _SidebarActionRow extends StatefulWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String? shortcutHint;
|
||||
final double t;
|
||||
final bool labelsInteractive;
|
||||
final double iconColumnWidth;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _SidebarActionRow({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.shortcutHint,
|
||||
required this.t,
|
||||
required this.labelsInteractive,
|
||||
required this.iconColumnWidth,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_SidebarActionRow> createState() => _SidebarActionRowState();
|
||||
}
|
||||
|
||||
class _SidebarActionRowState extends State<_SidebarActionRow> {
|
||||
bool _hovered = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final color = _hovered
|
||||
? theme.colorScheme.onSurface
|
||||
: theme.colorScheme.onSurfaceVariant;
|
||||
final content = Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: widget.iconColumnWidth,
|
||||
child: Center(child: Icon(widget.icon, size: 18, color: color)),
|
||||
),
|
||||
Expanded(
|
||||
child: widget.t > 0
|
||||
? IgnorePointer(
|
||||
ignoring: !widget.labelsInteractive,
|
||||
child: Opacity(
|
||||
opacity: widget.t,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: ChainSpace.md),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.label,
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.shortcutHint != null)
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.shortcutHint!,
|
||||
overflow: TextOverflow.clip,
|
||||
softWrap: false,
|
||||
maxLines: 1,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant
|
||||
.withValues(alpha: 0.7),
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
);
|
||||
final inner = Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: MouseRegion(
|
||||
onEnter: (_) => setState(() => _hovered = true),
|
||||
onExit: (_) => setState(() => _hovered = false),
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: widget.onTap,
|
||||
child: AnimatedContainer(
|
||||
duration: ChainMotion.fast,
|
||||
padding: const EdgeInsets.symmetric(vertical: ChainSpace.md),
|
||||
decoration: BoxDecoration(
|
||||
color: _hovered
|
||||
? theme.colorScheme.surfaceContainerHigh
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(ChainRadius.sm),
|
||||
),
|
||||
child: content,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
final semantic = Semantics(
|
||||
button: true,
|
||||
label: widget.shortcutHint == null
|
||||
? widget.label
|
||||
: '${widget.label} (${widget.shortcutHint})',
|
||||
child: ExcludeSemantics(child: inner),
|
||||
);
|
||||
if (widget.t > 0.5) return semantic;
|
||||
return Tooltip(
|
||||
message: widget.shortcutHint == null
|
||||
? widget.label
|
||||
: '${widget.label} · ${widget.shortcutHint}',
|
||||
preferBelow: false,
|
||||
waitDuration: Duration.zero,
|
||||
child: semantic,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1724,10 +1958,14 @@ class _Footer extends StatelessWidget {
|
|||
opacity: t,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: ChainSpace.sm),
|
||||
// Flexibles + clip: the strip lives inside the
|
||||
// animating rail, so a transient narrow width
|
||||
// must degrade gracefully instead of throwing
|
||||
// a RenderFlex overflow.
|
||||
child: Row(
|
||||
children: [
|
||||
_ThemeToggle(),
|
||||
_LanguageToggle(),
|
||||
Flexible(child: ClipRect(child: _ThemeToggle())),
|
||||
Flexible(child: ClipRect(child: _LanguageToggle())),
|
||||
const Expanded(child: Center(child: _SidebarClock())),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -444,10 +444,13 @@ class _PillarState extends State<_Pillar> {
|
|||
color: theme.colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
l.welcomePillarTechToggle,
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
Flexible(
|
||||
child: Text(
|
||||
l.welcomePillarTechToggle,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import '../data/error_presentation.dart';
|
|||
import '../data/hub.dart';
|
||||
import '../data/hub_auth_token.dart';
|
||||
import '../data/registry_token.dart';
|
||||
import '../data/sidebar_prefs.dart';
|
||||
import '../data/system_actions.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../pages/welcome.dart' show showFaiDoc;
|
||||
|
|
@ -604,6 +605,19 @@ class _FaiSettingsDialogState extends State<ChainSettingsDialog> {
|
|||
theme,
|
||||
),
|
||||
const _ThemePluginPanel(),
|
||||
const SizedBox(height: ChainSpace.xl),
|
||||
// Pinned rail: permanent sidebar labels for operators who
|
||||
// don't want (or can't use) the hover-expand gesture.
|
||||
ValueListenableBuilder<bool>(
|
||||
valueListenable: SidebarPrefs.pinned,
|
||||
builder: (context, pinned, _) => SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(l.settingsSidebarPinnedTitle),
|
||||
subtitle: Text(l.settingsSidebarPinnedBody),
|
||||
value: pinned,
|
||||
onChanged: (v) => SidebarPrefs.setPinned(v),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue