Skip to content

Personalize shopping lists with colors #155

Description

@OffCrazyFreak

Problem or motivation

A shopping list is identified only by its title, so the index at /shopping-lists is a wall of near-identical cards and telling "Tjedna kupovina" from "Tjedna kupovina 2" takes reading rather than glancing.

A colour is the cheapest thing that fixes that: one VARCHAR(7), no upload pipeline, no storage question, no payload cost, and it works at card size where an image would be a thumbnail anyway. It is also what the category actually does. AnyList uses colours and folders to tell lists apart, and reserves photos for items, where a picture of the exact product helps you find it on a shelf.

Digital cards already have this (cardColor); shopping lists have no color, icon or emoji field at all.

Companion issue: #154, the same goal with images. This one should land first.

Proposed solution

Reuse the digital-cards colour picker as-is, then apply the chosen colour in three places.

The picker. PR #142 ships a three-variant picker with a switcher:

  • frontend/src/app/(user)/digital-cards/components/forms/color-picker/card-color-field.tsx (the field wrapper)
  • color-picker-switcher.tsx offers "Paleta i nijansa", "Šesterokuti", "Samo paleta"
  • color-picker-swatch-slider.tsx, color-picker-honeycomb.tsx, color-picker-swatches.tsx
  • hue-slider.tsx
  • frontend/src/app/(user)/digital-cards/utils/card-colors.ts pins saturation 62 and lightness 42 so every reachable hue stays legible, plus three neutrals

It is currently feature-local under digital-cards/, so step one is lifting it into a shared location, probably components/custom/color-picker/ with the palette constants alongside. card-color-field.tsx is bound to DigitalCardFormData through useFormContext, so it needs generalising to a plain value/onChange field.

Where the colour shows up:

  1. Shopping list card border on the index (frontend/src/app/(user)/shopping-lists/components/shopping-list-item.tsx). This is the main payoff.
  2. An indicator next to the list name, a small dot or bar. Explicitly a stand-in until Personalize shopping lists with images #154 ships an image, so build it so it can be swapped for, or sit alongside, a thumbnail.
  3. A border on the info table on the list detail page (frontend/src/app/(user)/shopping-lists/[id]/components/shopping-list-info-table.tsx), subtle but present.

One design caveat worth settling early. The card palette is tuned for fills under white text: lightness 42 is quite dark, which is right for a filled card face and probably too heavy for a subtle table border at full opacity. Expect to need an opacity or a lightened variant for use 3, and possibly use 1. Decide whether that is a second derived value or just a Tailwind opacity modifier, and keep it in one place so the three surfaces cannot drift.

Alternatives considered

An emoji instead of a colour. Zero new UI, more expressive per list, and it doubles as the indicator in use 2. Rejected as the primary: it does not give a border, which is the surface that makes the index scannable. Worth considering as a later addition next to the colour.

A separate picker built for lists. Rejected outright: two colour pickers in one app is exactly the kind of drift the shared LINK_ACCESS_LABELS and compareHr consolidations were about. If the card picker is not good enough for lists, fix the card picker.

Auto-assign a colour per list. Rejected: the point is that the user recognises their list, and an assigned colour carries no meaning for them. extract-dominant-color.ts exists for the card case where an uploaded logo suggests a colour, which is a different situation.

Blocked on

Digital cards PR #142, which is where the picker comes from. Doing this before that merges would mean either duplicating the picker or building against a moving target.

Additional context

  • Backend: shopping_list.color as VARCHAR(7), nullable, matching DigitalCard.cardColor's @Column(name = "card_color", nullable = false, length = 7). Nullable here rather than not-null, so existing lists do not need a backfill and "no colour" stays a real state.
  • Frontend schema: color: z.string().regex(/^#[0-9a-fA-F]{6}$/).nullable() in frontend/src/lib/api/schemas/shopping-list.ts, mirroring the digital-card rule.
  • ddl-auto=update adds a nullable column cleanly, so there is no manual deploy step (contrast docs/DEPLOYMENT.md §10.1, which is about drops).
  • Copying a list should copy the colour. It is a plain string on the request, so this is nearly free, but it does need adding to the copy path in frontend/src/app/(user)/shopping-lists/[id]/hooks/use-shopping-list-mutations.ts.
  • Share-link recipients should see the colour: it is part of how the owner labelled the list, and it carries no private information.
  • Accessibility: colour must not be the only signal for anything. It is decoration next to a title that is already there, so it stays additive, and the indicator in use 2 needs no accessible name of its own as long as the title carries the meaning.

Acceptance:

  • The colour picker lives in a shared location and is no longer bound to DigitalCardFormData
  • Digital cards still use it, unchanged in behaviour
  • An owner can set and clear a list colour from the list modal
  • The colour renders as the list card border on the index
  • The colour renders as an indicator next to the list name
  • The colour renders as a subtle border on the info table on the detail page
  • The subtle-vs-solid treatment is defined once, not per surface
  • Copying a list copies the colour
  • A list with no colour renders exactly as it does today

Affected area

Frontend (web / PWA)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions