Improve category picker UX: grouped searchable icons + color picker#3
Improve category picker UX: grouped searchable icons + color picker#3
Conversation
There was a problem hiding this comment.
Pull request overview
This PR enhances the category creation and editing UX by replacing simple dropdown icon selection with a grouped, searchable autocomplete system and adding a color picker with curated swatches. The changes improve the mobile experience by disabling keyboard input in the icon field and include a live preview showing the icon, color, and name together.
Changes:
- Expanded category icon catalog with 50+ icons organized into 9 semantic groups (bills/finance, transport, food, subscriptions, health, work, entertainment, family/pets, travel)
- Replaced dropdown icon selector with searchable autocomplete supporting keyword matching and grouping
- Added curated color picker with 16 color swatches for category customization
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/pwa/src/features/categories/types.ts | Added color field to CategoryEditDraft interface |
| apps/pwa/src/features/categories/utils.ts | Updated buildDraftFromCategory to include color field |
| apps/pwa/src/features/categories/constants.ts | Expanded icon catalog to 50+ icons with CategoryIconOption type including label, group, and keywords; added 16-color palette |
| apps/pwa/src/features/categories/dialogs/AddCategoryDialog.tsx | Replaced dropdown with Autocomplete for icons, added color picker with swatches, added live category preview |
| apps/pwa/src/features/categories/dialogs/EditCategoryDialog.tsx | Replaced dropdown with Autocomplete for icons, added color picker with swatches, added live category preview |
| apps/pwa/src/features/categories/CategoriesScreen.tsx | Updated draft creation and patch object to include color field |
| apps/pwa/src/api.ts | Added optional color field to category update API body |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <Box | ||
| key={option} | ||
| component="button" | ||
| type="button" | ||
| onClick={() => setColor(option)} | ||
| sx={{ | ||
| width: 28, | ||
| height: 28, | ||
| borderRadius: '50%', | ||
| border: color === option ? '2px solid #111827' : '1px solid #CFD8DC', | ||
| backgroundColor: option, | ||
| cursor: 'pointer', | ||
| }} | ||
| /> |
There was a problem hiding this comment.
The color swatch buttons should have keyboard navigation support. Consider adding tabIndex={0} and onKeyDown handlers to allow keyboard users to select colors using Enter or Space keys.
| sx={{ | ||
| width: 28, | ||
| height: 28, | ||
| borderRadius: '50%', | ||
| border: color === option ? '2px solid #111827' : '1px solid #CFD8DC', | ||
| backgroundColor: option, | ||
| cursor: 'pointer', | ||
| }} |
There was a problem hiding this comment.
The border color '#111827' and '#CFD8DC' are hardcoded. Consider using theme colors like 'theme.palette.grey[900]' and 'theme.palette.divider' for better maintainability and theme consistency.
| sx={{ | |
| width: 28, | |
| height: 28, | |
| borderRadius: '50%', | |
| border: color === option ? '2px solid #111827' : '1px solid #CFD8DC', | |
| backgroundColor: option, | |
| cursor: 'pointer', | |
| }} | |
| sx={(theme) => ({ | |
| width: 28, | |
| height: 28, | |
| borderRadius: '50%', | |
| border: | |
| color === option | |
| ? `2px solid ${theme.palette.grey[900]}` | |
| : `1px solid ${theme.palette.divider}`, | |
| backgroundColor: option, | |
| cursor: 'pointer', | |
| })} |
| <Box | ||
| key={option} | ||
| component="button" | ||
| type="button" | ||
| onClick={() => setColor(option)} | ||
| sx={{ | ||
| width: 28, | ||
| height: 28, | ||
| borderRadius: '50%', | ||
| border: color === option ? '2px solid #111827' : '1px solid #CFD8DC', | ||
| backgroundColor: option, | ||
| cursor: 'pointer', | ||
| }} | ||
| /> |
There was a problem hiding this comment.
The color swatch buttons lack accessible labels. Consider adding aria-label attributes with the color name or value to make the color picker accessible to screen reader users.
| <Box | ||
| key={option} | ||
| component="button" | ||
| type="button" | ||
| onClick={() => onChangeDraft({ color: option })} | ||
| sx={{ | ||
| width: 28, | ||
| height: 28, | ||
| borderRadius: '50%', | ||
| border: draft.color === option ? '2px solid #111827' : '1px solid #CFD8DC', | ||
| backgroundColor: option, | ||
| cursor: 'pointer', | ||
| }} | ||
| /> |
There was a problem hiding this comment.
The color swatch buttons should have keyboard navigation support. Consider adding tabIndex={0} and onKeyDown handlers to allow keyboard users to select colors using Enter or Space keys.
| {(() => { | ||
| const IconComponent = CATEGORY_ICON_COMPONENTS[icon] ?? CategoryIcon; | ||
| return ( | ||
| <Avatar sx={{ width: 32, height: 32, bgcolor: `${color}22`, color }}> | ||
| <IconComponent fontSize="small" /> | ||
| </Avatar> | ||
| ); | ||
| })()} |
There was a problem hiding this comment.
The IIFE (Immediately Invoked Function Expression) pattern is unnecessary here. Consider extracting the IconComponent directly without wrapping it in an IIFE for better readability and performance.
| { name: 'shopping_bag', label: 'Shopping bag', group: 'subscriptions', keywords: ['shopping'] }, | ||
| { | ||
| name: 'shopping_cart', | ||
| label: 'Shopping cart', | ||
| group: 'subscriptions', |
There was a problem hiding this comment.
Shopping bag and shopping cart icons are grouped under "subscriptions" but these typically represent one-time shopping/groceries rather than recurring subscriptions. Consider moving these to a "shopping" group or keeping them in "bills/finance".
| { name: 'shopping_bag', label: 'Shopping bag', group: 'subscriptions', keywords: ['shopping'] }, | |
| { | |
| name: 'shopping_cart', | |
| label: 'Shopping cart', | |
| group: 'subscriptions', | |
| { name: 'shopping_bag', label: 'Shopping bag', group: 'bills/finance', keywords: ['shopping'] }, | |
| { | |
| name: 'shopping_cart', | |
| label: 'Shopping cart', | |
| group: 'bills/finance', |
| width: 28, | ||
| height: 28, | ||
| borderRadius: '50%', | ||
| border: draft.color === option ? '2px solid #111827' : '1px solid #CFD8DC', |
There was a problem hiding this comment.
The border color '#111827' is hardcoded. Consider using theme colors like 'theme.palette.grey[900]' or 'theme.palette.text.primary' for better maintainability and theme consistency.
| border: draft.color === option ? '2px solid #111827' : '1px solid #CFD8DC', | |
| border: | |
| draft.color === option | |
| ? `2px solid ${theme.palette.text.primary}` | |
| : '1px solid #CFD8DC', |
| <Box | ||
| key={option} | ||
| component="button" | ||
| type="button" | ||
| onClick={() => onChangeDraft({ color: option })} | ||
| sx={{ | ||
| width: 28, | ||
| height: 28, | ||
| borderRadius: '50%', | ||
| border: draft.color === option ? '2px solid #111827' : '1px solid #CFD8DC', | ||
| backgroundColor: option, | ||
| cursor: 'pointer', | ||
| }} | ||
| /> |
There was a problem hiding this comment.
The color swatch buttons lack accessible labels. Consider adding aria-label attributes with the color name or value to make the color picker accessible to screen reader users.
| 'none', | ||
| ); | ||
| const [icon, setIcon] = useState<string>('savings'); | ||
| const [color, setColor] = useState<string>('#2E7D32'); |
There was a problem hiding this comment.
The default color '#2E7D32' is hardcoded in multiple places. Consider extracting it as a constant (e.g., 'DEFAULT_CATEGORY_COLOR') in constants.ts and importing it to avoid duplication and improve maintainability.
| {(() => { | ||
| const IconComponent = CATEGORY_ICON_COMPONENTS[icon] ?? CategoryIcon; | ||
| return ( | ||
| <Avatar sx={{ width: 32, height: 32, bgcolor: `${color}22`, color }}> |
There was a problem hiding this comment.
The alpha transparency value '22' (hex) appended to the color results in very low opacity (~13%). This may not provide enough visual distinction for the background. Consider using a higher value like '33' (20% opacity) or '44' (27% opacity) for better visibility.
| <Avatar sx={{ width: 32, height: 32, bgcolor: `${color}22`, color }}> | |
| <Avatar sx={{ width: 32, height: 32, bgcolor: `${color}33`, color }}> |
- extract DEFAULT_CATEGORY_COLOR constant (no more hardcoded duplication) - use theme palette for swatch borders instead of hardcoded hex - add aria-label on color swatch buttons for accessibility - remove unnecessary 'as CategoryIconOption[]' type casts - remove IIFE pattern from preview icon rendering - bump avatar background alpha from 22 to 33 (13% → 20% opacity) - move shopping_bag/shopping_cart from subscriptions to new shopping group
Summary
Upgrades category creation/editing UX with a much richer icon and color selection flow.
What changed
Files
apps/pwa/src/features/categories/constants.tsapps/pwa/src/features/categories/CategoriesScreen.tsxapps/pwa/src/features/categories/dialogs/AddCategoryDialog.tsxapps/pwa/src/features/categories/dialogs/EditCategoryDialog.tsxapps/pwa/src/features/categories/types.tsapps/pwa/src/features/categories/utils.tsapps/pwa/src/api.tsValidation
pnpm --filter @tithe/pwa buildpasses