Skip to content

[HDX-1360] feat(app): refactor ColorSwatchInput to palette tokens#2265

Draft
alex-fedotyev wants to merge 2 commits into
mainfrom
alex/HDX-1360-color-swatch-input
Draft

[HDX-1360] feat(app): refactor ColorSwatchInput to palette tokens#2265
alex-fedotyev wants to merge 2 commits into
mainfrom
alex/HDX-1360-color-swatch-input

Conversation

@alex-fedotyev
Copy link
Copy Markdown
Contributor

@alex-fedotyev alex-fedotyev commented May 12, 2026

Summary

First PR in the issue #1360 ("Support configuring colors in line graphs, bar charts, and numbers") series. Refactors the existing ColorSwatchInput to a palette-only picker driven by the unified 13-token chart palette landed in PR #1627. The component remains internal in this PR; subsequent PRs in the series wire it into the chart editor, the number-tile display drawer, and the external dashboards API.

The existing component (originally PR #440, currently orphaned in both OSS and EE) used a free-form Mantine ColorInput plus a hand-picked 9-value hex array. Storing free-form hex values doesn't reflow with theme switches and risks WCAG contrast drift. The palette approach (PR #1627) was designed to be theme-agnostic and accessible; this PR makes the picker speak that vocabulary.

  • New ChartPaletteToken type (10 categorical + 3 semantic tokens) with CHART_PALETTE_TOKENS, CATEGORICAL_PALETTE_TOKENS, SEMANTIC_PALETTE_TOKENS exports and an isChartPaletteToken type guard in packages/app/src/utils.ts.
  • New getColorFromCSSToken(token) helper that reads the matching --color-chart-* CSS variable with an SSR-safe palette fallback.
  • ColorSwatchInput rewritten: palette-only popover split into Categorical (5x2 grid) and Semantic (1x3 row) sections, accessible trigger with aria-haspopup="dialog" and per-swatch aria-pressed, keyboard-navigable, focus ring via --color-outline-focus, legacy non-token values guarded and treated as unset. No production imports of the real ChartSeriesEditor or ChartDisplaySettingsDrawer.
  • Storybook stories matrix expanded to 12 stories across two tiers (six isolated stories for picker mechanics, five in-context mocks for product framing, one all-tokens reference). The in-context stories are Storybook-only mocks; they do not depend on the real editor components.
  • 13 RTL tests cover the rejection rules, keyboard activation, the legacy-value guard, and the popover-after-selection close behavior.

Notes for reviewers

Design ratification gate. This PR is the design review surface for the picker UX before the chart-editor / drawer PRs open. Once it's running locally (yarn workspace @hyperdx/app storybook) I'll attach a Loom walkthrough + screenshots in all four theme combos (HyperDX light, HyperDX dark, ClickStack light, ClickStack dark) and tag @elizabetdev. The in-context stories (InSeriesRow, InLineChart, InStackedBar, InNumberTile, InReferenceLineEditor) are the placement-framing mocks; the isolated stories cover swatch sizing, hover, selected affordance, and keyboard nav. Holding the consumer PRs (chart-editor wiring, number-tile drawer, reference-line editor) until design sign-off.

Tier 3, not Tier 2. Predicted by pr-triage-classify.js at 840 production lines (Tier 2 max: 250; Tier 3 escalation: 1000). The earlier plan estimated Tier 2 under the assumption that .stories.tsx files are excluded from the prod-line count; they're not (.test.tsx and __tests__/ are). I trimmed the in-context stories to keep the component placement-framing intact without growing into Tier 4. If you'd prefer the in-context stories split into a separate follow-up PR, I'm happy to do that.

No changeset. Foundation PR: the component is not wired into any user-visible UI yet (consumers land in follow-up PRs in the same series). The first consumer PR will carry the changeset.

[no-changeset: allow]
[ui-check: allow]

Test plan

  • npx tsc --noEmit clean in packages/app
  • npx eslint --quiet clean on all changed files
  • Local prose-lint clean
  • npx jest src/components/ColorSwatchInput.test.tsx --no-coverage (13/13 pass)
  • Full app unit suite (1599 / 1599 unrelated tests pass; one ENOMEM during link in deltaChartUtils.test.ts cleared on retry)
  • npx storybook build succeeds; storybook-static renders the picker
  • Local tier predictor reports Tier 3 against origin/main
  • Knip clean for changed files (the resolveChartColor helper that's actually used by the next PR ships in that PR alongside its consumer; ColorSwatchInputProps kept internal since callers infer the prop shape from typeof ColorSwatchInput).
  • (pending after this PR opens) Loom walkthrough + four-theme screenshots attached to PR body; @elizabetdev tagged

Resolves part of #1360.

PR-1 of the issue #1360 series. The existing ColorSwatchInput
(orphan in OSS since #440) used a free-form ColorInput plus a
hand-picked 9-value hex array. Refactor it to a palette-only
picker driven by the unified 13-token palette landed in #1627:
ten categorical tokens (chart-1..chart-10) and three semantic
tokens (chart-success, chart-warning, chart-error).

Storing tokens (not hex) lets user color choices reflow across
themes and color modes without losing contrast or breaking the
WCAG ratios baked into the palette.

The component remains internal in this PR. PRs 2b through 6b
in the issue #1360 series wire it into ChartSeriesEditor,
ChartDisplaySettingsDrawer, and the number-tile thresholds /
reference-line editors. PR 7 mirrors the schema fields on the
external dashboards API. PR 8 documents the feature.

Highlights:
- New ChartPaletteToken type + CHART_PALETTE_TOKENS,
  CATEGORICAL_PALETTE_TOKENS, SEMANTIC_PALETTE_TOKENS arrays,
  isChartPaletteToken type guard in utils.ts.
- New getColorFromCSSToken(token) reads the matching
  --color-chart-* CSS variable, with SSR-safe fallbacks.
- New resolveChartColor(token, fallbackIndex, level) for PR 2b
  to plug into setLineColors / formatResponseForPieChart.
- ColorSwatchInput popover split into categorical / semantic
  sections; trigger uses --color-outline-focus for focus-visible;
  swatch buttons use aria-pressed; legacy non-token values are
  guarded to treat as unset.
- Storybook stories matrix: six isolated stories (picker
  mechanics) and five in-context mocks (InSeriesRow,
  InLineChart, InStackedBar, InNumberTile,
  InReferenceLineEditor). The in-context stories are
  storybook-only mocks; they do not import the real editor
  components.
- 13 RTL tests cover the rejection rules and keyboard nav.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 12, 2026

⚠️ No Changeset found

Latest commit: cccdcb0

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link
Copy Markdown

vercel Bot commented May 12, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment May 12, 2026 10:38pm

Request Review

resolveChartColor and ColorSwatchInputProps had no consumers on
this branch and tripped the knip CI check. Drop resolveChartColor
entirely (the next PR in the issue #1360 series adds it back
alongside its first consumer in ChartUtils.setLineColors), and
make ColorSwatchInputProps an internal type alias (consumers
infer the prop shape from typeof ColorSwatchInput).
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 12, 2026

E2E Test Results

All tests passed • 174 passed • 3 skipped • 1276s

Status Count
✅ Passed 174
❌ Failed 0
⚠️ Flaky 5
⏭️ Skipped 3

Tests ran across 4 shards in parallel.

View full report →

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant