Add colours generator.
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
---
|
||||
---
|
||||
|
||||
<div id="color-generator" class="mt-8">
|
||||
|
||||
<!-- Swatch -->
|
||||
<div class="flex justify-center mb-8">
|
||||
<div
|
||||
id="cg-swatch"
|
||||
class="w-44 h-44 rounded-2xl border border-zinc-800"
|
||||
style="background-color: #1f1f22; transition: background-color 0.35s cubic-bezier(0.4,0,0.2,1);"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- Format rows -->
|
||||
<div
|
||||
id="cg-values"
|
||||
class="mb-8 divide-y divide-zinc-800 border border-zinc-800 rounded-xl overflow-hidden opacity-0"
|
||||
style="transition: opacity 0.25s ease;"
|
||||
aria-live="polite"
|
||||
>
|
||||
{[
|
||||
{ id: 'cg-hex', label: 'HEX' },
|
||||
{ id: 'cg-rgb', label: 'RGB' },
|
||||
{ id: 'cg-hsl', label: 'HSL' },
|
||||
].map(({ id, label }) => (
|
||||
<div class="flex items-center gap-3 px-4 py-3 bg-zinc-900/40">
|
||||
<span class="w-10 text-xs font-semibold uppercase tracking-widest text-zinc-500 shrink-0">
|
||||
{label}
|
||||
</span>
|
||||
<span
|
||||
id={id}
|
||||
class="flex-1 font-mono text-sm text-zinc-100 tabular-nums select-all"
|
||||
></span>
|
||||
<button
|
||||
id={`${id}-copy`}
|
||||
type="button"
|
||||
aria-label={`Copy ${label} value`}
|
||||
class="group shrink-0 p-1 rounded text-zinc-600 hover:text-zinc-300 focus:outline-none focus-visible:ring-1 focus-visible:ring-[#534AB7] transition-colors cursor-pointer"
|
||||
>
|
||||
<svg id={`${id}-copy-icon`} xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/>
|
||||
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>
|
||||
</svg>
|
||||
<svg id={`${id}-check-icon`} class="hidden text-[#534AB7]" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20 6 9 17l-5-5"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<!-- Generate button -->
|
||||
<div class="flex justify-center">
|
||||
<button
|
||||
id="cg-btn"
|
||||
type="button"
|
||||
class="w-full sm:w-auto px-8 py-3 bg-[#534AB7] text-white font-semibold rounded-lg hover:bg-[#4740a0] active:bg-[#3d3990] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-950 transition-colors duration-100 cursor-pointer"
|
||||
>
|
||||
Generate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const swatch = document.getElementById('cg-swatch') as HTMLDivElement;
|
||||
const valuesEl = document.getElementById('cg-values') as HTMLDivElement;
|
||||
const hexEl = document.getElementById('cg-hex') as HTMLSpanElement;
|
||||
const rgbEl = document.getElementById('cg-rgb') as HTMLSpanElement;
|
||||
const hslEl = document.getElementById('cg-hsl') as HTMLSpanElement;
|
||||
const btn = document.getElementById('cg-btn') as HTMLButtonElement;
|
||||
|
||||
function rgbToHsl(r: number, g: number, b: number): [number, number, number] {
|
||||
const rn = r / 255, gn = g / 255, bn = b / 255;
|
||||
const max = Math.max(rn, gn, bn), min = Math.min(rn, gn, bn);
|
||||
const l = (max + min) / 2;
|
||||
if (max === min) return [0, 0, Math.round(l * 100)];
|
||||
const d = max - min;
|
||||
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
let h = 0;
|
||||
if (max === rn) h = ((gn - bn) / d + (gn < bn ? 6 : 0)) / 6;
|
||||
else if (max === gn) h = ((bn - rn) / d + 2) / 6;
|
||||
else h = ((rn - gn) / d + 4) / 6;
|
||||
return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)];
|
||||
}
|
||||
|
||||
function toHex(n: number) {
|
||||
return n.toString(16).padStart(2, '0');
|
||||
}
|
||||
|
||||
function makeCopyHandler(el: HTMLSpanElement, copyIconId: string, checkIconId: string) {
|
||||
const copyIcon = document.getElementById(copyIconId)!;
|
||||
const checkIcon = document.getElementById(checkIconId)!;
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
return async () => {
|
||||
const value = el.textContent?.trim();
|
||||
if (!value) return;
|
||||
await navigator.clipboard.writeText(value);
|
||||
copyIcon.classList.add('hidden');
|
||||
checkIcon.classList.remove('hidden');
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
checkIcon.classList.add('hidden');
|
||||
copyIcon.classList.remove('hidden');
|
||||
}, 1500);
|
||||
};
|
||||
}
|
||||
|
||||
document.getElementById('cg-hex-copy')!
|
||||
.addEventListener('click', makeCopyHandler(hexEl, 'cg-hex-copy-icon', 'cg-hex-check-icon'));
|
||||
document.getElementById('cg-rgb-copy')!
|
||||
.addEventListener('click', makeCopyHandler(rgbEl, 'cg-rgb-copy-icon', 'cg-rgb-check-icon'));
|
||||
document.getElementById('cg-hsl-copy')!
|
||||
.addEventListener('click', makeCopyHandler(hslEl, 'cg-hsl-copy-icon', 'cg-hsl-check-icon'));
|
||||
|
||||
function generate() {
|
||||
const r = Math.floor(Math.random() * 256);
|
||||
const g = Math.floor(Math.random() * 256);
|
||||
const b = Math.floor(Math.random() * 256);
|
||||
const [h, s, l] = rgbToHsl(r, g, b);
|
||||
|
||||
const hex = `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
|
||||
|
||||
swatch.style.backgroundColor = hex;
|
||||
hexEl.textContent = hex;
|
||||
rgbEl.textContent = `rgb(${r}, ${g}, ${b})`;
|
||||
hslEl.textContent = `hsl(${h}, ${s}%, ${l}%)`;
|
||||
valuesEl.style.opacity = '1';
|
||||
}
|
||||
|
||||
btn.addEventListener('click', generate);
|
||||
</script>
|
||||
@@ -19,7 +19,7 @@ export const generators: Generator[] = [
|
||||
title: 'Color',
|
||||
description: 'Generate a random color in HEX, RGB, or HSL format.',
|
||||
icon: 'palette',
|
||||
status: 'coming-soon',
|
||||
status: 'live',
|
||||
},
|
||||
{
|
||||
slug: 'password',
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import ColorGenerator from '../../components/generators/ColorGenerator.astro';
|
||||
import AdBanner from '../../components/AdBanner.astro';
|
||||
import { generators } from '../../data/generators';
|
||||
|
||||
const generator = generators.find((g) => g.slug === 'colors')!;
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={`${generator.title} Generator — Randify`}
|
||||
description={generator.description}
|
||||
>
|
||||
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
|
||||
<nav aria-label="Breadcrumb" class="mb-10">
|
||||
<a
|
||||
href="/"
|
||||
class="inline-flex items-center gap-1.5 text-sm text-zinc-500 hover:text-zinc-200 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] rounded"
|
||||
aria-label="Back to all generators"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="m15 18-6-6 6-6"/>
|
||||
</svg>
|
||||
All generators
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<header class="mb-2">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span
|
||||
class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">
|
||||
{generator.title} Generator
|
||||
</h1>
|
||||
<p class="mt-2 text-base text-zinc-400">{generator.description}</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<ColorGenerator />
|
||||
</main>
|
||||
|
||||
<div class="mt-12 grid grid-cols-3 gap-3">
|
||||
<AdBanner size="tile" />
|
||||
<AdBanner size="tile" />
|
||||
<AdBanner size="tile" />
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
Reference in New Issue
Block a user