#23 — List Color & Icon Customisation #23

Closed
opened 2026-08-18 13:12:54 +02:00 by lena · 1 comment
lena commented 2026-08-18 13:12:54 +02:00 (Migrated from git.butzei.de)

Story: List Color & Icon Customisation

As a list owner,
I want to assign a colour and icon to a list,
so that I can tell lists apart at a glance in the sidebar.

Acceptance criteria:

  • The list menu (owner-only) includes an "Customise" option opening a small picker
  • The colour picker offers a fixed palette of at least 10 colours (11: Grey + 10 vivid)
  • The icon picker offers a fixed set of at least 15 icons (18; Sparkles substitutes for the
    story's broom example — lucide-react 1.24 has no broom glyph, see design doc)
  • The selected colour and icon appear in the sidebar list item and in the list header
  • Changes are visible to all list members immediately (no reload required) — via the existing
    TodoListBroadcastDto WebSocket path
  • Defaults are a neutral grey colour and a generic list icon

Out of scope for this story:

  • Custom icon uploads
  • Colour themes that affect more than the list accent
  • Per-member colour overrides

Blockers: None

Priority: Could — low effort, high perceived polish; good sprint filler alongside a heavier feature.

Resolution

Delivered 2026-07-21. New TodoListColor/TodoListIcon closed enums, SetTodoListAppearanceCommand
(owner-only), and a ListAppearanceDialog picker wired into OwnerActionsMenu's new "Customise"
item. See 23_list_color_icon_design.md for the full design rationale.

/code-review (8 finder angles, high effort) found and fixed one real bug: RemoveTodoListMemberCommandHandler's
broadcast had never forwarded IsArchived (pre-dating this story, but re-exposed on the exact line
this story touched to add Color/Icon) — an archived list's members-changed broadcast silently
reset isArchived to false on every connected client until the next full refetch. Fixed alongside.

Review also surfaced a recurring pattern worth tracking rather than fixing inline: TodoListBroadcastDto
is hand-constructed from TodoListDto at 8 separate call sites, and this is now the second field
(after IsArchived) to have been silently dropped at one of them because the compiler can't catch a
missing optional constructor arg. Drafted as docs/features/new/62_todolist_broadcast_dto_mapping_helper.md
rather than refactored in this cycle, to keep the fix scoped to the one bug actually found.

# Story: List Color & Icon Customisation **As a** list owner, **I want to** assign a colour and icon to a list, **so that** I can tell lists apart at a glance in the sidebar. **Acceptance criteria:** - [x] The list menu (owner-only) includes an "Customise" option opening a small picker - [x] The colour picker offers a fixed palette of at least 10 colours (11: Grey + 10 vivid) - [x] The icon picker offers a fixed set of at least 15 icons (18; Sparkles substitutes for the story's broom example — lucide-react 1.24 has no broom glyph, see design doc) - [x] The selected colour and icon appear in the sidebar list item and in the list header - [x] Changes are visible to all list members immediately (no reload required) — via the existing `TodoListBroadcastDto` WebSocket path - [x] Defaults are a neutral grey colour and a generic list icon **Out of scope for this story:** - Custom icon uploads - Colour themes that affect more than the list accent - Per-member colour overrides **Blockers:** None **Priority:** Could — low effort, high perceived polish; good sprint filler alongside a heavier feature. ## Resolution Delivered 2026-07-21. New `TodoListColor`/`TodoListIcon` closed enums, `SetTodoListAppearanceCommand` (owner-only), and a `ListAppearanceDialog` picker wired into `OwnerActionsMenu`'s new "Customise" item. See `23_list_color_icon_design.md` for the full design rationale. `/code-review` (8 finder angles, high effort) found and fixed one real bug: `RemoveTodoListMemberCommandHandler`'s broadcast had never forwarded `IsArchived` (pre-dating this story, but re-exposed on the exact line this story touched to add `Color`/`Icon`) — an archived list's members-changed broadcast silently reset `isArchived` to `false` on every connected client until the next full refetch. Fixed alongside. Review also surfaced a recurring pattern worth tracking rather than fixing inline: `TodoListBroadcastDto` is hand-constructed from `TodoListDto` at 8 separate call sites, and this is now the *second* field (after `IsArchived`) to have been silently dropped at one of them because the compiler can't catch a missing optional constructor arg. Drafted as `docs/features/new/62_todolist_broadcast_dto_mapping_helper.md` rather than refactored in this cycle, to keep the fix scoped to the one bug actually found.
lena commented 2026-08-18 13:12:54 +02:00 (Migrated from git.butzei.de)

design (23_list_color_icon_design.md)

Design: List Colour & Icon Customisation (#23)

Data model

Two new plain C# enums (not Vogen VOs — matches the LabelColor precedent for closed palettes;
no validation logic is needed since the type system + JSON deserialization already reject
out-of-range values):

Common/Types/TodoListColor.cs

Grey (default/neutral), Red, Orange, Amber, Yellow, Lime, Green, Teal, Blue, Purple, Pink

11 values — Grey is the neutral default the AC requires, plus 10 vivid colours for the picker
(satisfies "at least 10 colours").

Common/Types/TodoListIcon.cs

List (default/generic), ShoppingCart, Sparkles, Clapperboard, Home, CheckSquare, Utensils,
Wrench, Car, Gift, Book, Dumbbell, PawPrint, Plane, Music, Heart, Star, Briefcase

18 values — List is the generic default, 17 more for the picker (satisfies "at least 15 icons").
The story's example set includes a broom (); lucide-react 1.24 has no broom glyph, so Sparkles
substitutes for cleaning-flavoured lists — captures the same "chore" intent without a literal match.

Both stored as text via HasConversion<string>(), same as LabelEntity.Color — one EF migration
(AddColorAndIconToTodoList) adding two non-nullable columns with defaults (Grey / List) to
TodoListEntity.

API surface

One new command, matching the RenameTodoListCommand shape (owner-only, single mutation, broadcasts
the updated list):

SetTodoListAppearanceCommand(TodoListId Id, TodoListColor Color, TodoListIcon Icon) : IRequest<TodoListDto>

Both fields are set together in one call — the picker UI (colour grid + icon grid in one dialog)
never needs to persist a partial selection, so there's no reason to split into two commands.
Authorization: AuthorizeTodoListOwnerAccessForCurrentUserQuery, identical to RenameTodoListCommandHandler.
No activity-feed entry — cosmetic-only change, doesn't rise to the level of ListRenamed/list
membership events already logged (keeps the activity feed focused on collaboration-relevant events).

TodoListDto, TodoListBroadcastDto, and TodoListSaveDto all gain Color and Icon (defaulted
to TodoListColor.Grey / TodoListIcon.List on the create-time TodoListSaveDto positional
params, so CreateTodoListCommand doesn't need a frontend change to keep compiling — the AC only
asks for edit-after-creation via the list menu, not a colour/icon picker in the create-list flow).

Frontend

  • ReactUi/src/lib/listColors.ts — mirrors labelColors.ts: Record<TodoListColor, string> of
    literal Tailwind bg-*-500 classes (JIT scanner needs literal strings, not template-built ones).
  • ReactUi/src/lib/listIcons.ts — Record<TodoListIcon, LucideIcon> mapping each enum value to its
    lucide-react component reference.
  • OwnerActionsMenu.tsx — new "Customise" DropdownMenuItem opening a Dialog (consistent with
    Rename/Delete/Transfer, all Dialog in this menu — not Popover, since the trigger lives in the
    sidebar's owner menu where clipping was the reason #58 chose Popover for an inline row
    control; this dialog is invoked from a dropdown, same as Rename already is).
  • New ListAppearanceDialog.tsx — colour swatch grid + icon grid, Save calls
    callApi('SetTodoListAppearanceCommand', {id, color, icon}).
  • TodoListItem.tsx — small coloured dot + icon glyph before the title in the sidebar row.
  • TodoListHeader.tsx — same swatch + icon next to the <h2> title; TodoListHeader gains
    color/icon props, passed from TodoList.tsx's selectedTodoList.
  • Store: no change needed — updateTodoList already does a spread merge
    ({...t, ...todoList}), so new broadcast fields flow through automatically.

Real-time

SetTodoListAppearanceCommandHandler publishes changeSubject.Updated(id, TodoListBroadcastDto)
with the new fields populated — UserScopedTodoListChangePublisher needs no change, it's already
generic over TodoListBroadcastDto.

Out of scope (per story)

Custom icon uploads, colour themes beyond the list accent, per-member overrides. Not touching the
create-list flow's dialog — colour/icon are edit-only via the owner menu, per AC wording ("The list
menu ... includes a 'Customise' option").

Security note

Pre-reviewed: both new fields are closed server-side enums — no free-text input, no injection
surface. Mutation gated by the existing AuthorizeTodoListOwnerAccessForCurrentUserQuery (owner-only,
same as Rename/Archive). No new endpoint-exclusion-list concern — this command is meant to be
publicly callable over HTTP like every other list-mutation command.

**design** (`23_list_color_icon_design.md`) # Design: List Colour & Icon Customisation (`#23`) ## Data model Two new plain C# enums (not Vogen VOs — matches the `LabelColor` precedent for closed palettes; no validation logic is needed since the type system + JSON deserialization already reject out-of-range values): **`Common/Types/TodoListColor.cs`** ``` Grey (default/neutral), Red, Orange, Amber, Yellow, Lime, Green, Teal, Blue, Purple, Pink ``` 11 values — `Grey` is the neutral default the AC requires, plus 10 vivid colours for the picker (satisfies "at least 10 colours"). **`Common/Types/TodoListIcon.cs`** ``` List (default/generic), ShoppingCart, Sparkles, Clapperboard, Home, CheckSquare, Utensils, Wrench, Car, Gift, Book, Dumbbell, PawPrint, Plane, Music, Heart, Star, Briefcase ``` 18 values — `List` is the generic default, 17 more for the picker (satisfies "at least 15 icons"). The story's example set includes a broom (); lucide-react 1.24 has no broom glyph, so `Sparkles` substitutes for cleaning-flavoured lists — captures the same "chore" intent without a literal match. Both stored as `text` via `HasConversion<string>()`, same as `LabelEntity.Color` — one EF migration (`AddColorAndIconToTodoList`) adding two non-nullable columns with defaults (`Grey` / `List`) to `TodoListEntity`. ## API surface One new command, matching the `RenameTodoListCommand` shape (owner-only, single mutation, broadcasts the updated list): ``` SetTodoListAppearanceCommand(TodoListId Id, TodoListColor Color, TodoListIcon Icon) : IRequest<TodoListDto> ``` Both fields are set together in one call — the picker UI (colour grid + icon grid in one dialog) never needs to persist a partial selection, so there's no reason to split into two commands. Authorization: `AuthorizeTodoListOwnerAccessForCurrentUserQuery`, identical to `RenameTodoListCommandHandler`. No activity-feed entry — cosmetic-only change, doesn't rise to the level of `ListRenamed`/list membership events already logged (keeps the activity feed focused on collaboration-relevant events). `TodoListDto`, `TodoListBroadcastDto`, and `TodoListSaveDto` all gain `Color` and `Icon` (defaulted to `TodoListColor.Grey` / `TodoListIcon.List` on the create-time `TodoListSaveDto` positional params, so `CreateTodoListCommand` doesn't need a frontend change to keep compiling — the AC only asks for edit-after-creation via the list menu, not a colour/icon picker in the create-list flow). ## Frontend - `ReactUi/src/lib/listColors.ts` — mirrors `labelColors.ts`: `Record<TodoListColor, string>` of literal Tailwind `bg-*-500` classes (JIT scanner needs literal strings, not template-built ones). - `ReactUi/src/lib/listIcons.ts` — `Record<TodoListIcon, LucideIcon>` mapping each enum value to its lucide-react component reference. - `OwnerActionsMenu.tsx` — new "Customise" `DropdownMenuItem` opening a `Dialog` (consistent with Rename/Delete/Transfer, all `Dialog` in this menu — not `Popover`, since the trigger lives in the sidebar's owner menu where clipping was the reason `#58` chose `Popover` for an *inline* row control; this dialog is invoked from a dropdown, same as Rename already is). - New `ListAppearanceDialog.tsx` — colour swatch grid + icon grid, `Save` calls `callApi('SetTodoListAppearanceCommand', {id, color, icon})`. - `TodoListItem.tsx` — small coloured dot + icon glyph before the title in the sidebar row. - `TodoListHeader.tsx` — same swatch + icon next to the `<h2>` title; `TodoListHeader` gains `color`/`icon` props, passed from `TodoList.tsx`'s `selectedTodoList`. - Store: no change needed — `updateTodoList` already does a spread merge (`{...t, ...todoList}`), so new broadcast fields flow through automatically. ## Real-time `SetTodoListAppearanceCommandHandler` publishes `changeSubject.Updated(id, TodoListBroadcastDto)` with the new fields populated — `UserScopedTodoListChangePublisher` needs no change, it's already generic over `TodoListBroadcastDto`. ## Out of scope (per story) Custom icon uploads, colour themes beyond the list accent, per-member overrides. Not touching the create-list flow's dialog — colour/icon are edit-only via the owner menu, per AC wording ("The list menu ... includes a 'Customise' option"). ## Security note Pre-reviewed: both new fields are closed server-side enums — no free-text input, no injection surface. Mutation gated by the existing `AuthorizeTodoListOwnerAccessForCurrentUserQuery` (owner-only, same as Rename/Archive). No new endpoint-exclusion-list concern — this command is meant to be publicly callable over HTTP like every other list-mutation command.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
robert/todo#23
No description provided.