Skip to content

Commit 898be62

Browse files
Eoicclaude
andcommitted
refactor: Improve shelf and topic assignment bottom sheets
Add search field to filter shelves/topics by name, move "create new" action to a compact icon button in the header, and replace full-width action buttons with right-aligned TextButton/FilledButton pair separated by a divider. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bd70a13 commit 898be62

2 files changed

Lines changed: 120 additions & 151 deletions

File tree

client/lib/widgets/shelves/move_to_shelf_sheet.dart

Lines changed: 59 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:papyrus/data/data_store.dart';
33
import 'package:papyrus/models/book.dart';
44
import 'package:papyrus/models/shelf.dart';
55
import 'package:papyrus/themes/design_tokens.dart';
6+
import 'package:papyrus/widgets/input/search_field.dart';
67
import 'package:papyrus/widgets/shared/bottom_sheet_handle.dart';
78
import 'package:papyrus/widgets/shelves/add_shelf_sheet.dart';
89
import 'package:provider/provider.dart';
@@ -39,6 +40,8 @@ class MoveToShelfSheet extends StatefulWidget {
3940

4041
class _MoveToShelfSheetState extends State<MoveToShelfSheet> {
4142
late Set<String> _selectedShelfIds;
43+
final _searchController = TextEditingController();
44+
String _searchQuery = '';
4245

4346
@override
4447
void initState() {
@@ -48,6 +51,12 @@ class _MoveToShelfSheetState extends State<MoveToShelfSheet> {
4851
_selectedShelfIds = dataStore.getShelfIdsForBook(widget.book.id).toSet();
4952
}
5053

54+
@override
55+
void dispose() {
56+
_searchController.dispose();
57+
super.dispose();
58+
}
59+
5160
@override
5261
Widget build(BuildContext context) {
5362
final colorScheme = Theme.of(context).colorScheme;
@@ -105,46 +114,74 @@ class _MoveToShelfSheetState extends State<MoveToShelfSheet> {
105114
],
106115
),
107116
),
117+
// Create new shelf button
118+
IconButton.filledTonal(
119+
onPressed: _showCreateShelfSheet,
120+
icon: const Icon(Icons.add),
121+
tooltip: 'Create new shelf',
122+
),
108123
],
109124
),
110125
const SizedBox(height: Spacing.md),
111126

127+
// Search field
128+
SearchField(
129+
controller: _searchController,
130+
hintText: 'Search shelves...',
131+
onChanged: (value) => setState(() => _searchQuery = value),
132+
),
133+
const SizedBox(height: Spacing.md),
134+
112135
// Shelf list
113136
Expanded(
114-
child: ListView(
115-
controller: scrollController,
116-
children: [
117-
// Shelves list
118-
...shelves.map((shelf) => _buildShelfTile(context, shelf)),
119-
if (shelves.isNotEmpty) const SizedBox(height: Spacing.md),
137+
child: Builder(
138+
builder: (context) {
139+
final filteredShelves = _searchQuery.isEmpty
140+
? shelves
141+
: shelves
142+
.where(
143+
(s) => s.name.toLowerCase().contains(
144+
_searchQuery.toLowerCase(),
145+
),
146+
)
147+
.toList();
120148

121-
// Create new shelf button
122-
_buildCreateShelfButton(context),
123-
const SizedBox(height: Spacing.lg),
124-
],
149+
if (filteredShelves.isEmpty && _searchQuery.isNotEmpty) {
150+
return Center(
151+
child: Text(
152+
'No shelves found',
153+
style: textTheme.bodyMedium?.copyWith(
154+
color: colorScheme.onSurfaceVariant,
155+
),
156+
),
157+
);
158+
}
159+
160+
return ListView.builder(
161+
controller: scrollController,
162+
itemCount: filteredShelves.length,
163+
itemBuilder: (context, index) =>
164+
_buildShelfTile(context, filteredShelves[index]),
165+
);
166+
},
125167
),
126168
),
169+
Divider(height: Spacing.md, color: colorScheme.outlineVariant),
127170

128171
// Action buttons
129172
Padding(
130173
padding: EdgeInsets.only(
131-
bottom: MediaQuery.of(context).viewInsets.bottom + Spacing.lg,
174+
bottom: MediaQuery.of(context).viewInsets.bottom + Spacing.sm,
132175
),
133176
child: Row(
177+
mainAxisAlignment: MainAxisAlignment.end,
134178
children: [
135-
Expanded(
136-
child: OutlinedButton(
137-
onPressed: () => Navigator.pop(context),
138-
child: const Text('Cancel'),
139-
),
179+
TextButton(
180+
onPressed: () => Navigator.pop(context),
181+
child: const Text('Cancel'),
140182
),
141183
const SizedBox(width: Spacing.md),
142-
Expanded(
143-
child: FilledButton(
144-
onPressed: _onSave,
145-
child: const Text('Save'),
146-
),
147-
),
184+
FilledButton(onPressed: _onSave, child: const Text('Save')),
148185
],
149186
),
150187
),
@@ -257,59 +294,6 @@ class _MoveToShelfSheetState extends State<MoveToShelfSheet> {
257294
);
258295
}
259296

260-
Widget _buildCreateShelfButton(BuildContext context) {
261-
final colorScheme = Theme.of(context).colorScheme;
262-
final textTheme = Theme.of(context).textTheme;
263-
264-
return Card(
265-
margin: EdgeInsets.zero,
266-
elevation: 0,
267-
color: colorScheme.surfaceContainerLow,
268-
shape: RoundedRectangleBorder(
269-
borderRadius: BorderRadius.circular(AppRadius.md),
270-
side: BorderSide(
271-
color: colorScheme.outlineVariant,
272-
style: BorderStyle.solid,
273-
),
274-
),
275-
child: InkWell(
276-
onTap: _showCreateShelfSheet,
277-
borderRadius: BorderRadius.circular(AppRadius.md),
278-
child: Padding(
279-
padding: const EdgeInsets.symmetric(
280-
horizontal: Spacing.md,
281-
vertical: Spacing.md,
282-
),
283-
child: Row(
284-
children: [
285-
Container(
286-
width: 40,
287-
height: 40,
288-
decoration: BoxDecoration(
289-
color: colorScheme.primaryContainer,
290-
borderRadius: BorderRadius.circular(AppRadius.sm),
291-
),
292-
child: Icon(
293-
Icons.add,
294-
color: colorScheme.onPrimaryContainer,
295-
size: 20,
296-
),
297-
),
298-
const SizedBox(width: Spacing.md),
299-
Text(
300-
'Create new shelf',
301-
style: textTheme.titleSmall?.copyWith(
302-
fontWeight: FontWeight.w600,
303-
color: colorScheme.primary,
304-
),
305-
),
306-
],
307-
),
308-
),
309-
),
310-
);
311-
}
312-
313297
void _toggleShelf(String shelfId) {
314298
setState(() {
315299
if (_selectedShelfIds.contains(shelfId)) {

client/lib/widgets/topics/manage_topics_sheet.dart

Lines changed: 61 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:papyrus/data/data_store.dart';
33
import 'package:papyrus/models/book.dart';
44
import 'package:papyrus/models/tag.dart';
55
import 'package:papyrus/themes/design_tokens.dart';
6+
import 'package:papyrus/widgets/input/search_field.dart';
67
import 'package:papyrus/widgets/shared/bottom_sheet_handle.dart';
78
import 'package:papyrus/widgets/topics/add_topic_sheet.dart';
89
import 'package:provider/provider.dart';
@@ -39,6 +40,8 @@ class ManageTopicsSheet extends StatefulWidget {
3940

4041
class _ManageTopicsSheetState extends State<ManageTopicsSheet> {
4142
late Set<String> _selectedTagIds;
43+
final _searchController = TextEditingController();
44+
String _searchQuery = '';
4245

4346
@override
4447
void initState() {
@@ -47,6 +50,12 @@ class _ManageTopicsSheetState extends State<ManageTopicsSheet> {
4750
_selectedTagIds = dataStore.getTagIdsForBook(widget.book.id).toSet();
4851
}
4952

53+
@override
54+
void dispose() {
55+
_searchController.dispose();
56+
super.dispose();
57+
}
58+
5059
@override
5160
Widget build(BuildContext context) {
5261
final colorScheme = Theme.of(context).colorScheme;
@@ -104,45 +113,76 @@ class _ManageTopicsSheetState extends State<ManageTopicsSheet> {
104113
],
105114
),
106115
),
116+
// Create new topic button
117+
IconButton.filledTonal(
118+
onPressed: _showCreateTopicSheet,
119+
icon: const Icon(Icons.add),
120+
tooltip: 'Create new topic',
121+
),
107122
],
108123
),
109124
const SizedBox(height: Spacing.md),
110125

126+
// Search field
127+
SearchField(
128+
controller: _searchController,
129+
hintText: 'Search topics...',
130+
onChanged: (value) => setState(() => _searchQuery = value),
131+
),
132+
const SizedBox(height: Spacing.md),
133+
111134
// Topic list
112135
Expanded(
113136
child: tags.isEmpty
114137
? _buildEmptyState(context)
115-
: ListView(
116-
controller: scrollController,
117-
children: [
118-
...tags.map((tag) => _buildTagTile(context, tag)),
119-
const SizedBox(height: Spacing.md),
120-
_buildCreateTopicButton(context),
121-
const SizedBox(height: Spacing.lg),
122-
],
138+
: Builder(
139+
builder: (context) {
140+
final filteredTags = _searchQuery.isEmpty
141+
? tags
142+
: tags
143+
.where(
144+
(t) => t.name.toLowerCase().contains(
145+
_searchQuery.toLowerCase(),
146+
),
147+
)
148+
.toList();
149+
150+
if (filteredTags.isEmpty && _searchQuery.isNotEmpty) {
151+
return Center(
152+
child: Text(
153+
'No topics found',
154+
style: textTheme.bodyMedium?.copyWith(
155+
color: colorScheme.onSurfaceVariant,
156+
),
157+
),
158+
);
159+
}
160+
161+
return ListView.builder(
162+
controller: scrollController,
163+
itemCount: filteredTags.length,
164+
itemBuilder: (context, index) =>
165+
_buildTagTile(context, filteredTags[index]),
166+
);
167+
},
123168
),
124169
),
170+
Divider(height: Spacing.md, color: colorScheme.outlineVariant),
125171

126172
// Action buttons
127173
Padding(
128174
padding: EdgeInsets.only(
129-
bottom: MediaQuery.of(context).viewInsets.bottom + Spacing.lg,
175+
bottom: MediaQuery.of(context).viewInsets.bottom + Spacing.sm,
130176
),
131177
child: Row(
178+
mainAxisAlignment: MainAxisAlignment.end,
132179
children: [
133-
Expanded(
134-
child: OutlinedButton(
135-
onPressed: () => Navigator.pop(context),
136-
child: const Text('Cancel'),
137-
),
180+
TextButton(
181+
onPressed: () => Navigator.pop(context),
182+
child: const Text('Cancel'),
138183
),
139184
const SizedBox(width: Spacing.md),
140-
Expanded(
141-
child: FilledButton(
142-
onPressed: _onSave,
143-
child: const Text('Save'),
144-
),
145-
),
185+
FilledButton(onPressed: _onSave, child: const Text('Save')),
146186
],
147187
),
148188
),
@@ -201,13 +241,11 @@ class _ManageTopicsSheetState extends State<ManageTopicsSheet> {
201241
),
202242
const SizedBox(height: Spacing.sm),
203243
Text(
204-
'Create a topic to get started',
244+
'Tap + to create a topic',
205245
style: textTheme.bodyMedium?.copyWith(
206246
color: colorScheme.onSurfaceVariant,
207247
),
208248
),
209-
const SizedBox(height: Spacing.lg),
210-
_buildCreateTopicButton(context),
211249
],
212250
);
213251
}
@@ -298,59 +336,6 @@ class _ManageTopicsSheetState extends State<ManageTopicsSheet> {
298336
);
299337
}
300338

301-
Widget _buildCreateTopicButton(BuildContext context) {
302-
final colorScheme = Theme.of(context).colorScheme;
303-
final textTheme = Theme.of(context).textTheme;
304-
305-
return Card(
306-
margin: EdgeInsets.zero,
307-
elevation: 0,
308-
color: colorScheme.surfaceContainerLow,
309-
shape: RoundedRectangleBorder(
310-
borderRadius: BorderRadius.circular(AppRadius.md),
311-
side: BorderSide(
312-
color: colorScheme.outlineVariant,
313-
style: BorderStyle.solid,
314-
),
315-
),
316-
child: InkWell(
317-
onTap: _showCreateTopicSheet,
318-
borderRadius: BorderRadius.circular(AppRadius.md),
319-
child: Padding(
320-
padding: const EdgeInsets.symmetric(
321-
horizontal: Spacing.md,
322-
vertical: Spacing.md,
323-
),
324-
child: Row(
325-
children: [
326-
Container(
327-
width: 40,
328-
height: 40,
329-
decoration: BoxDecoration(
330-
color: colorScheme.primaryContainer,
331-
borderRadius: BorderRadius.circular(AppRadius.sm),
332-
),
333-
child: Icon(
334-
Icons.add,
335-
color: colorScheme.onPrimaryContainer,
336-
size: 20,
337-
),
338-
),
339-
const SizedBox(width: Spacing.md),
340-
Text(
341-
'Create new topic',
342-
style: textTheme.titleSmall?.copyWith(
343-
fontWeight: FontWeight.w600,
344-
color: colorScheme.primary,
345-
),
346-
),
347-
],
348-
),
349-
),
350-
),
351-
);
352-
}
353-
354339
void _toggleTag(String tagId) {
355340
setState(() {
356341
if (_selectedTagIds.contains(tagId)) {

0 commit comments

Comments
 (0)