Label create form: color dropdown with preset swatches (5-10) + custom hex with live preview #325

Closed
opened 2026-09-11 14:50:36 +00:00 by crueber · 3 comments
Owner

What's requested

On the label create form (and any label color input), replace the raw 6-hex text input with a color dropdown: 5–10 preset swatches to choose from, plus an option to specify a custom hex value. When the hex field holds a valid 6-hex value, show a live color preview next to it.

Current state (code evidence)

  • web/src/pages/Labels.jsx create form (:69-99): the color field is a bare text input — class="input w-28", pattern="[0-9a-fA-F]{6}", title="6-hex RGB without #", defaulting to d73a4a. No visual feedback until the label is created and rendered in the list.
  • The list row already renders color swatches correctly (:43-47 — inline background-color: #<color> dot), so a preview can reuse the exact same rendering.
  • The default color signal is getColor() seeded "d73a4a"; label creation posts {name, color} through labels.create — no backend change needed.
  • Label rendering elsewhere (issue page LabelChip, milestones page) consumes the same {name, color} shape — color values are unconstrained strings server-side, validated client-side only by this pattern attribute.

Proposed design

  1. Presets: a fixed palette constant in web/src/lib/ (headless, per convention) of 8 label-friendly colors — balanced across semantic families so the chooser covers bug/idea/question/wontfix-ish hues without implying the GitHub label names:
    e.g. d73a4a (red), e36209 (orange), f9d0c4→ better saturated set: d73a4a, e36209, fbca04 (yellow), 0e8a16 (green), 1d76db (blue), 5319e7 (purple), d876e3 (magenta), 3a3a3a (gray-ish). (These are the classic GitHub label palette hues — familiar and proven legible on both themes; final swatch list is the implementer's, but 8 is the target count.)
  2. Dropdown UI: a <select>-style control (match the app's filter-select styling, Issues.jsx:96) or a small popover swatch grid (the repo has established popover patterns — RefPicker/ChooserMenu in CommentComposer) — implementer's call, but it must include:
    • one option per preset swatch (rendered with the color dot in the closed state),
    • a final "Custom hex…" option that reveals the free-text hex input.
  3. Hex input + preview: the existing text input stays for the custom path, with:
    • the pattern validation kept,
    • a live preview dot next to it (same rendering as the list rows, :43-47) that shows the color when the value is a valid 6-hex (regex ^[0-9a-fA-F]{6}$) and falls back to a neutral/empty state when invalid — never a broken or stale preview,
    • selecting a preset swatch fills the input too (single source of truth: the color signal), so the preview always reflects the effective value.
  4. Form submit unchanged: posts the resolved 6-hex (no #), same validation.
  5. Scope: the create form on the labels page. If the color input is factored as a small component (recommended — LabelColorPicker in web/src/components/), note that a future issue-edit/rename flow can reuse it; do not build the edit flow in this issue.

Acceptance criteria

  • Create form offers a dropdown with 5–10 preset swatches plus a custom-hex option; presets fill the effective color in one click.
  • Typing a valid 6-hex value shows a live preview dot next to the input; invalid input shows no misleading preview (neutral state) and the form still enforces the pattern on submit.
  • The preview renders identically to the list-row swatches (same inline background-color approach).
  • Created labels carry the resolved 6-hex (no #) — server contract unchanged, no new endpoints.
  • Palette constant is headless-testable (count, valid hex per entry, no duplicates).
  • Light/dark themes: swatches and preview legible; popover/select styling matches the app's existing filter-select or popover patterns; works at 390px.
## What's requested On the label create form (and any label color input), replace the raw 6-hex text input with a **color dropdown**: 5–10 preset swatches to choose from, plus an option to specify a custom hex value. When the hex field holds a **valid** 6-hex value, show a **live color preview** next to it. ## Current state (code evidence) - `web/src/pages/Labels.jsx` create form (:69-99): the color field is a bare text input — `class="input w-28"`, `pattern="[0-9a-fA-F]{6}"`, `title="6-hex RGB without #"`, defaulting to `d73a4a`. No visual feedback until the label is created and rendered in the list. - The list row already renders color swatches correctly (`:43-47` — inline `background-color: #<color>` dot), so a preview can reuse the exact same rendering. - The default color signal is `getColor()` seeded `"d73a4a"`; label creation posts `{name, color}` through `labels.create` — no backend change needed. - Label rendering elsewhere (issue page `LabelChip`, milestones page) consumes the same `{name, color}` shape — color *values* are unconstrained strings server-side, validated client-side only by this pattern attribute. ## Proposed design 1. **Presets**: a fixed palette constant in `web/src/lib/` (headless, per convention) of 8 label-friendly colors — balanced across semantic families so the chooser covers bug/idea/question/wontfix-ish hues without implying the GitHub label names: e.g. `d73a4a` (red), `e36209` (orange), `f9d0c4`→ better saturated set: `d73a4a`, `e36209`, `fbca04` (yellow), `0e8a16` (green), `1d76db` (blue), `5319e7` (purple), `d876e3` (magenta), `3a3a3a` (gray-ish). (These are the classic GitHub label palette hues — familiar and proven legible on both themes; final swatch list is the implementer's, but 8 is the target count.) 2. **Dropdown UI**: a `<select>`-style control (match the app's filter-select styling, `Issues.jsx:96`) or a small popover swatch grid (the repo has established popover patterns — `RefPicker`/`ChooserMenu` in CommentComposer) — implementer's call, but it must include: - one option per preset swatch (rendered with the color dot in the closed state), - a final "Custom hex…" option that reveals the free-text hex input. 3. **Hex input + preview**: the existing text input stays for the custom path, with: - the `pattern` validation kept, - a **live preview dot** next to it (same rendering as the list rows, `:43-47`) that shows the color when the value is a valid 6-hex (regex `^[0-9a-fA-F]{6}$`) and falls back to a neutral/empty state when invalid — never a broken or stale preview, - selecting a preset swatch fills the input too (single source of truth: the color signal), so the preview always reflects the effective value. 4. **Form submit unchanged**: posts the resolved 6-hex (no `#`), same validation. 5. **Scope**: the create form on the labels page. If the color input is factored as a small component (recommended — `LabelColorPicker` in `web/src/components/`), note that a future issue-edit/rename flow can reuse it; do not build the edit flow in this issue. ## Acceptance criteria - [ ] Create form offers a dropdown with 5–10 preset swatches plus a custom-hex option; presets fill the effective color in one click. - [ ] Typing a valid 6-hex value shows a live preview dot next to the input; invalid input shows no misleading preview (neutral state) and the form still enforces the pattern on submit. - [ ] The preview renders identically to the list-row swatches (same inline background-color approach). - [ ] Created labels carry the resolved 6-hex (no `#`) — server contract unchanged, no new endpoints. - [ ] Palette constant is headless-testable (count, valid hex per entry, no duplicates). - [ ] Light/dark themes: swatches and preview legible; popover/select styling matches the app's existing filter-select or popover patterns; works at 390px.
crueber added this to the v1 milestone 2026-09-11 14:50:37 +00:00
Author
Owner

Fix open in PR #335: #335 (branch fix/issue-325). Preset dropdown (8 swatches) + Custom hex reveal + live preview reusing the row swatch rendering; headless tests + vite build green; no backend change, no new deps. Browser proof explicitly open (shared-daemon loopback block) — worth a live-Chrome pass on review.

Fix open in PR #335: https://git.packden.us/crueber/walhub/pulls/335 (branch fix/issue-325). Preset dropdown (8 swatches) + Custom hex reveal + live preview reusing the row swatch rendering; headless tests + vite build green; no backend change, no new deps. Browser proof explicitly open (shared-daemon loopback block) — worth a live-Chrome pass on review.
Author
Owner

Review of PR #335 (fix/issue-325) — verified in scratch worktree at origin/fix/issue-325 (b747e66). No browser pass (sandbox blocks loopback; node tests + reasoning only — noted explicitly per instructions).

ACCEPTANCE (all met):

  • Palette web/src/lib/label-colors.js: 8 presets, classic GitHub hues (d73a4a first — form default preserved), all lowercase 6-hex, no duplicates. Sane hues, legible both themes.
  • Closed state: trigger button shows swatch + mono hex (LabelColorPicker.jsx). Neutral (no background) while invalid — no misleading preview.
  • Popover: native buttons, role=listbox/option, aria-haspopup/expanded/selected; Esc closes + refocuses trigger, outside-click closes, listeners removed in onCleanup (LabelPicker/RefPicker idioms). No focus trap but Tab reaches all buttons — fine for scope.
  • Custom hex: pattern=[0-9a-fA-F]{6} kept on the input (lives inside the , so native validation still blocks submit on invalid — submit contract unchanged: labels.create({name, color}) with getColor() seeded d73a4a, Labels.jsx:16,26).
  • Live preview: valid() ? background-color : {} — preview dot reuses the exact list-row swatch classes (inline-block h-3 w-3 rounded-full border …, verified identical to Labels.jsx list rows).
  • Popover reuses audited .label-drop (ui.css opaque + max-width:calc(100vw-1rem) per #278) at w-64 + flex-wrap trigger row — 390px-safe. No new CSS, no new deps (package.json/ui.css untouched — laws 1/8 ok; law 7 n/a, pure local UI; law 12 ok, 02_issues.md decision in the same PR).
  • Reusable shape sane: props {color, onChange}, single source of truth in parent signal; showCustom() keeps input visible for non-preset values.

TESTS (scratch worktree, node_modules symlinked from main):

  • label-colors.test.js: 10/10 pass.
  • Full unit suite excl. smoke: 627 pass / 0 fail (PR body says 630 — recount suggests the 3 smoke tests were included in that number; non-smoke actual is 627).
  • smoke.test.js errors identically on clean main (no loopback server in sandbox — pre-existing environmental, not caused by this PR).
  • vite build: green. esbuild SDK bundle: green.

NITS (non-blocking, no push made):

  • White ✓ checkmark on bright presets (fbca04 yellow especially) is low-contrast; drop-shadow only partly mitigates. Follow-up could use a luminance-aware check color — not worth holding this PR.

MERGE RECOMMENDATION: ready to merge (modulo the requested real-browser sanity check, which neither author nor reviewer could perform in this sandbox).

Review of PR #335 (fix/issue-325) — verified in scratch worktree at origin/fix/issue-325 (b747e66). No browser pass (sandbox blocks loopback; node tests + reasoning only — noted explicitly per instructions). ACCEPTANCE (all met): - Palette web/src/lib/label-colors.js: 8 presets, classic GitHub hues (d73a4a first — form default preserved), all lowercase 6-hex, no duplicates. Sane hues, legible both themes. - Closed state: trigger button shows swatch + mono hex (LabelColorPicker.jsx). Neutral (no background) while invalid — no misleading preview. - Popover: native buttons, role=listbox/option, aria-haspopup/expanded/selected; Esc closes + refocuses trigger, outside-click closes, listeners removed in onCleanup (LabelPicker/RefPicker idioms). No focus trap but Tab reaches all buttons — fine for scope. - Custom hex: pattern=[0-9a-fA-F]{6} kept on the input (lives inside the <form>, so native validation still blocks submit on invalid — submit contract unchanged: labels.create({name, color}) with getColor() seeded d73a4a, Labels.jsx:16,26). - Live preview: valid() ? background-color : {} — preview dot reuses the exact list-row swatch classes (inline-block h-3 w-3 rounded-full border …, verified identical to Labels.jsx list rows). - Popover reuses audited .label-drop (ui.css opaque + max-width:calc(100vw-1rem) per #278) at w-64 + flex-wrap trigger row — 390px-safe. No new CSS, no new deps (package.json/ui.css untouched — laws 1/8 ok; law 7 n/a, pure local UI; law 12 ok, 02_issues.md decision in the same PR). - Reusable shape sane: props {color, onChange}, single source of truth in parent signal; showCustom() keeps input visible for non-preset values. TESTS (scratch worktree, node_modules symlinked from main): - label-colors.test.js: 10/10 pass. - Full unit suite excl. smoke: 627 pass / 0 fail (PR body says 630 — recount suggests the 3 smoke tests were included in that number; non-smoke actual is 627). - smoke.test.js errors identically on clean main (no loopback server in sandbox — pre-existing environmental, not caused by this PR). - vite build: green. esbuild SDK bundle: green. NITS (non-blocking, no push made): - White ✓ checkmark on bright presets (fbca04 yellow especially) is low-contrast; drop-shadow only partly mitigates. Follow-up could use a luminance-aware check color — not worth holding this PR. MERGE RECOMMENDATION: ready to merge (modulo the requested real-browser sanity check, which neither author nor reviewer could perform in this sandbox).
Author
Owner

Fixed by PR #335 (review clean; palette + popover a11y + submit contract + #278 popover reuse verified), merged. Closing.

Fixed by PR #335 (review clean; palette + popover a11y + submit contract + #278 popover reuse verified), merged. Closing.
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
crueber/walhub#325
No description provided.