Conversation
🟡 Heimdall Review Status
🟡
|
| Code Owner | Status | Calculation | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| ui-systems-eng-team |
🟡
0/1
|
Denominator calculation
|
15d5c80 to
1f1f05e
Compare
hcopp
left a comment
There was a problem hiding this comment.
Love the idea! Hopefully this can help you talk with Cursor about how to clean things up.
Also if you could get sample screenshots, description of your intended changes, and a link to the figma in the PR description that would be very helpful!
apps/docs/static/img/campaignCardBanners/color-pairing-tool_dark.png
Outdated
Show resolved
Hide resolved
apps/docs/src/components/page/ColorPairingTool/ComponentPlayground.tsx
Outdated
Show resolved
Hide resolved
apps/docs/src/components/page/ColorPairingTool/ComponentPlayground.tsx
Outdated
Show resolved
Hide resolved
apps/docs/src/components/page/ColorPairingTool/ContrastPanel.tsx
Outdated
Show resolved
Hide resolved
apps/docs/src/components/page/ColorPairingTool/HotspotImagePreview.tsx
Outdated
Show resolved
Hide resolved
| CDS layout components use inline styles generated from their style props, | ||
| which have higher specificity than CSS class rules. !important is required | ||
| here to override those inline styles at mobile breakpoints. | ||
| ────────────────────────────────────────────────────────────────────────── */ |
There was a problem hiding this comment.
We shouldn't need to use style props, but instead use our regular component props which support responsive values.
There was a problem hiding this comment.
cursor:
Here's a comment you can post:
Acknowledged — the !important overrides in ResultCard.module.css exist because CDS layout components (HStack, VStack, Box) apply style props as inline styles, which have higher specificity than CSS class rules. The fix is to replace these with responsive CDS component props (e.g. flexDirection={{ base: 'column', tablet: 'row' }}, width={{ base: '100%', tablet: '50%' }}).
We've already done this for ColorPicker.module.css (removed the .pickerRow !important override in this round). The remaining overrides in ResultCard.module.css touch layout across ResultCard, ComponentPlayground, and ContrastPanel simultaneously and need visual QA at both desktop and mobile breakpoints. Deferring to a dedicated follow-up PR.
|
@nicoledbelcher are you able to fill out the PR description? This is done in GitHub UI manually. You can look at other PRs to see what we normally do to fill it out.
Also your commits are unverified, you should follow https://docs.github.com/en/authentication/managing-commit-signature-verification/adding-a-gpg-key-to-your-github-account. This is a requirement to merge in code. Lastly, can you pull in the latest from master? Cursor can help with this. |
Adds ColorPairingTool to docs site, color-pairing utilities and components to vite-app, cds-finder app, guidelines documentation, and various supporting updates. Made-with: Cursor
…heming - Remove Color matching / Color pairs tab strip; single-flow layout - Scope LineChart scrubber overlay/line CSS vars to chart card for light playground - Hue-aware primitive matching and related adjustments Made-with: Cursor
…ixes - Checkerboard thumbnails in UploadZone now respect light/dark color mode - Light mode thumb borders use subtle dark borders instead of invisible white - Restore top padding on contrast panel for proper spacing on mobile - Increase mobile image preview height to 300px for balanced blur padding - Fix hotspot drifting off image on mobile by removing align-items stretch - Fix cards row clipping LineChart on mobile by overriding fixed height - Strip trailing commas in color input instead of creating phantom results - Remove red error outline when typing a trailing comma - Remove token tag next to "Color match to components" heading Made-with: Cursor
- Move reducer/initialState into parent component (index.tsx), rename state.ts to types.ts for shared type definitions only - Remove sharedStyles.ts (CoinbaseMono font not available in docs) - Move hotspot label inline styles to ResultCard.module.css - Use CDS Box props instead of inline styles in ContrastPanel - Replace checker-placeholder.png with theme-aware inline SVG - Replace banner PNGs with SVGs for light/dark modes - Simplify LineChart height prop (remove unnecessary responsive object) - Hide MetadataLinks on pages with no metadata (e.g. playground) - Run docs:lint --fix (all files pass) Note: the style props → responsive CDS props refactor (removing !important overrides in ResultCard.module.css) is deferred to a follow-up. It touches the responsive layout across multiple components and needs careful visual QA at both desktop and mobile breakpoints. Made-with: Cursor
…mprove playground - Extract PlaygroundContent into its own file for cleaner separation - Extract generic FileDropZone component and useFileUpload hook for reuse - Replace hardcoded spectrum values and types in tokens.ts with CDS theme imports - Remove unused CSS modules and useImageUpload hook - Simplify ContrastPanel and WcagBadge to use CDS spacing props - Update LineChart card background to use "bg" token - Add hideLlmLink option to MetadataLinks component - Fix ContentHeader bannerHeight JSDoc format Made-with: Cursor
fafe4f6 to
2c12130
Compare
| toast.show('Failed to copy to clipboard'); | ||
| } | ||
| }, [llmDocUrl, toast]); | ||
|
|
There was a problem hiding this comment.
Can we add this back? And the comment above and toast
| /** URL to Figma */ | ||
| figma?: string; | ||
| /** Hide the "View as Markdown" link */ | ||
| hideLlmLink?: boolean; |
There was a problem hiding this comment.
nit: can we rename this to hideLlmLinks and have it hide both "View as Markdown" and "Copy for LLM"?
|
|
||
| /** | ||
| * Displays metadata links (Source, Storybook, Changelog, Figma) and LLM doc buttons. | ||
| * Displays metadata links (Source, Storybook, Changelog, Figma) and View as Markdown. |
There was a problem hiding this comment.
nit: can you undo this change
| source={source} | ||
| storybook={storybook} | ||
| /> | ||
| )} |
There was a problem hiding this comment.
Can we get rid of this conditional? This will hide the buttons for all pages that don't have any of these four links.
Instead we should start passing in the logic to conditionally hide the buttons as well.
If needed, the logic inside of MetadataLinks can return nothing if needed - to prevent extra padding at the bottom.
|
|
||
| export type TokenFamily = ThemeVars.SpectrumHue; | ||
| export type TokenStep = ThemeVars.SpectrumHueStep; | ||
| export type ColorToken = ThemeVars.SpectrumColor; |
There was a problem hiding this comment.
nit: we shouldn't need to re-export TokenFamily, TokenStep, ColorToken, lightSpectrum, and darkSpectrum. Files importing from here should instead import from useTheme(), or, if unable, import from defaultTheme.
| Subtitle | ||
| </Text> | ||
| <HStack alignItems="center" gap={0.5}> | ||
| <Text color="fg" font="headline"> |
There was a problem hiding this comment.
nit: shouldn't need to set "fg" here, that is the default color.
| style={{ height: '100%', width: '100%' }} | ||
| > | ||
| <VStack gap={1}> | ||
| <Box padding={2} paddingBottom={0}> |
There was a problem hiding this comment.
nit: don't need paddingBottom={0}, that is the default.
| <HStack | ||
| className={styles.cardsRow} | ||
| gap={2} | ||
| style={{ alignItems: 'stretch', height: 200 }} |
There was a problem hiding this comment.
We can set
<HStack
className={styles.cardsRow}
gap={2}
alignItems="stretch"
height={200}
| style={{ color: pText, border: 'none', fontWeight: 600, fontSize: 15 }} | ||
| > | ||
| Button | ||
| </Interactable> |
There was a problem hiding this comment.
I think we should be using here, setting style={{ backgroundColor: 'X' }} if needed, unless there is a very specific reason.
| const spectrum = selectedMode === 'light' ? lightSpectrum : darkSpectrum; | ||
| const checkerSvg = `data:image/svg+xml,${encodeURIComponent( | ||
| `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect width="20" height="20" fill="rgb(${spectrum.gray15})"/><rect x="20" y="20" width="20" height="20" fill="rgb(${spectrum.gray15})"/><rect x="20" width="20" height="20" fill="rgb(${spectrum.gray10})"/><rect y="20" width="20" height="20" fill="rgb(${spectrum.gray10})"/></svg>`, | ||
| )}`; |
There was a problem hiding this comment.
This should be a separate .svg file, like we have for color-pairing-tool.svg - it can still use theme variables since these are set as css variables accessible in dom.
| error: null, | ||
| }; | ||
|
|
||
| function reducer(state: AppState, action: Action): AppState { |
There was a problem hiding this comment.
We shouldn't be following a redux style state management.
Cursor should rework this to use regular state management, meaning we have a root component which has the main state, then inside of the upload file it manages drag enter/drag leave, etc and then it calls to colorpairingtool when files have been added.
Then the parent can manage loading and then sending to results state where that new component showing state can manage carousel progress, contrast interaction, etc.
| imgSrc: string | null; | ||
| imgDataURL: string | null; | ||
| imgDispW: number; | ||
| imgDispH: number; |
There was a problem hiding this comment.
nit: can this be imgWidth and imgHeight? feels much easier to read.
| export type ResultEntry = { | ||
| filename: string; | ||
| imgSrc: string | null; | ||
| imgDataURL: string | null; |
There was a problem hiding this comment.
Normally we prefer undefined over null values, across all types
| box-shadow: | ||
| 0 0 0 1px rgba(0, 0, 0, 0.2), | ||
| 0 1px 4px rgba(0, 0, 0, 0.3); | ||
| } |
There was a problem hiding this comment.
We should replace these with linaria css or ideally props directly on primitives. We can use Polymorphic components to use a Box and even set it to a span or whatever component is needed.
| border: '1px solid rgba(0,0,0,0.06)', | ||
| flexShrink: 0, | ||
| padding: 8, | ||
| }} |
There was a problem hiding this comment.
Nit: we should use bordered and use inline props such as padding={1}. Box is flex by default so won't need that here or anywhere else

What changed? Why?
Added a new sidebar to the docs site called extras and created a new color pairing tool inside it.
What it is
The Color Pairing Tool is an internal design utility built into the CDS documentation site. It helps designers and engineers find the closest CDS spectrum primitives for any color and automatically generates accessible, theme-aware color pairings that work across light and dark modes.
How it works
Image upload — Users can upload up to 10 images (PNG, JPG, or WebP) via file picker or drag-and-drop. The tool extracts dominant colors from each image using a k-means clustering algorithm run client-side on an .
Manual hex entry — Users can type one or more comma-separated hex codes directly into a text input.
2. Token Matching
For each extracted or entered color, the tool runs a hue-aware matching algorithm against the full CDS spectrum (both light and dark). This isn't a simple "nearest Euclidean distance" match — it's purpose-built for CDS tokens and factors in perceptual hue so the matched primitive feels like a natural fit, not just the mathematically closest one.
Once a primary background token is matched, the tool:
Selects a foreground text color that meets WCAG AA contrast requirements (minimum 4.5:1 for normal text).
Computes a secondary/complementary token pairing.
Offers a high-contrast mode toggle that enforces even stricter contrast ratios.
Displays live WCAG contrast ratios (AA Normal, AA Large, AAA) for every pairing.
4. Component Preview
The results feed into a Component Playground that renders real CDS components — Card, Button, MessagingCard, LineChart, ProgressBar, and more — using the matched token pairings. This gives an immediate preview of how the colors will look in a production UI across both light and dark themes.
For image uploads, users can drag a hotspot around the image to resample the dominant color from a different region, which recalculates the token match and all downstream pairings in real time.
Results can be exported as structured JSON containing the matched tokens, hex values, text colors, and button state variants for both light and dark modes — ready to hand off to engineering.
Key technical details
Runs entirely client-side — no server calls. All color math, image processing, and token matching happen in the browser.
Uses CDS design tokens as the source of truth (imported from tokens.ts), so matches always stay in sync with the design system.
Theme-aware throughout — every visual (checkerboards, previews, component playground) adapts to the user's current light/dark mode setting.
UI changes
Multiple outputs carousel:

Testing
You should test selecting a color from the color picker without uploading an image, and then test uploading one or multiple images. Users can only upload, PNG, JGP and WEBP files, no video/animated gif files allowed.
How has it been tested?
Testing instructions
Illustrations/Icons Checklist
Required if this PR changes files under
packages/illustrations/**orpackages/icons/**Change management
type=routine
risk=low
impact=sev5
automerge=false