Add order color stripe spec

This commit is contained in:
SJCoetz 2026-07-10 02:03:16 +02:00
parent 6d03be2ebc
commit 0d508095f0

View file

@ -0,0 +1,117 @@
# Per-order color stripe on Collection Confirm and Invoicing
**Goal:** Visually separate each order group on the Collection Confirm and Invoicing pages with a colored left-edge stripe, so the list reads as distinct orders instead of one continuous flow.
## Scope
### In Scope
- New helper module `src/lib/order-color.ts` exporting:
- A `ORDER_COLOR_PALETTE` const — a curated list of 6 Tailwind colour token pairs (`border-{color}-500/40` + `bg-{color}-500/8`) with a matching translucent stripe.
- A `colorForOrder(orderId: string)` function that hashes the id and returns one of the palette entries deterministically. Same id → same color, every render, every page.
- Collection Confirm: each group section gets a 4px-wide coloured left-edge stripe. The stripe uses `border-l-4 border-{color}-500/40` on the existing `<section>` (or a thin coloured bar).
- Invoicing: same treatment on each `<section>` that represents an order group.
- Light unit test for `colorForOrder` covering: deterministic, evenly distributed across the palette, returns a known colour for a known id.
### Out of Scope
- The header pills, filter chips, or any non-group element.
- Tab-level colouring (each tab, not each order).
- Per-status colouring (e.g. a different colour for `awaiting` vs `collected`).
- Dark-mode-specific colour variants — the chosen palette already has decent contrast on both.
- The Collection Pool page or any other list page.
## Current State
Both pages already group items by `orderNumber` and render a `<section>` per group. Each section has `rounded-xl border bg-white p-4 shadow-sm` (Collection Confirm) or `rounded-xl border bg-white p-4 shadow-sm` (Invoicing). The two groups are visually identical, so the eye reads them as one continuous list.
A deterministic hash-to-palette helper gives the right balance: stable across renders and across the two pages, but visually distinct per order.
## User Confirmed Rule
- Use a 6-color curated palette, hashed by order id.
## UX
### Per-order stripe
```
┌─Collection Confirm──────────────────┐
│ PO-2026-07-009 · Supplier · 3 items │
└─ Card ──────────────────────────────┐
│ ▌ Supplier name │ ← 4px coloured stripe on the left
│ ▌ Description │
│ ▌ ... │
└────────────────────────────────────┘
```
- Stripe is 4px wide, full height of the group card.
- Stripe colour comes from `colorForOrder(group.orderNumber)`. Same order on Collection Confirm and Invoicing renders the same colour.
- The stripe is decorative — the rest of the card layout is unchanged.
### Palette
Six colors, in stable order, all designed to read on a light background:
| Index | Tailwind tokens |
| ----- | --------------------------- |
| 0 | `border-emerald-500/40` |
| 1 | `border-sky-500/40` |
| 2 | `border-amber-500/40` |
| 3 | `border-rose-500/40` |
| 4 | `border-violet-500/40` |
| 5 | `border-teal-500/40` |
`bg-{color}-500/8` is also exported for callers that want a faint body tint (not used by this change, but available).
## Behavior
### Hashing
`colorForOrder` uses a small FNV-1a hash so the distribution is even and the helper has no deps. The hash is taken mod 6 to pick the palette index. Empty / undefined input falls back to index 0 so the helper is total.
```ts
const PALETTE_SIZE = 6;
const FNV_OFFSET = 0x811c9dc5;
const FNV_PRIME = 0x01000193;
export function colorForOrder(orderId: string | undefined | null): OrderColor {
if (!orderId) return ORDER_COLOR_PALETTE[0];
let hash = FNV_OFFSET;
for (let i = 0; i < orderId.length; i++) {
hash ^= orderId.charCodeAt(i);
hash = Math.imul(hash, FNV_PRIME) >>> 0;
}
return ORDER_COLOR_PALETTE[hash % PALETTE_SIZE];
}
```
The `>>> 0` keeps the value in the uint32 range so `Math.imul` doesn't produce a negative result on overflow.
## Testing
### Unit test (`src/lib/order-color.test.ts`)
1. Returns the same entry for the same id (deterministic).
2. Returns the first palette entry for `""`, `null`, and `undefined`.
3. For 60 distinct ids, the distribution across the 6 palette entries is at least 1 of each (catches a degenerate hash).
4. Known id → known entry (e.g. `"PO-2026-07-009"` → a specific index captured at test-writing time and locked in).
### Manual smoke
1. Open Collection Confirm. The first order has a coloured stripe. Open Invoicing, find the same order. The stripe colour matches.
2. Two adjacent orders should have visibly different stripes (probability of same index is 1/6).
3. No layout regression — the stripe is 4px and the rest of the card is unchanged.
## Acceptance Criteria
- Every order group on Collection Confirm and Invoicing has a 4px left-edge stripe in a colour from the palette.
- The same order renders the same colour on both pages.
- Adjacent orders have visibly different stripes (≥ 1/6 chance of collision; visual variation is the goal).
- The unit tests pass; full suite non-regressive.
## Risk / Rollback
- **Visual risk:** low. The stripe is a thin decorative element; no layout shifts beyond 4px of horizontal padding.
- **Hash risk:** none. FNV-1a is stable across runtimes and platforms.
- **Rollback:** delete `src/lib/order-color.ts`, remove the `border-l-4 border-{color}-500/40` classes from the section elements in both pages, remove the import.