ruler-components 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/dist/index.d.mts +51 -0
  2. package/dist/index.d.ts +51 -0
  3. package/dist/index.js +534 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/index.mjs +498 -0
  6. package/dist/index.mjs.map +1 -0
  7. package/package.json +21 -2
  8. package/AGENTS.md +0 -9
  9. package/CLAUDE.md +0 -1
  10. package/app/docs/[slug]/page.tsx +0 -147
  11. package/app/docs/layout.tsx +0 -26
  12. package/app/docs/page.tsx +0 -148
  13. package/app/favicon.ico +0 -0
  14. package/app/globals.css +0 -168
  15. package/app/icon.png +0 -0
  16. package/app/layout.tsx +0 -90
  17. package/app/opengraph-image.tsx +0 -10
  18. package/app/page.tsx +0 -34
  19. package/app/twitter-image.tsx +0 -10
  20. package/components/docs/accordion-doc-page.tsx +0 -352
  21. package/components/docs/alert-doc-page.tsx +0 -293
  22. package/components/docs/allinput-doc-page.tsx +0 -183
  23. package/components/docs/avatar-doc-page.tsx +0 -424
  24. package/components/docs/badge-doc-page.tsx +0 -363
  25. package/components/docs/button-doc-page.tsx +0 -406
  26. package/components/docs/card-doc-page.tsx +0 -364
  27. package/components/docs/checkbox-doc-page.tsx +0 -277
  28. package/components/docs/colors-doc-page.tsx +0 -430
  29. package/components/docs/dialog-doc-page.tsx +0 -364
  30. package/components/docs/docs-footer.tsx +0 -11
  31. package/components/docs/docs-mobile-menu-bar.tsx +0 -22
  32. package/components/docs/docs-mobile-nav-context.tsx +0 -27
  33. package/components/docs/docs-page.tsx +0 -263
  34. package/components/docs/docs-search.tsx +0 -303
  35. package/components/docs/docs-sidebar.tsx +0 -165
  36. package/components/docs/docs-topbar.tsx +0 -32
  37. package/components/docs/dropdown-doc-page.tsx +0 -522
  38. package/components/docs/getting-started-doc-page.tsx +0 -205
  39. package/components/docs/label-doc-page.tsx +0 -173
  40. package/components/docs/loaders-doc-page.tsx +0 -244
  41. package/components/docs/menu-doc-page.tsx +0 -373
  42. package/components/docs/modal-doc-page.tsx +0 -436
  43. package/components/docs/next-steps.tsx +0 -53
  44. package/components/docs/pagination-doc-page.tsx +0 -316
  45. package/components/docs/progress-doc-page.tsx +0 -387
  46. package/components/docs/radio-doc-page.tsx +0 -270
  47. package/components/docs/skeleton-doc-page.tsx +0 -230
  48. package/components/docs/stepper-doc-page.tsx +0 -309
  49. package/components/docs/switch-doc-page.tsx +0 -264
  50. package/components/docs/table-doc-page.tsx +0 -689
  51. package/components/docs/tabs-doc-page.tsx +0 -359
  52. package/components/docs/theming-doc-page.tsx +0 -243
  53. package/components/docs/tooltip-doc-page.tsx +0 -357
  54. package/components/docs/usage-section.tsx +0 -21
  55. package/components/icons/discord.tsx +0 -9
  56. package/components/icons/github.tsx +0 -9
  57. package/components/icons/x.tsx +0 -9
  58. package/components/installation-section.tsx +0 -34
  59. package/components/landing/feature-highlights.tsx +0 -105
  60. package/components/landing/footer-strip.tsx +0 -45
  61. package/components/landing/hero-card.tsx +0 -14
  62. package/components/landing/hero-spotlight.tsx +0 -126
  63. package/components/landing/hero.tsx +0 -64
  64. package/components/landing/navbar.tsx +0 -44
  65. package/components/landing/quick-start-section.tsx +0 -59
  66. package/components/landing/site-footer.tsx +0 -55
  67. package/components/theme-provider.tsx +0 -60
  68. package/components/ui/accordion.tsx +0 -174
  69. package/components/ui/alert.tsx +0 -103
  70. package/components/ui/allinput.tsx +0 -210
  71. package/components/ui/avatar.tsx +0 -179
  72. package/components/ui/badge.tsx +0 -52
  73. package/components/ui/button.tsx +0 -75
  74. package/components/ui/card.tsx +0 -105
  75. package/components/ui/checkbox.tsx +0 -130
  76. package/components/ui/code-block.tsx +0 -60
  77. package/components/ui/data-table.tsx +0 -235
  78. package/components/ui/dialog.tsx +0 -173
  79. package/components/ui/dropdown.tsx +0 -78
  80. package/components/ui/feature-card.tsx +0 -38
  81. package/components/ui/input.tsx +0 -44
  82. package/components/ui/label.tsx +0 -110
  83. package/components/ui/loaders.tsx +0 -139
  84. package/components/ui/menu.tsx +0 -118
  85. package/components/ui/modal.tsx +0 -399
  86. package/components/ui/package-manager-tabs.tsx +0 -40
  87. package/components/ui/pagination.tsx +0 -200
  88. package/components/ui/progress.tsx +0 -134
  89. package/components/ui/radio.tsx +0 -262
  90. package/components/ui/skeleton.tsx +0 -102
  91. package/components/ui/steppers.tsx +0 -309
  92. package/components/ui/switch.tsx +0 -153
  93. package/components/ui/table.tsx +0 -58
  94. package/components/ui/tabs.tsx +0 -166
  95. package/components/ui/theme-toggle.tsx +0 -60
  96. package/components/ui/tooltip.tsx +0 -238
  97. package/data/color-palette.ts +0 -35
  98. package/data/docs-nav.ts +0 -84
  99. package/eslint.config.mjs +0 -18
  100. package/index.ts +0 -8
  101. package/lib/cn.ts +0 -4
  102. package/lib/color.ts +0 -85
  103. package/lib/og-image.tsx +0 -138
  104. package/lib/theme.ts +0 -33
  105. package/next.config.ts +0 -7
  106. package/postcss.config.mjs +0 -7
  107. package/public/dark.png +0 -0
  108. package/public/dark1.png +0 -0
  109. package/public/dark2.png +0 -0
  110. package/public/file.svg +0 -1
  111. package/public/globe.svg +0 -1
  112. package/public/light.png +0 -0
  113. package/public/light1.png +0 -0
  114. package/public/next.svg +0 -1
  115. package/public/vercel.svg +0 -1
  116. package/public/video/hero-demo.mp4 +0 -0
  117. package/public/window.svg +0 -1
  118. package/tsconfig.json +0 -34
@@ -1,430 +0,0 @@
1
- "use client";
2
-
3
- import { useEffect, useState } from "react";
4
- import { createPortal } from "react-dom";
5
- import { motion } from "framer-motion";
6
- import { Check, Copy, Palette } from "lucide-react";
7
- import { DocsPage, DocsPageHeader, DocsSection } from "@/components/docs/docs-page";
8
- import { Tooltip } from "@/components/ui/tooltip";
9
- import { COLOR_PALETTE } from "@/data/color-palette";
10
- import {
11
- contrastLabel,
12
- formatColor,
13
- getContrastRatio,
14
- readableTextColor,
15
- type ColorFormat,
16
- } from "@/lib/color";
17
- import { cn } from "@/lib/cn";
18
-
19
- const FORMATS: { label: string; value: ColorFormat }[] = [
20
- { label: "HEX", value: "hex" },
21
- { label: "RGB", value: "rgb" },
22
- { label: "HSL", value: "hsl" },
23
- { label: "CSS", value: "css" },
24
- ];
25
-
26
- export function ColorsDocPage() {
27
- const [selected, setSelected] = useState(
28
- COLOR_PALETTE.find((h) => h.name === "Blue")?.name ?? COLOR_PALETTE[0].name
29
- );
30
- const [activeStep, setActiveStep] = useState(500);
31
- const [format, setFormat] = useState<ColorFormat>("hex");
32
- const [copied, setCopied] = useState<string | null>(null);
33
- const [mounted, setMounted] = useState(false);
34
-
35
- useEffect(() => setMounted(true), []);
36
-
37
- const activeHue = COLOR_PALETTE.find((h) => h.name === selected) ?? COLOR_PALETTE[0];
38
- const activeShade =
39
- activeHue.shades.find((s) => s.step === activeStep) ?? activeHue.shades[5];
40
-
41
- const activeValue = formatColor(activeShade.hex, format);
42
- const textColor = readableTextColor(activeShade.hex);
43
- const isDarkOverlay = textColor === "#000000";
44
- const overlaySoft = isDarkOverlay ? "rgba(0,0,0,0.08)" : "rgba(255,255,255,0.16)";
45
- const overlayStrong = isDarkOverlay ? "rgba(0,0,0,0.14)" : "rgba(255,255,255,0.28)";
46
-
47
- function copy(value: string) {
48
- navigator.clipboard.writeText(value).catch(() => {});
49
- setCopied(value);
50
- setTimeout(() => setCopied((prev) => (prev === value ? null : prev)), 1200);
51
- }
52
-
53
- function pickHue(name: string) {
54
- setSelected(name);
55
- setActiveStep(500);
56
- }
57
-
58
- return (
59
- <DocsPage wide className="relative z-10 min-w-0">
60
- {mounted &&
61
- createPortal(
62
- <div
63
- aria-hidden
64
- className="pointer-events-none fixed left-0 right-0 top-0 z-0 h-80 max-w-full lg:left-64"
65
- style={{
66
- background: `radial-gradient(ellipse 70% 90% at 80% 0%, ${activeShade.hex}42, transparent 70%)`,
67
- }}
68
- />,
69
- document.getElementById("docs-shell") ?? document.body
70
- )}
71
-
72
- <DocsPageHeader
73
- icon={Palette}
74
- title="Colors"
75
- description="The full color palette available across ruler.ui — every hue and shade used throughout components, ready to copy in HEX, RGB, HSL, or CSS."
76
- />
77
-
78
- <div className="mt-6 flex flex-wrap items-center gap-x-6 gap-y-2 text-sm text-zinc-500 dark:text-zinc-400">
79
- <span>
80
- <strong className="font-semibold text-zinc-900 dark:text-white">
81
- {COLOR_PALETTE.length}
82
- </strong>{" "}
83
- hues
84
- </span>
85
- <span className="h-1 w-1 rounded-full bg-zinc-300 dark:bg-zinc-700" />
86
- <span>
87
- <strong className="font-semibold text-zinc-900 dark:text-white">
88
- {COLOR_PALETTE.length * 11}
89
- </strong>{" "}
90
- shades
91
- </span>
92
- <span className="h-1 w-1 rounded-full bg-zinc-300 dark:bg-zinc-700" />
93
- <span>WCAG contrast built in</span>
94
- </div>
95
-
96
- <DocsSection first title="Explore" description="Every hue as a full-tone paint strip. Click one to spotlight it below.">
97
- <div className="-mx-1 flex min-w-0 gap-3 overflow-x-auto px-1 py-2 [-ms-overflow-style:none] [scrollbar-width:none] sm:gap-5 [&::-webkit-scrollbar]:hidden">
98
- {COLOR_PALETTE.map((hue) => {
99
- const isSelected = hue.name === selected;
100
- const gradient = `linear-gradient(to bottom, ${hue.shades.map((s) => s.hex).join(", ")})`;
101
- return (
102
- <button
103
- key={hue.name}
104
- type="button"
105
- onClick={() => pickHue(hue.name)}
106
- style={{ backgroundImage: gradient }}
107
- className={cn(
108
- "relative h-52 w-32 shrink-0 origin-bottom cursor-pointer overflow-hidden rounded-2xl shadow-[0_6px_16px_-10px_rgba(0,0,0,0.2)] ring-2 ring-offset-2 ring-offset-white transition-[transform,box-shadow] duration-300 ease-out will-change-transform hover:-translate-y-1 hover:scale-[1.03] focus:outline-none focus-visible:ring-zinc-900 dark:ring-offset-zinc-950 dark:focus-visible:ring-white",
109
- isSelected ? "scale-[1.02] ring-zinc-900 dark:ring-white" : "scale-100 ring-transparent"
110
- )}
111
- >
112
- <div className="absolute inset-x-0 bottom-0 flex items-center justify-between gap-2 bg-black/30 px-3 py-2.5 backdrop-blur-md">
113
- <span className="text-sm font-semibold text-white">{hue.name}</span>
114
- {isSelected && (
115
- <span className="flex h-4 w-4 items-center justify-center rounded-full bg-white text-zinc-900">
116
- <Check className="h-2.5 w-2.5" strokeWidth={3} />
117
- </span>
118
- )}
119
- </div>
120
- </button>
121
- );
122
- })}
123
- </div>
124
- </DocsSection>
125
-
126
- <motion.div
127
- key={activeHue.name}
128
- initial={{ opacity: 0, y: 6 }}
129
- animate={{ opacity: 1, y: 0 }}
130
- transition={{ duration: 0.2, ease: "easeOut" }}
131
- >
132
- <DocsSection
133
- title={`${activeHue.name} ${activeShade.step}`}
134
- description="The selected shade, spotlighted — drag through the ramp to compare weights."
135
- >
136
- <div
137
- className="relative flex min-h-[18rem] flex-col justify-between overflow-hidden rounded-[1.5rem] p-5 shadow-[0_24px_60px_-18px_rgba(0,0,0,0.3)] transition-colors duration-500 sm:min-h-[22rem] sm:rounded-[2rem] sm:p-10"
138
- style={{ backgroundColor: activeShade.hex }}
139
- >
140
- <span
141
- aria-hidden
142
- className="pointer-events-none absolute -right-4 -top-8 select-none font-mono text-[8.5rem] font-bold leading-none sm:text-[10.5rem]"
143
- style={{ color: textColor, opacity: 0.09 }}
144
- >
145
- {activeShade.hex.replace("#", "").toUpperCase()}
146
- </span>
147
-
148
- <div className="relative flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between" style={{ color: textColor }}>
149
- <div className="min-w-0">
150
- <p className="text-xs font-semibold uppercase tracking-[0.2em] opacity-60">
151
- Palette / {activeHue.name}
152
- </p>
153
- <h3 className="mt-2 text-3xl font-bold tracking-tight sm:mt-3 sm:text-6xl">
154
- {activeHue.name} {activeShade.step}
155
- </h3>
156
- </div>
157
- <div
158
- className="flex w-fit max-w-full shrink-0 items-center gap-0.5 self-start rounded-full p-1 backdrop-blur-md sm:gap-1"
159
- style={{ backgroundColor: overlaySoft }}
160
- >
161
- {FORMATS.map((f) => (
162
- <button
163
- key={f.value}
164
- type="button"
165
- onClick={() => setFormat(f.value)}
166
- className="relative rounded-full px-3 py-1.5 text-xs font-medium transition-colors"
167
- style={{ color: textColor }}
168
- >
169
- {format === f.value && (
170
- <motion.span
171
- layoutId="hero-format-pill"
172
- className="absolute inset-0 rounded-full backdrop-blur-md"
173
- style={{ backgroundColor: overlayStrong }}
174
- transition={{ type: "spring", stiffness: 500, damping: 35 }}
175
- />
176
- )}
177
- <span className="relative">{f.label}</span>
178
- </button>
179
- ))}
180
- </div>
181
- </div>
182
-
183
- <div className="relative mt-8">
184
- <div className="flex items-end justify-between gap-6" style={{ color: textColor }}>
185
- <div>
186
- <p className="break-all font-mono text-xl font-semibold sm:text-3xl">{activeValue}</p>
187
- <p className="mt-1 text-xs font-medium opacity-70">
188
- {contrastLabel(getContrastRatio(activeShade.hex, "#ffffff"))} on white ·{" "}
189
- {contrastLabel(getContrastRatio(activeShade.hex, "#000000"))} on black
190
- </p>
191
- </div>
192
- <Tooltip content={copied === activeValue ? "Copied!" : "Copy value"}>
193
- <button
194
- type="button"
195
- onClick={() => copy(activeValue)}
196
- className="flex h-11 w-11 shrink-0 items-center justify-center rounded-full backdrop-blur-md transition-transform hover:scale-105"
197
- style={{ backgroundColor: overlaySoft, color: textColor }}
198
- >
199
- {copied === activeValue ? (
200
- <Check className="h-4 w-4" />
201
- ) : (
202
- <Copy className="h-4 w-4" />
203
- )}
204
- </button>
205
- </Tooltip>
206
- </div>
207
-
208
- <div className="mt-6 flex h-10 gap-1">
209
- {activeHue.shades.map((shade) => {
210
- const isActiveShade = shade.step === activeStep;
211
- return (
212
- <div key={shade.step} className="relative h-full flex-1">
213
- <Tooltip
214
- content={
215
- <span className="flex items-center gap-1.5">
216
- <span className="font-semibold">{shade.step}</span>
217
- <span className="opacity-60">{shade.hex.toUpperCase()}</span>
218
- </span>
219
- }
220
- >
221
- <button
222
- type="button"
223
- onClick={() => setActiveStep(shade.step)}
224
- className={cn(
225
- "absolute inset-0 rounded-md transition-all duration-150 hover:-translate-y-1 focus:outline-none",
226
- isActiveShade && "-translate-y-1.5"
227
- )}
228
- style={{
229
- backgroundColor: shade.hex,
230
- boxShadow: isActiveShade
231
- ? `0 8px 18px -6px ${shade.hex}, 0 0 0 2px ${overlayStrong}`
232
- : undefined,
233
- }}
234
- />
235
- </Tooltip>
236
- {isActiveShade && (
237
- <motion.span
238
- layoutId="ramp-active-dot"
239
- className="absolute -bottom-3 left-1/2 h-1 w-1 -translate-x-1/2 rounded-full"
240
- style={{ backgroundColor: textColor }}
241
- />
242
- )}
243
- </div>
244
- );
245
- })}
246
- </div>
247
- </div>
248
- </div>
249
- </DocsSection>
250
-
251
- <DocsSection
252
- title="Readable on any surface"
253
- description="How this shade reads as text, side by side against pure white and pure black."
254
- >
255
- <div className="grid gap-4 sm:grid-cols-2">
256
- <ContrastPreviewCard hex={activeShade.hex} surface="light" />
257
- <ContrastPreviewCard hex={activeShade.hex} surface="dark" />
258
- </div>
259
- </DocsSection>
260
-
261
- <DocsSection
262
- title={`Every shade in ${activeHue.name}`}
263
- description="Formatted, ready to copy, in whichever format you're working with."
264
- >
265
- <div className="overflow-x-auto rounded-2xl border border-zinc-200 shadow-[0_2px_8px_rgba(0,0,0,0.04)] dark:border-zinc-800 dark:shadow-[0_2px_8px_rgba(0,0,0,0.2)]">
266
- <div className="min-w-[32rem]">
267
- <div className="flex items-center gap-4 border-b border-zinc-200 bg-zinc-50/70 px-4 py-2.5 text-xs font-medium text-zinc-500 dark:border-zinc-800 dark:bg-zinc-900/40 dark:text-zinc-400">
268
- <span className="w-9" />
269
- <span className="flex-1">Name</span>
270
- <span className="w-36 shrink-0 sm:w-52">Value</span>
271
- <span className="w-20 text-right sm:w-24">Contrast</span>
272
- </div>
273
- <div className="divide-y divide-zinc-200 dark:divide-zinc-800">
274
- {activeHue.shades.map((shade) => {
275
- const value = formatColor(shade.hex, format);
276
- const isCopied = copied === value;
277
- const isRowActive = shade.step === activeStep;
278
- return (
279
- <div
280
- key={shade.step}
281
- onClick={() => setActiveStep(shade.step)}
282
- className={cn(
283
- "group relative flex cursor-pointer items-center gap-4 pl-4 pr-4 py-3 transition-colors",
284
- isRowActive ? "bg-zinc-50 dark:bg-zinc-900/60" : "hover:bg-zinc-50/70 dark:hover:bg-zinc-900/40"
285
- )}
286
- >
287
- <span
288
- aria-hidden
289
- className={cn(
290
- "absolute left-0 top-1/2 h-6 w-1 -translate-y-1/2 rounded-r-full transition-opacity duration-200",
291
- isRowActive ? "opacity-100" : "opacity-0"
292
- )}
293
- style={{ backgroundColor: shade.hex }}
294
- />
295
- <span
296
- className="h-9 w-9 shrink-0 rounded-xl border border-black/5 shadow-sm transition-transform duration-200 group-hover:scale-105 dark:border-white/10"
297
- style={{ backgroundColor: shade.hex }}
298
- />
299
- <span className="flex-1 text-sm font-medium text-zinc-900 dark:text-white">
300
- {activeHue.name}{" "}
301
- <span className="text-zinc-400 dark:text-zinc-500">{shade.step}</span>
302
- </span>
303
- <div className="w-36 shrink-0 sm:w-52">
304
- <Tooltip content={isCopied ? "Copied!" : "Copy to clipboard"}>
305
- <button
306
- type="button"
307
- onClick={(e) => {
308
- e.stopPropagation();
309
- copy(value);
310
- }}
311
- className="group/copy flex w-full items-center gap-2 whitespace-nowrap rounded-md font-mono text-xs text-zinc-500 transition-colors hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100"
312
- >
313
- <span className="min-w-0 flex-1 truncate text-left">{value}</span>
314
- {isCopied ? (
315
- <Check className="h-3 w-3 shrink-0 text-green-500" />
316
- ) : (
317
- <Copy className="h-3 w-3 shrink-0 opacity-0 transition-opacity group-hover/copy:opacity-100" />
318
- )}
319
- </button>
320
- </Tooltip>
321
- </div>
322
- <div
323
- className="flex w-20 items-center justify-end gap-1.5 sm:w-24"
324
- onClick={(e) => e.stopPropagation()}
325
- >
326
- <ContrastBadge hex={shade.hex} against="#ffffff" />
327
- <ContrastBadge hex={shade.hex} against="#000000" />
328
- </div>
329
- </div>
330
- );
331
- })}
332
- </div>
333
- </div>
334
- </div>
335
- </DocsSection>
336
- </motion.div>
337
- </DocsPage>
338
- );
339
- }
340
-
341
- function ContrastPreviewCard({ hex, surface }: { hex: string; surface: "light" | "dark" }) {
342
- const isLight = surface === "light";
343
- const bg = isLight ? "#ffffff" : "#09090b";
344
- const ratio = getContrastRatio(hex, bg);
345
- const label = contrastLabel(ratio);
346
- const pct = Math.min(100, (ratio / 21) * 100);
347
-
348
- const labelColor = isLight
349
- ? label === "Fail"
350
- ? "text-red-600"
351
- : label === "AA"
352
- ? "text-yellow-600"
353
- : "text-green-600"
354
- : label === "Fail"
355
- ? "text-red-400"
356
- : label === "AA"
357
- ? "text-yellow-400"
358
- : "text-green-400";
359
-
360
- return (
361
- <div
362
- className={cn(
363
- "rounded-2xl border p-6",
364
- isLight ? "border-zinc-200 bg-white" : "border-zinc-800 bg-zinc-950"
365
- )}
366
- >
367
- <p
368
- className={cn(
369
- "text-xs font-medium uppercase tracking-wide",
370
- isLight ? "text-zinc-400" : "text-zinc-500"
371
- )}
372
- >
373
- On {isLight ? "light" : "dark"} surface
374
- </p>
375
- <p className="mt-3 font-mono text-3xl font-bold" style={{ color: hex }}>
376
- Aa Bb Cc
377
- </p>
378
- <p className="mt-1 text-lg font-semibold" style={{ color: hex }}>
379
- The quick brown fox jumps.
380
- </p>
381
-
382
- <div className="mt-5">
383
- <div
384
- className={cn(
385
- "relative h-1.5 w-full overflow-hidden rounded-full",
386
- isLight ? "bg-zinc-100" : "bg-zinc-800"
387
- )}
388
- >
389
- <div
390
- className={cn(
391
- "h-full rounded-full transition-all duration-300",
392
- label === "Fail" ? "bg-red-500" : label === "AA" ? "bg-yellow-500" : "bg-green-500"
393
- )}
394
- style={{ width: `${pct}%` }}
395
- />
396
- </div>
397
- <div
398
- className={cn(
399
- "mt-1.5 flex items-center justify-between text-xs",
400
- isLight ? "text-zinc-500" : "text-zinc-400"
401
- )}
402
- >
403
- <span className="font-mono">{ratio.toFixed(2)}:1</span>
404
- <span className={cn("font-semibold", labelColor)}>{label}</span>
405
- </div>
406
- </div>
407
- </div>
408
- );
409
- }
410
-
411
- function ContrastBadge({ hex, against }: { hex: string; against: "#ffffff" | "#000000" }) {
412
- const ratio = getContrastRatio(hex, against);
413
- const label = contrastLabel(ratio);
414
- const passes = label !== "Fail";
415
- const surface = against === "#ffffff" ? "white" : "black";
416
-
417
- return (
418
- <Tooltip content={`${ratio.toFixed(2)}:1 · ${label} on ${surface}`}>
419
- <span
420
- className={cn(
421
- "flex h-6 w-9 items-center justify-center rounded-md text-[11px] font-semibold ring-1 ring-inset",
422
- passes ? "ring-green-500/30" : "ring-red-500/30"
423
- )}
424
- style={{ backgroundColor: against, color: hex }}
425
- >
426
- Aa
427
- </span>
428
- </Tooltip>
429
- );
430
- }