Skip to content

Commit 65f8278

Browse files
committed
feat: ability to copy colors in v4 docs
1 parent b6f0956 commit 65f8278

File tree

4 files changed

+146
-45
lines changed

4 files changed

+146
-45
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"@tailwindcss/postcss": "4.0.0",
1818
"@types/mdx": "^2.0.13",
1919
"clsx": "^2.1.1",
20+
"colorizr": "^3.0.7",
2021
"dedent": "^1.5.3",
2122
"fathom-client": "^3.7.2",
2223
"feed": "^4.2.2",

pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/color-palette.tsx

Lines changed: 49 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,60 +3,64 @@ 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+
const styles = await fs.readFile(
12+
path.join(__dirname, "../../node_modules/tailwindcss/theme.css"),
13+
"utf-8",
14+
);
1115

12-
let colors: Record<string, Record<string, string>> = {};
13-
for (let line of styles.split("\n")) {
14-
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+
const colors: Record<string, Record<string, string>> = {};
17+
for (const line of styles.split("\n")) {
18+
if (line.startsWith(" --color-")) {
19+
const [key, value] = line
20+
.split(":")
21+
.map((part) => part.trim().replace(";", ""));
22+
const match = key.match(/^--color-([a-z]+)-(\d+)$/);
1723

18-
if (match) {
19-
const [, group, shade] = match;
24+
if (match) {
25+
const [, group, shade] = match;
2026

21-
if (!colors[group]) {
22-
colors[group] = {};
23-
}
27+
if (!colors[group]) {
28+
colors[group] = {};
29+
}
2430

25-
colors[group][shade] = value;
26-
}
27-
}
31+
colors[group][shade] = value;
32+
}
33+
}
2834
}
2935

3036
export function ColorPalette() {
31-
return (
32-
<div className="not-prose grid grid-cols-[auto_minmax(0,_1fr)] items-center gap-4">
33-
<div className="col-start-2 grid grid-cols-11 justify-items-center gap-1.5 font-medium text-gray-950 *:rotate-180 *:[writing-mode:vertical-lr] sm:gap-4 sm:*:rotate-0 sm:*:[writing-mode:horizontal-tb] dark:text-white">
34-
<div>50</div>
35-
<div>100</div>
36-
<div>200</div>
37-
<div>300</div>
38-
<div>400</div>
39-
<div>500</div>
40-
<div>600</div>
41-
<div>700</div>
42-
<div>800</div>
43-
<div>900</div>
44-
<div>950</div>
45-
</div>
46-
{Object.entries(colors).map(([key, shades]) => (
47-
<React.Fragment key={key}>
48-
<p className="font-medium text-gray-950 capitalize sm:pr-12 dark:text-white">{key}</p>
49-
<div className="grid grid-cols-11 gap-1.5 sm:gap-4">
50-
{Object.keys(shades).map((shade, i) => (
51-
<div
52-
key={i}
53-
style={{ backgroundColor: `var(--color-${key}-${shade})` }}
54-
className="aspect-1/1 rounded-sm outline -outline-offset-1 outline-black/10 sm:rounded-md dark:outline-white/10"
55-
/>
56-
))}
57-
</div>
58-
</React.Fragment>
59-
))}
60-
</div>
61-
);
37+
return (
38+
<div className="not-prose grid grid-cols-[auto_minmax(0,_1fr)] items-center gap-4">
39+
<div className="col-start-2 grid grid-cols-11 justify-items-center gap-1.5 font-medium text-gray-950 *:rotate-180 *:[writing-mode:vertical-lr] sm:gap-4 sm:*:rotate-0 sm:*:[writing-mode:horizontal-tb] dark:text-white">
40+
<div>50</div>
41+
<div>100</div>
42+
<div>200</div>
43+
<div>300</div>
44+
<div>400</div>
45+
<div>500</div>
46+
<div>600</div>
47+
<div>700</div>
48+
<div>800</div>
49+
<div>900</div>
50+
<div>950</div>
51+
</div>
52+
{Object.entries(colors).map(([key, shades]) => (
53+
<React.Fragment key={key}>
54+
<p className="font-medium text-gray-950 capitalize sm:pr-12 dark:text-white">
55+
{key}
56+
</p>
57+
<div className="grid grid-cols-11 gap-1.5 sm:gap-4">
58+
{Object.keys(shades).map((shade, i) => (
59+
<Color key={i} name={key} shade={shade} />
60+
))}
61+
</div>
62+
</React.Fragment>
63+
))}
64+
</div>
65+
);
6266
}

src/components/color.tsx

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"use client";
2+
3+
import { oklch2hex } from "colorizr";
4+
import { useEffect, useRef, useState } from "react";
5+
import clsx from "clsx";
6+
import {
7+
Button,
8+
Tooltip,
9+
TooltipPanel,
10+
TooltipTrigger,
11+
} from "@headlessui/react";
12+
13+
export function Color({ name, shade }: { name: string; shade: string }) {
14+
const [justCopied, setJustCopied] = useState(false);
15+
16+
const buttonRef = useRef<HTMLButtonElement>(null);
17+
18+
const colorVariableName = `--color-${name}-${shade}`;
19+
20+
const copyHexToClipboard = () => {
21+
if (!buttonRef.current) {
22+
return;
23+
}
24+
25+
const styleValue = buttonRef.current
26+
.computedStyleMap()
27+
.get(colorVariableName);
28+
29+
if (!styleValue) {
30+
return;
31+
}
32+
33+
const oklchWithCSSFunctionalNotation = styleValue.toString();
34+
35+
// oklch(0.808 0.114 19.571) to 0.808 0.114 19.571
36+
const oklch = oklchWithCSSFunctionalNotation.slice(6, -1);
37+
38+
// 0.808 0.114 19.571 to [0.808, 0.114, 19.571]
39+
const oklchTuple = oklch.split(" ").map(Number) as [number, number, number];
40+
41+
const hex = oklch2hex(oklchTuple);
42+
43+
navigator.clipboard.writeText(hex);
44+
45+
setJustCopied(true);
46+
};
47+
48+
useEffect(() => {
49+
const timeout = setTimeout(() => {
50+
if (!justCopied) {
51+
return;
52+
}
53+
54+
setJustCopied(false);
55+
}, 1300);
56+
57+
return () => clearTimeout(timeout);
58+
}, [justCopied]);
59+
60+
const whiteHasContrastAgainstShade = Number(shade) > 400;
61+
62+
return (
63+
<Tooltip as="div" showDelayMs={100} hideDelayMs={0} className="relative">
64+
<TooltipTrigger>
65+
<Button
66+
ref={buttonRef}
67+
type="button"
68+
onClick={copyHexToClipboard}
69+
onTouchEnd={copyHexToClipboard}
70+
style={{ backgroundColor: `var(${colorVariableName})` }}
71+
className={clsx(
72+
"h-full w-full cursor-pointer aspect-1/1 rounded-sm outline -outline-offset-1 outline-black/10 sm:rounded-md dark:outline-white/10 flex items-center justify-center",
73+
)}
74+
/>
75+
</TooltipTrigger>
76+
<TooltipPanel
77+
as="div"
78+
anchor="top"
79+
className="pointer-events-none z-10 translate-y-2 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"
80+
>
81+
{justCopied ? "Copied!" : "Click to copy"}
82+
</TooltipPanel>
83+
</Tooltip>
84+
);
85+
/*
86+
87+
*/
88+
}

0 commit comments

Comments
 (0)