diff --git a/.claude/context/design-system/DESIGN_SYSTEM.md b/.claude/context/design-system/DESIGN_SYSTEM.md index 0f6cd9d..1ab5159 100644 --- a/.claude/context/design-system/DESIGN_SYSTEM.md +++ b/.claude/context/design-system/DESIGN_SYSTEM.md @@ -178,7 +178,9 @@ multi-select) desktop - there can be up to 9: favorite, share, download, delete, copy, move, rename, sync, details); the rest sit behind a trailing "More" button that opens the same grouped-list sheet a file row's own overflow - menu (`ellipsis`/`ellipsis-vertical`) already uses. + menu (`ellipsis`/`ellipsis-vertical`) already uses. Which actions land in + the inline slots vs. "More" is the user's own priority order (Settings + → Action bar, §4's 9-part list), not a fixed per-kind assignment. - Mobile: inline actions are plain 20px accent-text icons, no fill; "More" is the same 40px `NooOverflowButton` treatment (`ellipsis`/ `ellipsis-vertical`, fg-3) file rows use for their own overflow menu. @@ -317,7 +319,12 @@ Sidebar items are 38px tall with radius 12, an 18px icon and a 14/500 label. The 5. Files cache 6. Appearance 7. Tabs - 8. Swipe on a file + 8. Action bar - a reorder-only list (no pin/hide, unlike Tabs): the + priority order for the Selection action bar's bulk actions (favorite, + share, download, delete, copy, move, rename, sync, details) - the + first few (3 mobile / 4 desktop) land in the bar's fixed inline + slots, the rest sit behind "More". See §2 "Selection action bar". + 9. Swipe on a file Mobile uses one column of grouped lists. Desktop uses a 2-column grid of cards with a 1px line and radius 20. - **Share sheet / dialog:** sections in this order: diff --git a/lib/models/selection_action.dart b/lib/models/selection_action.dart index 4ca4411..1df0a9a 100644 --- a/lib/models/selection_action.dart +++ b/lib/models/selection_action.dart @@ -1,16 +1,91 @@ import 'package:flutter/material.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; + +/// A bulk action's identity, stable across the dynamic icon/label a +/// [SelectionAction] actually renders (e.g. `favorite` covers both +/// "Favorite" and "Remove from favorites"). This is what the user's +/// configured [order] - Settings' "Action bar" reorder list - and +/// [NooSelectionBar]'s inline/overflow split key off, since the live +/// [SelectionAction] list is rebuilt fresh on every selection change and +/// can't be compared by icon/label alone. +/// +/// Not every screen offers every kind (e.g. Favorites has no `rename` or +/// `sync`) - [orderSelectionActions] just skips whatever kind a screen +/// didn't build. +enum SelectionActionKind { + favorite, + share, + download, + delete, + copy, + move, + rename, + sync, + details; + + /// Generic label for the Settings reorder row - the live bar uses each + /// screen's own contextual label (e.g. "Remove from favorites") instead. + String get label => switch (this) { + SelectionActionKind.favorite => 'Favorite', + SelectionActionKind.share => 'Share', + SelectionActionKind.download => 'Download', + SelectionActionKind.delete => 'Delete', + SelectionActionKind.copy => 'Copy', + SelectionActionKind.move => 'Move', + SelectionActionKind.rename => 'Rename', + SelectionActionKind.sync => 'Sync to device', + SelectionActionKind.details => 'Details', + }; + + IconData get icon => switch (this) { + SelectionActionKind.favorite => LucideIcons.star, + SelectionActionKind.share => LucideIcons.share2, + SelectionActionKind.download => LucideIcons.download, + SelectionActionKind.delete => LucideIcons.trash2, + SelectionActionKind.copy => LucideIcons.copy, + SelectionActionKind.move => LucideIcons.folderInput, + SelectionActionKind.rename => LucideIcons.filePen, + SelectionActionKind.sync => LucideIcons.hardDriveDownload, + SelectionActionKind.details => LucideIcons.info, + }; +} /// One bulk action available for the current multi-selection (e.g. favorite, /// download, delete), shown as an icon button in the sticky selection /// toolbar each tab (Files, Photos, ...) renders inline in its own content. class SelectionAction { + final SelectionActionKind kind; final IconData icon; final String label; final VoidCallback onTap; const SelectionAction({ + required this.kind, required this.icon, required this.label, required this.onTap, }); } + +/// Reorders [actions] to match the user's configured [order] (Settings' +/// "Action bar" section, [SelectionActionKind] values) - this is what makes +/// that setting take effect, since it's [order] that decides which actions +/// land in `NooSelectionBar`'s inline slots vs. behind "More". A kind in +/// [order] the caller didn't build (e.g. `rename` on a multi-item +/// selection) is just skipped; a kind [actions] has that [order] doesn't +/// mention (e.g. a kind added in a later app update, before the saved +/// order is migrated - see [SettingsController.selectionActionOrder]) is +/// appended at the end in its original position. +List orderSelectionActions( + List actions, + List order, +) { + final byKind = {for (final a in actions) a.kind: a}; + final ordered = []; + for (final kind in order) { + final action = byKind.remove(kind); + if (action != null) ordered.add(action); + } + ordered.addAll(byKind.values); + return ordered; +} diff --git a/lib/providers/settings_controller.dart b/lib/providers/settings_controller.dart index f330885..8517fa1 100644 --- a/lib/providers/settings_controller.dart +++ b/lib/providers/settings_controller.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../models/app_tab.dart'; +import '../models/selection_action.dart'; import '../theme/app_theme.dart'; /// What swiping a Files list-view item left/right does, user-configurable @@ -24,6 +25,7 @@ class SettingsController extends ChangeNotifier { static const _prefUseDynamicColor = 'ui_use_dynamic_color'; static const _prefSeedColor = 'ui_seed_color'; static const _prefTabOrder = 'ui_tab_order'; + static const _prefSelectionActionOrder = 'ui_selection_action_order'; static const _prefHiddenTabs = 'ui_hidden_tabs'; static const _prefDefaultTab = 'ui_default_tab'; static const _prefSwipeLeftAction = 'ui_swipe_left_action'; @@ -47,6 +49,9 @@ class SettingsController extends ChangeNotifier { Set _hiddenTabs = {}; AppTab _defaultTab = AppTab.files; + List _selectionActionOrder = SelectionActionKind.values + .toList(); + SwipeAction _swipeLeftAction = SwipeAction.delete; SwipeAction _swipeRightAction = SwipeAction.favorite; @@ -86,6 +91,12 @@ class SettingsController extends ChangeNotifier { Set get hiddenTabs => _hiddenTabs; AppTab get defaultTab => _defaultTab; + /// The priority order bulk actions (favorite, share, download, ...) show + /// in on the multi-select action bar - see [orderSelectionActions]. Every + /// [SelectionActionKind] is always present here (nothing is hidden, only + /// reordered), so [NooSelectionBar]'s fixed inline slots are always full. + List get selectionActionOrder => _selectionActionOrder; + /// The tabs the bottom nav bar should actually show, in order. List get visibleTabs => _tabOrder.where((t) => !_hiddenTabs.contains(t)).toList(); @@ -134,6 +145,25 @@ class SettingsController extends ChangeNotifier { _tabOrder = order; } + final savedActionOrderNames = prefs.getStringList( + _prefSelectionActionOrder, + ); + if (savedActionOrderNames != null) { + final order = []; + for (final name in savedActionOrderNames) { + final match = SelectionActionKind.values + .where((k) => k.name == name) + .firstOrNull; + if (match != null) order.add(match); + } + // Forward-compat: a kind added in a later app update won't be in an + // older saved order yet, so append anything missing. + for (final kind in SelectionActionKind.values) { + if (!order.contains(kind)) order.add(kind); + } + _selectionActionOrder = order; + } + final savedHiddenNames = prefs.getStringList(_prefHiddenTabs); if (savedHiddenNames != null) { _hiddenTabs = savedHiddenNames @@ -234,6 +264,17 @@ class SettingsController extends ChangeNotifier { ); } + void setSelectionActionOrder(List order) { + _selectionActionOrder = order; + notifyListeners(); + _prefsFuture.then( + (p) => p.setStringList( + _prefSelectionActionOrder, + order.map((k) => k.name).toList(), + ), + ); + } + /// Shows or hides a tab in the bottom nav bar. Refuses to hide the last /// remaining visible tab. If the tab being hidden is the current default, /// the default falls back to the next visible tab. diff --git a/lib/views/account_view.dart b/lib/views/account_view.dart index a294ac7..5861d5e 100644 --- a/lib/views/account_view.dart +++ b/lib/views/account_view.dart @@ -5,6 +5,7 @@ import '../widgets/noo/nav/noo_toolbar.dart'; import '../widgets/noo/noo_layout.dart'; import '../widgets/settings/settings_account_card.dart'; import '../widgets/settings/settings_accounts.dart'; +import '../widgets/settings/settings_action_bar.dart'; import '../widgets/settings/settings_appearance.dart'; import '../widgets/settings/settings_file_sync.dart'; import '../widgets/settings/settings_files_cache.dart'; @@ -12,9 +13,11 @@ import '../widgets/settings/settings_security.dart'; import '../widgets/settings/settings_swipe.dart'; import '../widgets/settings/settings_tabs.dart'; -/// Settings, pushed on top of the shell (DESIGN_SYSTEM.md 4's 8-section +/// Settings, pushed on top of the shell (DESIGN_SYSTEM.md 4's 9-section /// order: account card, accounts, security, file sync, files cache, -/// appearance, tabs, swipe on a file). One column of [SettingsSection]s on +/// appearance, tabs, action bar, swipe on a file). One column of +/// [SettingsSection]s (and [SettingsActionBarSection], which has no option +/// rows of its own to put in one - just the reorder list) on /// mobile; a 2-column grid of cards on desktop - see each /// `widgets/settings/*.dart` file for a section's own content and any /// setting that had to be slotted in or grouped under "Advanced appearance". @@ -29,6 +32,7 @@ class AccountView extends StatelessWidget { SettingsFilesCacheSection(), SettingsAppearanceSection(), SettingsTabsSection(), + SettingsActionBarSection(), SettingsSwipeSection(), ]; @@ -48,7 +52,9 @@ class AccountView extends StatelessWidget { ), body: SafeArea( top: false, - child: desktop ? _DesktopGrid(sections: _sections) : _MobileList(sections: _sections), + child: desktop + ? _DesktopGrid(sections: _sections) + : _MobileList(sections: _sections), ), ); } @@ -61,10 +67,18 @@ class _MobileList extends StatelessWidget { @override Widget build(BuildContext context) { return ListView( - padding: const EdgeInsets.fromLTRB(NooSpace.sm, NooSpace.sm, NooSpace.sm, NooSpace.xxl), + padding: const EdgeInsets.fromLTRB( + NooSpace.sm, + NooSpace.sm, + NooSpace.sm, + NooSpace.xxl, + ), physics: const BouncingScrollPhysics(), children: [ - for (final section in sections) ...[section, const SizedBox(height: NooSpace.xl)], + for (final section in sections) ...[ + section, + const SizedBox(height: NooSpace.xl), + ], ], ); } @@ -112,7 +126,10 @@ class _Column extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - for (final child in children) ...[child, const SizedBox(height: NooSpace.xl)], + for (final child in children) ...[ + child, + const SizedBox(height: NooSpace.xl), + ], ], ); } diff --git a/lib/views/favorites_view.dart b/lib/views/favorites_view.dart index 56404a5..e27eab2 100644 --- a/lib/views/favorites_view.dart +++ b/lib/views/favorites_view.dart @@ -96,13 +96,15 @@ class _FavoritesViewState extends State { /// The bulk actions shown in the sticky selection toolbar for the /// currently-selected items - same set Files/Photos offer. List _buildSelectionActions(List selected) { - return [ + final actions = [ SelectionAction( + kind: SelectionActionKind.favorite, icon: LucideIcons.starOff, label: 'Remove from favorites', onTap: () => _unfavoriteSelected(selected), ), SelectionAction( + kind: SelectionActionKind.share, icon: LucideIcons.share2, label: 'Share', onTap: () => selected.length == 1 @@ -110,32 +112,41 @@ class _FavoritesViewState extends State { : _shareSelected(context, selected), ), SelectionAction( + kind: SelectionActionKind.download, icon: LucideIcons.download, label: 'Download', onTap: () => _downloadSelected(context, selected), ), SelectionAction( + kind: SelectionActionKind.delete, icon: LucideIcons.trash2, label: 'Delete', onTap: () => _confirmDeleteSelected(context, selected), ), SelectionAction( + kind: SelectionActionKind.copy, icon: LucideIcons.copy, label: 'Copy', onTap: () => _moveOrCopySelected(selected, copy: true), ), SelectionAction( + kind: SelectionActionKind.move, icon: LucideIcons.folderInput, label: 'Move', onTap: () => _moveOrCopySelected(selected, copy: false), ), if (selected.length == 1) SelectionAction( + kind: SelectionActionKind.details, icon: LucideIcons.info, label: 'Details', onTap: () => DetailsSheet.show(context, selected.single), ), ]; + return orderSelectionActions( + actions, + context.read().selectionActionOrder, + ); } Future _unfavoriteSelected(List items) async { diff --git a/lib/views/files_view.dart b/lib/views/files_view.dart index 568032f..a8cf266 100644 --- a/lib/views/files_view.dart +++ b/lib/views/files_view.dart @@ -419,6 +419,7 @@ class _FilesViewState extends State if (pick.isPicking) { return [ SelectionAction( + kind: SelectionActionKind.favorite, icon: LucideIcons.check, label: 'Use ${selected.length} item(s)', onTap: () => pick.confirmPick(selected), @@ -427,13 +428,15 @@ class _FilesViewState extends State } final allFavorited = selected.every((i) => i.isFavorite); final allSynced = selected.every((i) => sync.isPathSynced(i.path)); - return [ + final actions = [ SelectionAction( + kind: SelectionActionKind.favorite, icon: allFavorited ? LucideIcons.starOff : LucideIcons.star, label: allFavorited ? 'Remove from favorites' : 'Favorite', onTap: () => _favoriteSelected(context, selected), ), SelectionAction( + kind: SelectionActionKind.share, icon: LucideIcons.share2, label: 'Share', onTap: () => selected.length == 1 @@ -441,43 +444,54 @@ class _FilesViewState extends State : _shareSelected(context, selected), ), SelectionAction( + kind: SelectionActionKind.download, icon: LucideIcons.download, label: 'Download', onTap: () => _downloadSelected(context, selected), ), SelectionAction( + kind: SelectionActionKind.delete, icon: LucideIcons.trash2, label: 'Delete', onTap: () => _confirmDeleteSelected(context, selected), ), SelectionAction( + kind: SelectionActionKind.copy, icon: LucideIcons.copy, label: 'Copy', onTap: () => _moveOrCopySelected(context, selected, copy: true), ), SelectionAction( + kind: SelectionActionKind.move, icon: LucideIcons.folderInput, label: 'Move', onTap: () => _moveOrCopySelected(context, selected, copy: false), ), if (selected.length == 1) SelectionAction( + kind: SelectionActionKind.rename, icon: LucideIcons.filePen, label: 'Rename', onTap: () => _renameItem(selected.single), ), SelectionAction( + kind: SelectionActionKind.sync, icon: LucideIcons.hardDriveDownload, label: allSynced ? 'Stop syncing to device' : 'Sync to device', onTap: () => _toggleSyncSelected(context, sync, selected), ), if (selected.length == 1) SelectionAction( + kind: SelectionActionKind.details, icon: LucideIcons.info, label: 'Details', onTap: () => DetailsSheet.show(context, selected.single), ), ]; + return orderSelectionActions( + actions, + context.read().selectionActionOrder, + ); } Future _renameItem(NextcloudItem item) async { diff --git a/lib/views/photos_view.dart b/lib/views/photos_view.dart index cf5e9b3..40dcc67 100644 --- a/lib/views/photos_view.dart +++ b/lib/views/photos_view.dart @@ -11,6 +11,7 @@ import '../providers/item_operations.dart'; import '../providers/photos_controller.dart'; import '../providers/pick_controller.dart'; import '../providers/session_controller.dart'; +import '../providers/settings_controller.dart'; import '../services/download_service.dart'; import '../theme/design_tokens.dart'; import '../widgets/details/details_sheet.dart'; @@ -113,6 +114,7 @@ class _PhotosViewState extends State { if (pick.isPicking) { return [ SelectionAction( + kind: SelectionActionKind.favorite, icon: LucideIcons.check, label: 'Use ${selected.length} item(s)', onTap: () => pick.confirmPick(selected), @@ -123,13 +125,15 @@ class _PhotosViewState extends State { // uses for favorite/unfavorite (see `files_view.dart`) rather than a // filled heart. final allFavorited = selected.every((i) => i.isFavorite); - return [ + final actions = [ SelectionAction( + kind: SelectionActionKind.favorite, icon: allFavorited ? LucideIcons.starOff : LucideIcons.star, label: allFavorited ? 'Remove from favorites' : 'Favorite', onTap: () => _favoriteSelected(selected), ), SelectionAction( + kind: SelectionActionKind.share, icon: LucideIcons.share2, label: 'Share', onTap: () => selected.length == 1 @@ -137,32 +141,41 @@ class _PhotosViewState extends State { : _shareSelected(context, selected), ), SelectionAction( + kind: SelectionActionKind.download, icon: LucideIcons.download, label: 'Download', onTap: () => _downloadSelected(context, selected), ), SelectionAction( + kind: SelectionActionKind.delete, icon: LucideIcons.trash2, label: 'Delete', onTap: () => _confirmDeleteSelected(context, selected), ), SelectionAction( + kind: SelectionActionKind.copy, icon: LucideIcons.copy, label: 'Copy', onTap: () => _moveOrCopySelected(selected, copy: true), ), SelectionAction( + kind: SelectionActionKind.move, icon: LucideIcons.folderInput, label: 'Move', onTap: () => _moveOrCopySelected(selected, copy: false), ), if (selected.length == 1) SelectionAction( + kind: SelectionActionKind.details, icon: LucideIcons.info, label: 'Details', onTap: () => DetailsSheet.show(context, selected.single), ), ]; + return orderSelectionActions( + actions, + context.read().selectionActionOrder, + ); } /// Splits an already-filtered/sorted list into contiguous month runs diff --git a/lib/widgets/noo/lists/noo_tab_order_row.dart b/lib/widgets/noo/lists/noo_tab_order_row.dart index d1f276e..9a74740 100644 --- a/lib/widgets/noo/lists/noo_tab_order_row.dart +++ b/lib/widgets/noo/lists/noo_tab_order_row.dart @@ -11,6 +11,11 @@ class NooTabOrderRow extends StatelessWidget { final bool pinned; final VoidCallback? onTogglePin; + /// False hides the trailing pin toggle entirely - for reorder-only lists + /// (Settings' Action bar section) that have no "pinned" concept, just an + /// order. + final bool showPin; + /// This row's index inside its `ReorderableListView` - when set, only the /// grip icon starts a drag (`ReorderableDragStartListener` wraps just the /// icon, not the whole row), so a vertical drag/scroll starting anywhere @@ -25,6 +30,7 @@ class NooTabOrderRow extends StatelessWidget { required this.label, this.pinned = false, this.onTogglePin, + this.showPin = true, this.dragIndex, }); @@ -50,23 +56,24 @@ class NooTabOrderRow extends StatelessWidget { style: NooText.bodyL.copyWith(height: 1, color: colors.fg1), ), ), - GestureDetector( - onTap: onTogglePin, - child: Container( - width: 36, - height: 36, - alignment: Alignment.center, - decoration: BoxDecoration( - color: pinned ? colors.accentSoft : colors.surface2, - shape: BoxShape.circle, - ), - child: Icon( - pinned ? LucideIcons.pin : LucideIcons.pinOff, - size: 16, - color: pinned ? colors.accentText : colors.fg3, + if (showPin) + GestureDetector( + onTap: onTogglePin, + child: Container( + width: 36, + height: 36, + alignment: Alignment.center, + decoration: BoxDecoration( + color: pinned ? colors.accentSoft : colors.surface2, + shape: BoxShape.circle, + ), + child: Icon( + pinned ? LucideIcons.pin : LucideIcons.pinOff, + size: 16, + color: pinned ? colors.accentText : colors.fg3, + ), ), ), - ), ], ), ); diff --git a/lib/widgets/settings/settings_action_bar.dart b/lib/widgets/settings/settings_action_bar.dart new file mode 100644 index 0000000..46e9225 --- /dev/null +++ b/lib/widgets/settings/settings_action_bar.dart @@ -0,0 +1,132 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../../models/selection_action.dart'; +import '../../providers/settings_controller.dart'; +import '../../theme/design_tokens.dart'; +import '../noo/lists/noo_tab_order_row.dart'; +import '../noo/noo_layout.dart'; + +/// Settings section: the priority order bulk actions (favorite, share, +/// download, ...) show in on the Files/Photos/Favorites multi-select +/// action bar. Every action stays available everywhere - this only decides +/// which ones land in `NooSelectionBar`'s fixed inline slots (3 mobile / 4 +/// desktop, see its own doc comment) vs. behind its trailing "More" - +/// there's no pin/hide concept here the way Tabs has, so this is just a +/// reorder list, not a whole section of its own option rows. +class SettingsActionBarSection extends StatelessWidget { + const SettingsActionBarSection({super.key}); + + static const _subtitle = + 'Drag to reorder - the first ones shown appear directly in the bar, the rest sit behind "More"'; + + void _handleReorder(SettingsController settings, int oldIndex, int newIndex) { + final order = List.from(settings.selectionActionOrder); + final kind = order.removeAt(oldIndex); + order.insert(newIndex, kind); + settings.setSelectionActionOrder(order); + } + + @override + Widget build(BuildContext context) { + final settings = context.watch(); + final desktop = NooLayout.isDesktop(context); + + final orderList = _ActionOrderList( + order: settings.selectionActionOrder, + onReorder: (oldIndex, newIndex) => + _handleReorder(settings, oldIndex, newIndex), + ); + + final colors = context.nooColors; + + if (!desktop) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Text( + 'Action bar', + style: NooText.label.copyWith(color: colors.fg2), + ), + ), + const SizedBox(height: 4), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Text( + _subtitle, + style: NooText.meta.copyWith(color: colors.fg3), + ), + ), + const SizedBox(height: 8), + orderList, + ], + ); + } + + return Container( + decoration: BoxDecoration( + color: colors.surface, + border: Border.all(color: colors.line), + borderRadius: BorderRadius.circular(NooRadii.card), + ), + padding: const EdgeInsets.all(NooSpace.lg), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Action bar', + style: NooText.cardTitle.copyWith(color: colors.fg1), + ), + const SizedBox(height: 4), + Text(_subtitle, style: NooText.meta.copyWith(color: colors.fg3)), + const SizedBox(height: NooSpace.md), + orderList, + ], + ), + ); + } +} + +/// The reorderable [NooTabOrderRow] stack for [SelectionActionKind]s - same +/// radius-20, line-divided card `settings_tabs.dart`'s `_TabOrderList` +/// draws, minus the pin toggle (`showPin: false`): there's nothing to +/// show/hide here, only an order. +class _ActionOrderList extends StatelessWidget { + final List order; + final void Function(int oldIndex, int newIndex) onReorder; + + const _ActionOrderList({required this.order, required this.onReorder}); + + @override + Widget build(BuildContext context) { + final colors = context.nooColors; + final list = ReorderableListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + buildDefaultDragHandles: false, + itemCount: order.length, + onReorderItem: onReorder, + itemBuilder: (context, index) { + final kind = order[index]; + return Padding( + key: ValueKey(kind), + padding: EdgeInsets.only(top: index > 0 ? 1 : 0), + child: NooTabOrderRow( + icon: kind.icon, + label: kind.label, + showPin: false, + dragIndex: index, + ), + ); + }, + ); + return ClipRRect( + borderRadius: BorderRadius.circular(NooRadii.card), + child: DecoratedBox( + decoration: BoxDecoration(color: colors.line), + child: list, + ), + ); + } +} diff --git a/test/widgets/noo/lists_test.dart b/test/widgets/noo/lists_test.dart index 233b93f..1966eca 100644 --- a/test/widgets/noo/lists_test.dart +++ b/test/widgets/noo/lists_test.dart @@ -14,12 +14,15 @@ import 'package:noo/widgets/noo/lists/noo_tab_order_row.dart'; import 'noo_test_utils.dart'; /// [n] actions labelled A, B, C, ... each incrementing an entry in [taps] -/// keyed by its own label when tapped. +/// keyed by its own label when tapped. [n] must be at most +/// `SelectionActionKind.values.length` - each generated action needs its +/// own distinct kind. List _actions(int n, Map taps) => List.generate(n, (i) { final label = String.fromCharCode(65 + i); taps[label] = 0; return SelectionAction( + kind: SelectionActionKind.values[i], icon: LucideIcons.star, label: label, onTap: () => taps[label] = taps[label]! + 1,