Skip to content

Commit f6346c8

Browse files
ryanhaticusphilipp-spiess
andauthoredFeb 21, 2025··
feat: ability to copy colors in v4 docs (#1945)
- Enables the ability to copy colors in the v4 `/docs/colors` page. - Converts `oklch` to hex as it's more recognizable from a DX perspective. This should be okay because the color palette doesn't utilize values. Gif: ![click-to-copy](https://github.com/user-attachments/assets/4f0ce530-dfe7-40c7-9a0b-947afc558ba0) --------- Co-authored-by: Philipp Spiess <[email protected]>
1 parent 794640d commit f6346c8

File tree

2 files changed

+384
-9
lines changed

2 files changed

+384
-9
lines changed
 

‎src/components/color-palette.tsx

+6-9
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,21 @@ import path from "node:path";
33

44
import { fileURLToPath } from "node:url";
55
import React from "react";
6+
import { Color } from "./color";
67

78
const __filename = fileURLToPath(import.meta.url);
89
const __dirname = path.dirname(__filename);
910

10-
const styles = await fs.readFile(path.join(__dirname, "../../node_modules/tailwindcss/theme.css"), "utf-8");
11+
let styles = await fs.readFile(path.join(__dirname, "../../node_modules/tailwindcss/theme.css"), "utf-8");
1112

1213
let colors: Record<string, Record<string, string>> = {};
1314
for (let line of styles.split("\n")) {
1415
if (line.startsWith(" --color-")) {
15-
const [key, value] = line.split(":").map((part) => part.trim().replace(";", ""));
16-
const match = key.match(/^--color-([a-z]+)-(\d+)$/);
16+
let [key, value] = line.split(":").map((part) => part.trim().replace(";", ""));
17+
let match = key.match(/^--color-([a-z]+)-(\d+)$/);
1718

1819
if (match) {
19-
const [, group, shade] = match;
20+
let [, group, shade] = match;
2021

2122
if (!colors[group]) {
2223
colors[group] = {};
@@ -49,11 +50,7 @@ export function ColorPalette() {
4950
<p className="font-medium text-gray-950 capitalize sm:pr-12 dark:text-white">{key}</p>
5051
<div className="grid grid-cols-11 gap-1.5 sm:gap-4">
5152
{Object.keys(shades).map((shade, i) => (
52-
<div
53-
key={i}
54-
style={{ backgroundColor: `var(--color-${key}-${shade})` }}
55-
className="aspect-1/1 rounded-sm outline -outline-offset-1 outline-black/10 sm:rounded-md dark:outline-white/10"
56-
/>
53+
<Color key={i} name={key} shade={shade} value={shades[shade]} />
5754
))}
5855
</div>
5956
</React.Fragment>

‎src/components/color.tsx

+378
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,378 @@
1+
"use client";
2+
3+
import { useEffect, useRef, useState } from "react";
4+
import clsx from "clsx";
5+
import { Button, Tooltip, TooltipPanel, TooltipTrigger } from "@headlessui/react";
6+
7+
const hexColors = {
8+
slate: {
9+
50: "#f8fafc",
10+
100: "#f1f5f9",
11+
200: "#e2e8f0",
12+
300: "#cbd5e1",
13+
400: "#94a3b8",
14+
500: "#64748b",
15+
600: "#475569",
16+
700: "#334155",
17+
800: "#1e293b",
18+
900: "#0f172a",
19+
950: "#020617",
20+
},
21+
gray: {
22+
50: "#f9fafb",
23+
100: "#f3f4f6",
24+
200: "#e5e7eb",
25+
300: "#d1d5db",
26+
400: "#9ca3af",
27+
500: "#6b7280",
28+
600: "#4b5563",
29+
700: "#374151",
30+
800: "#1f2937",
31+
900: "#111827",
32+
950: "#030712",
33+
},
34+
zinc: {
35+
50: "#fafafa",
36+
100: "#f4f4f5",
37+
200: "#e4e4e7",
38+
300: "#d4d4d8",
39+
400: "#a1a1aa",
40+
500: "#71717a",
41+
600: "#52525b",
42+
700: "#3f3f46",
43+
800: "#27272a",
44+
900: "#18181b",
45+
950: "#09090b",
46+
},
47+
neutral: {
48+
50: "#fafafa",
49+
100: "#f5f5f5",
50+
200: "#e5e5e5",
51+
300: "#d4d4d4",
52+
400: "#a3a3a3",
53+
500: "#737373",
54+
600: "#525252",
55+
700: "#404040",
56+
800: "#262626",
57+
900: "#171717",
58+
950: "#0a0a0a",
59+
},
60+
stone: {
61+
50: "#fafaf9",
62+
100: "#f5f5f4",
63+
200: "#e7e5e4",
64+
300: "#d6d3d1",
65+
400: "#a8a29e",
66+
500: "#78716c",
67+
600: "#57534e",
68+
700: "#44403c",
69+
800: "#292524",
70+
900: "#1c1917",
71+
950: "#0c0a09",
72+
},
73+
red: {
74+
50: "#fef2f2",
75+
100: "#fee2e2",
76+
200: "#fecaca",
77+
300: "#fca5a5",
78+
400: "#f87171",
79+
500: "#ef4444",
80+
600: "#dc2626",
81+
700: "#b91c1c",
82+
800: "#991b1b",
83+
900: "#7f1d1d",
84+
950: "#450a0a",
85+
},
86+
orange: {
87+
50: "#fff7ed",
88+
100: "#ffedd5",
89+
200: "#fed7aa",
90+
300: "#fdba74",
91+
400: "#fb923c",
92+
500: "#f97316",
93+
600: "#ea580c",
94+
700: "#c2410c",
95+
800: "#9a3412",
96+
900: "#7c2d12",
97+
950: "#431407",
98+
},
99+
amber: {
100+
50: "#fffbeb",
101+
100: "#fef3c7",
102+
200: "#fde68a",
103+
300: "#fcd34d",
104+
400: "#fbbf24",
105+
500: "#f59e0b",
106+
600: "#d97706",
107+
700: "#b45309",
108+
800: "#92400e",
109+
900: "#78350f",
110+
950: "#451a03",
111+
},
112+
yellow: {
113+
50: "#fefce8",
114+
100: "#fef9c3",
115+
200: "#fef08a",
116+
300: "#fde047",
117+
400: "#facc15",
118+
500: "#eab308",
119+
600: "#ca8a04",
120+
700: "#a16207",
121+
800: "#854d0e",
122+
900: "#713f12",
123+
950: "#422006",
124+
},
125+
lime: {
126+
50: "#f7fee7",
127+
100: "#ecfccb",
128+
200: "#d9f99d",
129+
300: "#bef264",
130+
400: "#a3e635",
131+
500: "#84cc16",
132+
600: "#65a30d",
133+
700: "#4d7c0f",
134+
800: "#3f6212",
135+
900: "#365314",
136+
950: "#1a2e05",
137+
},
138+
green: {
139+
50: "#f0fdf4",
140+
100: "#dcfce7",
141+
200: "#bbf7d0",
142+
300: "#86efac",
143+
400: "#4ade80",
144+
500: "#22c55e",
145+
600: "#16a34a",
146+
700: "#15803d",
147+
800: "#166534",
148+
900: "#14532d",
149+
950: "#052e16",
150+
},
151+
emerald: {
152+
50: "#ecfdf5",
153+
100: "#d1fae5",
154+
200: "#a7f3d0",
155+
300: "#6ee7b7",
156+
400: "#34d399",
157+
500: "#10b981",
158+
600: "#059669",
159+
700: "#047857",
160+
800: "#065f46",
161+
900: "#064e3b",
162+
950: "#022c22",
163+
},
164+
teal: {
165+
50: "#f0fdfa",
166+
100: "#ccfbf1",
167+
200: "#99f6e4",
168+
300: "#5eead4",
169+
400: "#2dd4bf",
170+
500: "#14b8a6",
171+
600: "#0d9488",
172+
700: "#0f766e",
173+
800: "#115e59",
174+
900: "#134e4a",
175+
950: "#042f2e",
176+
},
177+
cyan: {
178+
50: "#ecfeff",
179+
100: "#cffafe",
180+
200: "#a5f3fc",
181+
300: "#67e8f9",
182+
400: "#22d3ee",
183+
500: "#06b6d4",
184+
600: "#0891b2",
185+
700: "#0e7490",
186+
800: "#155e75",
187+
900: "#164e63",
188+
950: "#083344",
189+
},
190+
sky: {
191+
50: "#f0f9ff",
192+
100: "#e0f2fe",
193+
200: "#bae6fd",
194+
300: "#7dd3fc",
195+
400: "#38bdf8",
196+
500: "#0ea5e9",
197+
600: "#0284c7",
198+
700: "#0369a1",
199+
800: "#075985",
200+
900: "#0c4a6e",
201+
950: "#082f49",
202+
},
203+
blue: {
204+
50: "#eff6ff",
205+
100: "#dbeafe",
206+
200: "#bfdbfe",
207+
300: "#93c5fd",
208+
400: "#60a5fa",
209+
500: "#3b82f6",
210+
600: "#2563eb",
211+
700: "#1d4ed8",
212+
800: "#1e40af",
213+
900: "#1e3a8a",
214+
950: "#172554",
215+
},
216+
indigo: {
217+
50: "#eef2ff",
218+
100: "#e0e7ff",
219+
200: "#c7d2fe",
220+
300: "#a5b4fc",
221+
400: "#818cf8",
222+
500: "#6366f1",
223+
600: "#4f46e5",
224+
700: "#4338ca",
225+
800: "#3730a3",
226+
900: "#312e81",
227+
950: "#1e1b4b",
228+
},
229+
violet: {
230+
50: "#f5f3ff",
231+
100: "#ede9fe",
232+
200: "#ddd6fe",
233+
300: "#c4b5fd",
234+
400: "#a78bfa",
235+
500: "#8b5cf6",
236+
600: "#7c3aed",
237+
700: "#6d28d9",
238+
800: "#5b21b6",
239+
900: "#4c1d95",
240+
950: "#2e1065",
241+
},
242+
purple: {
243+
50: "#faf5ff",
244+
100: "#f3e8ff",
245+
200: "#e9d5ff",
246+
300: "#d8b4fe",
247+
400: "#c084fc",
248+
500: "#a855f7",
249+
600: "#9333ea",
250+
700: "#7e22ce",
251+
800: "#6b21a8",
252+
900: "#581c87",
253+
950: "#3b0764",
254+
},
255+
fuchsia: {
256+
50: "#fdf4ff",
257+
100: "#fae8ff",
258+
200: "#f5d0fe",
259+
300: "#f0abfc",
260+
400: "#e879f9",
261+
500: "#d946ef",
262+
600: "#c026d3",
263+
700: "#a21caf",
264+
800: "#86198f",
265+
900: "#701a75",
266+
950: "#4a044e",
267+
},
268+
pink: {
269+
50: "#fdf2f8",
270+
100: "#fce7f3",
271+
200: "#fbcfe8",
272+
300: "#f9a8d4",
273+
400: "#f472b6",
274+
500: "#ec4899",
275+
600: "#db2777",
276+
700: "#be185d",
277+
800: "#9d174d",
278+
900: "#831843",
279+
950: "#500724",
280+
},
281+
rose: {
282+
50: "#fff1f2",
283+
100: "#ffe4e6",
284+
200: "#fecdd3",
285+
300: "#fda4af",
286+
400: "#fb7185",
287+
500: "#f43f5e",
288+
600: "#e11d48",
289+
700: "#be123c",
290+
800: "#9f1239",
291+
900: "#881337",
292+
950: "#4c0519",
293+
},
294+
} as any;
295+
296+
export function Color({ name, shade, value }: { name: string; shade: string; value: string }) {
297+
let useShift = useShiftKey();
298+
let panelRef = useRef<HTMLElement>(null);
299+
300+
let colorVariableName = `--color-${name}-${shade}`;
301+
let hexValue = hexColors[name]?.[shade];
302+
303+
function copyHexToClipboard(e: React.MouseEvent) {
304+
e.preventDefault();
305+
e.stopPropagation();
306+
307+
let panel = panelRef.current;
308+
if (!panel) return;
309+
310+
let prevValue = panel.innerHTML;
311+
if (e.shiftKey) {
312+
navigator.clipboard.writeText(hexColors[name][shade]);
313+
panel.innerHTML = "Copied hex value!";
314+
} else {
315+
navigator.clipboard.writeText(value);
316+
panel.innerHTML = "Copied to clipboard!";
317+
}
318+
setTimeout(() => {
319+
panel.innerHTML = prevValue;
320+
}, 1300);
321+
}
322+
323+
return (
324+
<Tooltip as="div" showDelayMs={100} hideDelayMs={0} className="contents">
325+
<TooltipTrigger>
326+
<Button
327+
type="button"
328+
onClick={copyHexToClipboard}
329+
style={{ backgroundColor: `var(${colorVariableName})` }}
330+
className={clsx(
331+
"aspect-1/1 w-full rounded-sm outline -outline-offset-1 outline-black/10 sm:rounded-md dark:outline-white/10",
332+
)}
333+
/>
334+
</TooltipTrigger>
335+
<TooltipPanel
336+
as="div"
337+
anchor="top"
338+
className="pointer-events-none z-10 flex translate-y-2 items-center gap-1 rounded-full border border-gray-950 bg-gray-950/90 py-0.5 pr-2 pb-1 pl-3 text-center font-mono text-xs/6 font-medium whitespace-nowrap text-white opacity-100 inset-ring inset-ring-white/10 transition-[opacity] starting:opacity-0"
339+
>
340+
<span
341+
ref={(panel) => {
342+
if (panel) panelRef.current = panel;
343+
}}
344+
>
345+
{useShift && hexValue ? hexValue : value}
346+
</span>
347+
</TooltipPanel>
348+
</Tooltip>
349+
);
350+
}
351+
352+
function useShiftKey(): boolean {
353+
let [isShiftPressed, setIsShiftPressed] = useState(false);
354+
355+
useEffect(() => {
356+
let handleKeyDown = (event: KeyboardEvent) => {
357+
if (event.key === "Shift") {
358+
setIsShiftPressed(true);
359+
}
360+
};
361+
362+
let handleKeyUp = (event: KeyboardEvent) => {
363+
if (event.key === "Shift") {
364+
setIsShiftPressed(false);
365+
}
366+
};
367+
368+
window.addEventListener("keydown", handleKeyDown);
369+
window.addEventListener("keyup", handleKeyUp);
370+
371+
return () => {
372+
window.removeEventListener("keydown", handleKeyDown);
373+
window.removeEventListener("keyup", handleKeyUp);
374+
};
375+
}, []);
376+
377+
return isShiftPressed;
378+
}

0 commit comments

Comments
 (0)
Please sign in to comment.