Skip to content

Commit fafe4f6

Browse files
refactor(ColorPairingTool): extract components, use CDS tokens, and improve 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
1 parent 3c3170b commit fafe4f6

File tree

16 files changed

+436
-684
lines changed

16 files changed

+436
-684
lines changed

apps/docs/src/components/page/ColorPairingTool/ColorPairingTool.module.css

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
11
@media (max-width: 767px) {
2-
.exportLabelFull {
3-
display: none;
4-
}
5-
62
.toolbarRow {
73
position: relative;
84
}

apps/docs/src/components/page/ColorPairingTool/ColorPicker.module.css

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,3 @@
3030
0 0 0 1px rgba(0, 0, 0, 0.2),
3131
0 1px 4px rgba(0, 0, 0, 0.3);
3232
}
33-
34-
@media (max-width: 767px) {
35-
.pickerRow {
36-
flex-direction: column !important;
37-
}
38-
}

apps/docs/src/components/page/ColorPairingTool/ColorPicker.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
import React, { memo,useCallback, useRef, useState } from 'react';
1+
import React, { memo, useCallback, useRef, useState } from 'react';
22
import type { HsvColor } from 'react-colorful';
33
import { HsvColorPicker } from 'react-colorful';
44
import { Button } from '@coinbase/cds-web/buttons';
55
import { TextInput } from '@coinbase/cds-web/controls';
66
import { Icon } from '@coinbase/cds-web/icons';
7-
import { Box, HStack,VStack } from '@coinbase/cds-web/layout';
7+
import { Box, HStack, VStack } from '@coinbase/cds-web/layout';
88
import { Text } from '@coinbase/cds-web/typography';
99

1010
import styles from './ColorPicker.module.css';
11-
import { hsbToRgb, parseColorInput,rgbToHsb, toHex } from './colorUtils';
11+
import { hsbToRgb, parseColorInput, rgbToHsb, toHex } from './colorUtils';
1212

1313
type ColorPickerProps = {
1414
onApply: (inputValue: string) => void;
@@ -98,7 +98,11 @@ export const ColorPicker = memo(function ColorPicker({ onApply }: ColorPickerPro
9898
return (
9999
<VStack gap={1.5}>
100100
<Text font="headline">Or enter a color value</Text>
101-
<HStack alignItems="stretch" className={styles.pickerRow} gap={3}>
101+
<HStack
102+
alignItems="stretch"
103+
flexDirection={{ phone: 'column', tablet: 'row', desktop: 'row' }}
104+
gap={3}
105+
>
102106
{/* Left: react-colorful HSV picker */}
103107
<div className={styles.pickerWrapper}>
104108
<HsvColorPicker color={hsv} onChange={handlePickerChange} />

apps/docs/src/components/page/ColorPairingTool/ComponentPlayground.tsx

Lines changed: 13 additions & 207 deletions
Original file line numberDiff line numberDiff line change
@@ -1,218 +1,27 @@
1-
import React, { memo, useEffect, useState } from 'react';
1+
import React, { memo, useState } from 'react';
22
import type { ThemeVars } from '@coinbase/cds-common/core/theme';
3-
import { useTheme } from '@coinbase/cds-web';
4-
import { Button, IconButton } from '@coinbase/cds-web/buttons';
5-
import { Card, MessagingCard } from '@coinbase/cds-web/cards';
3+
import type { TabValue } from '@coinbase/cds-common/tabs/useTabs';
64
import { Box, HStack, VStack } from '@coinbase/cds-web/layout';
7-
import { Interactable } from '@coinbase/cds-web/system';
85
import { ThemeProvider } from '@coinbase/cds-web/system/ThemeProvider';
96
import { SegmentedTabs } from '@coinbase/cds-web/tabs';
10-
import { Tag } from '@coinbase/cds-web/tag';
117
import { Text } from '@coinbase/cds-web/typography';
12-
import { LineChart, Scrubber, SolidLine } from '@coinbase/cds-web-visualization';
138
import { useColorMode } from '@docusaurus/theme-common';
149

1510
import { useDocsTheme } from '../../../theme/Layout/Provider/UnifiedThemeContext';
1611

17-
import { aaTextColor } from './colorUtils';
12+
import { PlaygroundContent } from './PlaygroundContent';
1813
import styles from './ResultCard.module.css';
19-
import { darkSpectrum,lightSpectrum } from './tokens';
2014

21-
const PLAYGROUND_TABS = [
22-
{ id: 'light' as const, label: 'Light' },
23-
{ id: 'dark' as const, label: 'Dark' },
15+
const PLAYGROUND_TABS: TabValue[] = [
16+
{ id: 'light', label: 'Light' },
17+
{ id: 'dark', label: 'Dark' },
2418
];
2519

26-
const CHART_DATA_PRIMARY = [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58];
27-
const CHART_DATA_SECONDARY = [5, 18, 35, 28, 55, 70, 48, 62, 38, 15, 42, 55, 30, 45];
28-
29-
// ── PlaygroundContent ──────────────────────────────────────────────────────────
30-
// Must be rendered *inside* the ThemeProvider so useTheme() reads the nested
31-
// theme and resolves spectrum tokens to actual RGB values for the selected mode.
32-
33-
type PlaygroundContentProps = {
34-
selectedToken: string;
35-
selectedHex: string;
36-
selectedMode: 'light' | 'dark';
37-
imgSrc: string | null;
38-
};
39-
40-
const PlaygroundContent = memo(function PlaygroundContent({
41-
selectedToken,
42-
selectedHex,
43-
selectedMode,
44-
imgSrc,
45-
}: PlaygroundContentProps) {
46-
const theme = useTheme();
47-
48-
// theme.spectrum[token] returns a comma-separated "r,g,b" string for the active
49-
// color scheme. Wrap it with rgb() to produce a valid CSS color value.
50-
const spectrumRgb = theme.spectrum[selectedToken as ThemeVars.SpectrumColor];
51-
const pColor = spectrumRgb ? `rgb(${spectrumRgb})` : selectedHex;
52-
53-
const pText = aaTextColor(selectedHex);
54-
const pButtonBg = pText;
55-
const pButtonText = aaTextColor(pButtonBg);
56-
57-
const spectrum = selectedMode === 'light' ? lightSpectrum : darkSpectrum;
58-
const checkerSvg = `data:image/svg+xml,${encodeURIComponent(
59-
`<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>`,
60-
)}`;
61-
62-
return (
63-
<Box
64-
background="bgAlternate"
65-
borderRadius={200}
66-
padding={3}
67-
style={{ transition: 'background 0.2s ease' }}
68-
>
69-
<VStack gap={3} width="100%">
70-
<HStack alignItems="center" flexWrap="wrap" gap={2}>
71-
<Interactable
72-
as="button"
73-
blendStyles={{ background: pColor, borderColor: pColor }}
74-
borderRadius={1000}
75-
paddingX={3}
76-
paddingY={1}
77-
style={{ color: pText, border: 'none', fontWeight: 600, fontSize: 15 }}
78-
>
79-
Button
80-
</Interactable>
81-
<IconButton
82-
accessibilityLabel="Add"
83-
name="add"
84-
style={{ background: pColor, color: pText, borderColor: pColor }}
85-
/>
86-
<Tag
87-
emphasis="high"
88-
intent="promotional"
89-
style={{
90-
background: pColor,
91-
color: pText,
92-
['--cds-fg' as string]: pText,
93-
['--cds-fgPrimary' as string]: pText,
94-
}}
95-
>
96-
<span style={{ color: pText }}>Promo Tag</span>
97-
</Tag>
98-
</HStack>
99-
100-
<HStack
101-
className={styles.cardsRow}
102-
gap={2}
103-
style={{ alignItems: 'stretch', height: 200 }}
104-
width="100%"
105-
>
106-
<Box className={styles.cardBox} display="flex" style={{ flex: '1 1 0', minWidth: 0 }}>
107-
<MessagingCard
108-
action={
109-
<Button
110-
compact
111-
onClick={() => {}}
112-
style={{ background: pButtonBg, color: pButtonText, borderColor: pButtonBg }}
113-
variant="secondary"
114-
>
115-
<span className={styles.ctaLabelFull}>Learn more</span>
116-
<span className={styles.ctaLabelShort}>CTA</span>
117-
</Button>
118-
}
119-
description="Add up to 3 lines of body copy. Be concise."
120-
media={
121-
<img
122-
alt=""
123-
src={imgSrc || checkerSvg}
124-
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
125-
/>
126-
}
127-
mediaPlacement="end"
128-
styles={{
129-
root: {
130-
height: '100%',
131-
background: pColor,
132-
['--color-fgInverse' as string]: pText,
133-
['--color-bgPrimary' as string]: pColor,
134-
},
135-
contentContainer: {
136-
justifyContent: 'space-between',
137-
flex: '1 1 0',
138-
minWidth: 0,
139-
},
140-
mediaContainer: { maxWidth: '45%' },
141-
}}
142-
title="Title"
143-
type="upsell"
144-
width="100%"
145-
/>
146-
</Box>
147-
148-
<Box className={styles.cardBox} display="flex" style={{ flex: '1 1 0', minWidth: 0 }}>
149-
<Card
150-
background="bgSecondary"
151-
borderRadius={500}
152-
style={{ height: '100%', width: '100%' }}
153-
>
154-
<VStack gap={1}>
155-
<Box padding={2} paddingBottom={0}>
156-
<HStack alignItems="center" gap={1}>
157-
<Box
158-
borderRadius={1000}
159-
height={36}
160-
style={{ background: pColor, flexShrink: 0, overflow: 'hidden' }}
161-
width={36}
162-
>
163-
{imgSrc && (
164-
<img
165-
alt=""
166-
src={imgSrc}
167-
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
168-
/>
169-
)}
170-
</Box>
171-
<VStack gap={0}>
172-
<Text color="fgMuted" font="legal">
173-
Subtitle
174-
</Text>
175-
<HStack alignItems="center" gap={0.5}>
176-
<Text color="fg" font="headline">
177-
Title
178-
</Text>
179-
<Text color="fgPositive" font="label2">
180-
↑ 25.25%
181-
</Text>
182-
</HStack>
183-
</VStack>
184-
</HStack>
185-
</Box>
186-
<LineChart
187-
enableScrubbing
188-
height={110}
189-
series={[
190-
{ id: 'primary', data: CHART_DATA_PRIMARY, color: pColor },
191-
{ id: 'secondary', data: CHART_DATA_SECONDARY, color: theme.color['fg'] },
192-
]}
193-
xAxis={{ range: ({ min, max }) => ({ min, max: max - 8 }) }}
194-
>
195-
<Scrubber hideOverlay idlePulse LineComponent={SolidLine} />
196-
</LineChart>
197-
</VStack>
198-
</Card>
199-
</Box>
200-
</HStack>
201-
</VStack>
202-
</Box>
203-
);
204-
});
205-
206-
// ── ComponentPlayground ────────────────────────────────────────────────────────
207-
// Manages the light/dark tab state. Renders the header+tabs outside ThemeProvider
208-
// (so they stay in the docs color mode), then wraps PlaygroundContent in a
209-
// ThemeProvider scoped to the selected mode.
210-
21120
type ComponentPlaygroundProps = {
21221
/** CDS spectrum token name for the light-mode brand color (e.g. "blue40"). */
213-
lightToken: string;
22+
lightToken: ThemeVars.SpectrumColor;
21423
/** CDS spectrum token name for the dark-mode brand color (e.g. "blue50"). */
215-
darkToken: string;
24+
darkToken: ThemeVars.SpectrumColor;
21625
/** Hex value of lightToken — used for contrast ratio calculation only. */
21726
lightHex: string;
21827
/** Hex value of darkToken — used for contrast ratio calculation only. */
@@ -234,18 +43,19 @@ export const ComponentPlayground = memo(function ComponentPlayground({
23443
colorMode === 'light' ? 'light' : 'dark',
23544
);
23645

237-
// Sync the playground tab when the global docs color mode changes
238-
useEffect(() => {
46+
const [prevColorMode, setPrevColorMode] = useState(colorMode);
47+
if (prevColorMode !== colorMode) {
48+
setPrevColorMode(colorMode);
23949
setSelectedMode(colorMode === 'light' ? 'light' : 'dark');
240-
}, [colorMode]);
50+
}
24151

24252
const activeTab = PLAYGROUND_TABS.find((t) => t.id === selectedMode) ?? PLAYGROUND_TABS[0];
24353

24454
const selectedToken = selectedMode === 'light' ? lightToken : darkToken;
24555
const selectedHex = selectedMode === 'light' ? lightHex : darkHex;
24656

24757
return (
248-
<Box style={{ padding: '24px 32px 32px' }} width="100%">
58+
<Box padding={4} paddingTop={3} width="100%">
24959
<VStack gap={2} width="100%">
25060
<HStack
25161
alignItems="center"
@@ -256,7 +66,6 @@ export const ComponentPlayground = memo(function ComponentPlayground({
25666
<Text as="h3" font="title3">
25767
Color match to components
25868
</Text>
259-
{/* SegmentedTabs outside ThemeProvider — stays in the docs color mode */}
26069
<SegmentedTabs
26170
accessibilityLabel="Switch light or dark mode preview"
26271
activeTab={activeTab}
@@ -267,9 +76,6 @@ export const ComponentPlayground = memo(function ComponentPlayground({
26776
/>
26877
</HStack>
26978

270-
{/* ThemeProvider scopes the selected color scheme to PlaygroundContent only.
271-
PlaygroundContent calls useTheme() inside this boundary to resolve
272-
spectrum tokens to actual rgb() values for the active mode. */}
27379
<ThemeProvider activeColorScheme={selectedMode} display="contents" theme={theme}>
27480
<PlaygroundContent
27581
imgSrc={imgSrc}

apps/docs/src/components/page/ColorPairingTool/ContrastPanel.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import React, { memo } from 'react';
22
import { Icon } from '@coinbase/cds-web/icons';
3-
import { Box, HStack,VStack } from '@coinbase/cds-web/layout';
3+
import { Box, HStack, VStack } from '@coinbase/cds-web/layout';
44
import { Text } from '@coinbase/cds-web/typography';
55

6-
import { aaTextColor,contrastRatio, wcagLevels } from './colorUtils';
6+
import { aaTextColor, contrastRatio, wcagLevels } from './colorUtils';
77
import styles from './ResultCard.module.css';
8+
import { darkSpectrum, lightSpectrum } from './tokens';
89
import { WcagBadge } from './WcagBadge';
910

10-
const LIGHT_BG = '#FFFFFF';
11-
const DARK_BG = '#141519';
11+
const LIGHT_BG = `rgb(${lightSpectrum.gray0})`;
12+
const DARK_BG = `rgb(${darkSpectrum.gray5})`;
1213

1314
type ContrastRowProps = {
1415
label: string;

0 commit comments

Comments
 (0)