- Coin flip: slider 1–20 coins, heads/tails display with H/T count summary - List picker: textarea input, configurable pick count, unique or with-replacement, Ctrl+Enter shortcut - UUID/Token: UUID v4 via crypto, hex and base64 modes with configurable byte length, up to 20 at once Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
186 lines
7.4 KiB
Plaintext
186 lines
7.4 KiB
Plaintext
---
|
|
// Interactive UUID / token generator — runs on the client via inline script
|
|
---
|
|
|
|
<div id="uuid-generator" class="mt-8">
|
|
<div class="flex flex-col gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-zinc-400 mb-2">Type</label>
|
|
<div class="flex flex-wrap gap-2" role="radiogroup" aria-label="Token type">
|
|
{[
|
|
{ value: 'uuid', label: 'UUID v4' },
|
|
{ value: 'hex', label: 'Hex' },
|
|
{ value: 'base64', label: 'Base64' },
|
|
].map(({ value, label }) => (
|
|
<label class="cursor-pointer">
|
|
<input type="radio" name="ug-type" value={value} class="sr-only peer" checked={value === 'uuid'} />
|
|
<span class="inline-flex items-center justify-center px-4 h-9 rounded-lg border border-zinc-700 text-sm font-medium text-zinc-400 peer-checked:border-[#534AB7] peer-checked:text-[#534AB7] peer-checked:bg-[#534AB7]/10 hover:border-zinc-500 transition-colors select-none cursor-pointer">
|
|
{label}
|
|
</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col sm:flex-row gap-4">
|
|
<div id="ug-length-wrap" class="flex-1 hidden">
|
|
<label for="ug-length" class="block text-sm font-medium text-zinc-400 mb-1">Length (bytes)</label>
|
|
<input
|
|
id="ug-length"
|
|
type="number"
|
|
value="16"
|
|
min="4"
|
|
max="64"
|
|
class="w-full bg-zinc-900 border border-zinc-700 rounded-lg px-3 py-2 text-zinc-100 text-base focus:outline-none focus:border-[#534AB7] focus:ring-1 focus:ring-[#534AB7] transition-colors"
|
|
aria-label="Token length in bytes"
|
|
/>
|
|
</div>
|
|
<div class="flex-1">
|
|
<label for="ug-count" class="block text-sm font-medium text-zinc-400 mb-1">Count</label>
|
|
<input
|
|
id="ug-count"
|
|
type="number"
|
|
value="1"
|
|
min="1"
|
|
max="20"
|
|
class="w-full bg-zinc-900 border border-zinc-700 rounded-lg px-3 py-2 text-zinc-100 text-base focus:outline-none focus:border-[#534AB7] focus:ring-1 focus:ring-[#534AB7] transition-colors"
|
|
aria-label="Number of tokens to generate"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="my-8 min-h-16 flex flex-col gap-2" aria-live="polite" aria-label="Generated tokens">
|
|
<div id="ug-result" class="flex flex-col gap-2"></div>
|
|
|
|
<button
|
|
id="ug-copy-btn"
|
|
type="button"
|
|
aria-label="Copy all tokens to clipboard"
|
|
class="invisible self-start inline-flex items-center gap-1.5 text-xs font-medium text-zinc-500 hover:text-zinc-200 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] rounded mt-1"
|
|
>
|
|
<span id="ug-copy-label">Copy all</span>
|
|
<span id="ug-copy-icon" aria-hidden="true">
|
|
<svg 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>
|
|
</span>
|
|
<span id="ug-check-icon" class="hidden text-[#534AB7]" aria-hidden="true">
|
|
<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">
|
|
<path d="M20 6 9 17l-5-5"/>
|
|
</svg>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="flex justify-center">
|
|
<button
|
|
id="ug-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 lengthWrap = document.getElementById('ug-length-wrap') as HTMLDivElement;
|
|
const lengthInput = document.getElementById('ug-length') as HTMLInputElement;
|
|
const countInput = document.getElementById('ug-count') as HTMLInputElement;
|
|
const btn = document.getElementById('ug-btn') as HTMLButtonElement;
|
|
const copyBtn = document.getElementById('ug-copy-btn') as HTMLButtonElement;
|
|
const resultEl = document.getElementById('ug-result') as HTMLDivElement;
|
|
const copyLabel = document.getElementById('ug-copy-label') as HTMLSpanElement;
|
|
const copyIcon = document.getElementById('ug-copy-icon') as HTMLSpanElement;
|
|
const checkIcon = document.getElementById('ug-check-icon') as HTMLSpanElement;
|
|
|
|
let lastTokens: string[] = [];
|
|
let copyRevertTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
function getType(): string {
|
|
return (document.querySelector('input[name="ug-type"]:checked') as HTMLInputElement)?.value ?? 'uuid';
|
|
}
|
|
|
|
document.querySelectorAll('input[name="ug-type"]').forEach((radio) => {
|
|
radio.addEventListener('change', () => {
|
|
const type = getType();
|
|
lengthWrap.classList.toggle('hidden', type === 'uuid');
|
|
});
|
|
});
|
|
|
|
function uuidV4(): string {
|
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
const r = (crypto.getRandomValues(new Uint8Array(1))[0] & 0xf);
|
|
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
|
});
|
|
}
|
|
|
|
function randomBytes(n: number): Uint8Array {
|
|
const buf = new Uint8Array(n);
|
|
crypto.getRandomValues(buf);
|
|
return buf;
|
|
}
|
|
|
|
function toHex(bytes: Uint8Array): string {
|
|
return Array.from(bytes).map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
}
|
|
|
|
function toBase64(bytes: Uint8Array): string {
|
|
return btoa(String.fromCharCode(...bytes))
|
|
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
}
|
|
|
|
function generate() {
|
|
const type = getType();
|
|
const count = parseInt(countInput.value, 10);
|
|
const length = parseInt(lengthInput.value, 10);
|
|
|
|
lastTokens = Array.from({ length: count }, () => {
|
|
if (type === 'uuid') return uuidV4();
|
|
if (type === 'hex') return toHex(randomBytes(length));
|
|
return toBase64(randomBytes(length));
|
|
});
|
|
|
|
resultEl.innerHTML = '';
|
|
lastTokens.forEach((token) => {
|
|
const row = document.createElement('div');
|
|
row.className =
|
|
'flex items-center justify-between gap-3 bg-zinc-900 border border-zinc-700 rounded-lg px-3 py-2';
|
|
|
|
const code = document.createElement('code');
|
|
code.textContent = token;
|
|
code.className = 'text-sm font-mono text-zinc-100 break-all';
|
|
|
|
row.appendChild(code);
|
|
resultEl.appendChild(row);
|
|
});
|
|
|
|
copyBtn.classList.remove('invisible');
|
|
copyLabel.textContent = 'Copy all';
|
|
}
|
|
|
|
async function copyAll() {
|
|
if (!lastTokens.length) return;
|
|
await navigator.clipboard.writeText(lastTokens.join('\n'));
|
|
|
|
copyIcon.classList.add('hidden');
|
|
checkIcon.classList.remove('hidden');
|
|
copyLabel.textContent = 'Copied';
|
|
|
|
if (copyRevertTimer) clearTimeout(copyRevertTimer);
|
|
copyRevertTimer = setTimeout(() => {
|
|
checkIcon.classList.add('hidden');
|
|
copyIcon.classList.remove('hidden');
|
|
copyLabel.textContent = 'Copy all';
|
|
}, 1500);
|
|
}
|
|
|
|
btn.addEventListener('click', generate);
|
|
copyBtn.addEventListener('click', copyAll);
|
|
[countInput, lengthInput].forEach((el) => {
|
|
el.addEventListener('keydown', (e) => { if (e.key === 'Enter') generate(); });
|
|
});
|
|
</script>
|