diff --git a/docs/superpowers/specs/2026-07-09-order-color-stripe-design.md b/docs/superpowers/specs/2026-07-09-order-color-stripe-design.md new file mode 100644 index 0000000..e1ab62b --- /dev/null +++ b/docs/superpowers/specs/2026-07-09-order-color-stripe-design.md @@ -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 `
` (or a thin coloured bar). +- Invoicing: same treatment on each `
` 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 `
` 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. \ No newline at end of file