Release Random Tools 0.1.0

This commit is contained in:
2026-09-01 02:53:47 +02:00
commit 2ed2560b95
59 changed files with 8692 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
import { Component, type ErrorInfo, type ReactNode } from "react";
export class ErrorBoundary extends Component<
{ children: ReactNode },
{ error?: Error }
> {
state: { error?: Error } = {};
static getDerivedStateFromError(error: Error) {
return { error };
}
componentDidCatch(error: Error, info: ErrorInfo) {
console.error("Application failure", error, info);
}
render() {
if (this.state.error)
return (
<main className="fatal">
<h1>Random Tools could not continue</h1>
<p>{this.state.error.message}</p>
<button type="button" onClick={() => location.reload()}>
Reload
</button>
</main>
);
return this.props.children;
}
}
+44
View File
@@ -0,0 +1,44 @@
import { useEffect, useRef } from "react";
export function HelpDialog({
open,
onClose,
}: {
open: boolean;
onClose: () => void;
}) {
const dialog = useRef<HTMLDialogElement>(null);
useEffect(() => {
const node = dialog.current;
if (!node) return;
if (open && !node.open) node.showModal();
if (!open && node.open) node.close();
}, [open]);
return (
<dialog
ref={dialog}
className="help-dialog"
onClose={onClose}
onCancel={onClose}
aria-labelledby="help-title"
>
<div className="dialog-heading">
<div>
<p className="eyebrow">Local-first help</p>
<h2 id="help-title">About Random Tools</h2>
</div>
<button type="button" onClick={onClose} aria-label="Close help">
×
</button>
</div>
<p>
Generate secure or reproducible random values locally in the browser.
</p>
<p>
Local WebCrypto and seeded generation stay in this browser. RANDOM.ORG
is contacted only from its separate workspace after explicit consent;
local sources never fall back to it. Inputs and responses are bounded.
</p>
</dialog>
);
}
+695
View File
@@ -0,0 +1,695 @@
import { useState } from "react";
import { bytesToHex, triggerBlobDownload } from "@add-ideas/toolbox-helpers";
import {
DEFAULT_WORDS,
normalValues,
passphrase,
randomIntegers,
randomString,
rollDice,
sampleValues,
ulid,
uuidV4,
uuidV7,
} from "../random/generators";
import { randomOrgIntegers } from "../random/remote";
import { randomSource, type SourceMode } from "../random/source";
type Tab =
| "numbers"
| "strings"
| "identifiers"
| "dice"
| "lists"
| "passphrases"
| "remote";
interface Output {
title: string;
text: string;
recipe: Record<string, unknown>;
note?: string;
}
function Result({ output }: { output: Output | undefined }) {
if (!output)
return <p className="empty">Generate a result to see it here.</p>;
const report = JSON.stringify(
{ schemaVersion: 1, generatedBy: "add-ideas Rand Tools 0.1.0", ...output },
null,
2,
);
return (
<section className="result" aria-live="polite">
<div className="panel-heading">
<div>
<p className="eyebrow">Last successful result</p>
<h3>{output.title}</h3>
</div>
<div className="actions">
<button
type="button"
onClick={() => void navigator.clipboard.writeText(output.text)}
>
Copy
</button>
<button
type="button"
onClick={() =>
triggerBlobDownload(
new Blob([report], { type: "application/json" }),
"random-result.json",
)
}
>
Recipe JSON
</button>
</div>
</div>
<pre>{output.text}</pre>
{output.note && <p className="warning">{output.note}</p>}
<details>
<summary>Reproduction metadata</summary>
<pre>{JSON.stringify(output.recipe, null, 2)}</pre>
</details>
</section>
);
}
function SourceControls({
mode,
setMode,
seed,
setSeed,
}: {
mode: SourceMode;
setMode: (mode: SourceMode) => void;
seed: string;
setSeed: (seed: string) => void;
}) {
return (
<div className="source-controls">
<label className="field">
<span>Random source</span>
<select
value={mode}
onChange={(event) => setMode(event.target.value as SourceMode)}
>
<option value="secure">Secure local (WebCrypto)</option>
<option value="deterministic">Reproducible seeded</option>
</select>
</label>
{mode === "deterministic" && (
<label className="field">
<span>Seed</span>
<input
value={seed}
onChange={(event) => setSeed(event.target.value)}
/>
</label>
)}
<p className={mode === "secure" ? "success" : "warning"}>
{mode === "secure"
? "Uses crypto.getRandomValues. No fallback to seeded or remote randomness."
: "Reproducible pseudorandom output is not cryptographic and must not be used for keys, passwords, or security tokens."}
</p>
</div>
);
}
export function Workbench() {
const [tab, setTab] = useState<Tab>("numbers");
const [mode, setMode] = useState<SourceMode>("secure");
const [seed, setSeed] = useState("reproducible-example");
const [output, setOutput] = useState<Output>();
const [error, setError] = useState("");
const [count, setCount] = useState(10);
const [minimum, setMinimum] = useState(1);
const [maximum, setMaximum] = useState(100);
const [distribution, setDistribution] = useState<"uniform" | "normal">(
"uniform",
);
const [mean, setMean] = useState(0);
const [deviation, setDeviation] = useState(1);
const [length, setLength] = useState(24);
const [alphabet, setAlphabet] = useState(
"ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789",
);
const [identifier, setIdentifier] = useState<"uuid4" | "uuid7" | "ulid">(
"uuid7",
);
const [dice, setDice] = useState("4d6+2");
const [list, setList] = useState(
"amber\nbirch\ncedar\ndelta\nember\nforest\ngranite\nharbor",
);
const [sampleCount, setSampleCount] = useState(3);
const [shuffle, setShuffle] = useState(false);
const [wordCount, setWordCount] = useState(6);
const [customWords, setCustomWords] = useState("");
const [remoteConsent, setRemoteConsent] = useState(false);
const [remoteBusy, setRemoteBusy] = useState(false);
const local = (
operation: (source: ReturnType<typeof randomSource>) => {
title: string;
text: string;
parameters: Record<string, unknown>;
note?: string;
},
) => {
try {
const source = randomSource(mode, seed);
const result = operation(source);
setOutput({
title: result.title,
text: result.text,
note: result.note,
recipe: {
source: source.identity,
sourceClass:
mode === "secure"
? "cryptographic"
: "deterministic-non-cryptographic",
seed: mode === "deterministic" ? seed : undefined,
stateAfter: source.state?.(),
parameters: result.parameters,
},
});
setError("");
} catch (reason) {
setError(reason instanceof Error ? reason.message : "Generation failed.");
}
};
const generateNumbers = () =>
local((source) => {
const values =
distribution === "uniform"
? randomIntegers(source, count, minimum, maximum)
: normalValues(source, count, mean, deviation);
return {
title:
distribution === "uniform"
? "Uniform integers"
: "Normal distribution",
text: values.join("\n"),
parameters:
distribution === "uniform"
? { distribution, count, minimum, maximumInclusive: maximum }
: { distribution: "BoxMuller normal-v1", count, mean, deviation },
};
});
const generateString = () =>
local((source) => ({
title: "Random string",
text: randomString(source, length, alphabet),
parameters: { length, alphabet },
}));
const generateIdentifiers = () =>
local((source) => {
const now = Date.now();
const values = Array.from({ length: count }, () =>
identifier === "uuid4"
? uuidV4(source)
: identifier === "uuid7"
? uuidV7(source, now)
: ulid(source, now),
);
return {
title:
identifier === "uuid4"
? "UUIDv4"
: identifier === "uuid7"
? "UUIDv7"
: "Random ULID",
text: values.join("\n"),
parameters: {
identifier,
count,
timestampMilliseconds: identifier === "uuid4" ? undefined : now,
},
note:
identifier === "ulid"
? "These are random ULIDs, not a monotonic sequence; sort order among identifiers from the same millisecond is random."
: undefined,
};
});
const generateDice = () =>
local((source) => {
const result = rollDice(source, dice);
return {
title: `${result.expression} = ${result.total}`,
text: `${result.rolls.join(" + ")}${result.modifier ? (result.modifier > 0 ? ` + ${result.modifier}` : ` ${Math.abs(result.modifier)}`) : ""}\nTotal: ${result.total}`,
parameters: { expression: result.expression },
note: "This roller is not certified for regulated gambling or audited drawings.",
};
});
const generateList = () =>
local((source) => {
if (list.length > 4_000_000)
throw new Error("List input exceeds 4,000,000 UTF-16 units.");
const values = list
.replaceAll("\r\n", "\n")
.split("\n")
.filter((value) => value.length);
if (!values.length || values.length > 100_000)
throw new Error("Provide 1100,000 non-empty lines.");
const selected = shuffle
? source.shuffle(values)
: sampleValues(source, values, sampleCount);
return {
title: shuffle ? "Shuffled list" : "Unique sample",
text: selected.join("\n"),
parameters: {
operation: shuffle
? "FisherYates shuffle"
: "sample without replacement",
inputCount: values.length,
sampleCount: shuffle ? values.length : sampleCount,
},
};
});
const generatePassphrase = () =>
local((source) => {
if (customWords.length > 4_000_000)
throw new Error(
"Custom word-list input exceeds 4,000,000 UTF-16 units.",
);
const words = customWords.trim()
? customWords.replaceAll("\r\n", "\n").split("\n")
: DEFAULT_WORDS;
const result = passphrase(source, wordCount, words);
return {
title: "Passphrase",
text: result.value,
parameters: {
wordCount,
listSize: new Set(words).size,
separator: "-",
entropyModelBits: result.entropy,
},
note: `The ${result.entropy.toFixed(1)}-bit figure is only count × log₂(list size), assuming independent uniform choices. It is not a password-strength audit. The bundled list is project-authored; use a larger reviewed list for real passphrases.`,
};
});
const generateBytes = () =>
local((source) => ({
title: "Random bytes (hex)",
text: bytesToHex(source.bytes(length)),
parameters: { bytes: length, encoding: "lowercase hexadecimal" },
}));
const requestRemote = async () => {
if (!remoteConsent) {
setError("Confirm the external-service notice first.");
return;
}
setRemoteBusy(true);
setError("");
try {
const values = await randomOrgIntegers({ count, minimum, maximum });
setOutput({
title: "RANDOM.ORG integers",
text: values.join("\n"),
note: "Remote values were returned by RANDOM.ORG over HTTPS. They are not used for Toolbox passwords, keys, or secure local workflows.",
recipe: {
source: "RANDOM.ORG HTTP integer generator",
sourceClass: "remote-unverified",
requestedAt: new Date().toISOString(),
parameters: { count, minimum, maximum },
},
});
} catch (reason) {
setError(
reason instanceof Error ? reason.message : "Remote request failed.",
);
} finally {
setRemoteBusy(false);
}
};
const tabs = [
["numbers", "Numbers"],
["strings", "Strings & bytes"],
["identifiers", "Identifiers"],
["dice", "Dice"],
["lists", "Lists"],
["passphrases", "Passphrases"],
["remote", "RANDOM.ORG"],
] as const;
return (
<main className="workbench">
<header className="hero">
<div>
<p className="eyebrow">Secure local default</p>
<h1>Rand Tools</h1>
<p>
Generate numbers, strings, identifiers, dice, samples, shuffles, and
passphrases with explicit randomness sources.
</p>
</div>
<span className="privacy-pill">WebCrypto by default</span>
</header>
<SourceControls
mode={mode}
setMode={setMode}
seed={seed}
setSeed={setSeed}
/>
<nav
className="panel workspace-tabs"
role="tablist"
aria-label="Random workspaces"
>
{tabs.map(([value, label]) => (
<button
type="button"
role="tab"
aria-selected={tab === value}
onClick={() => setTab(value)}
key={value}
>
{label}
</button>
))}
</nav>
<div className="generator-layout">
<section
className="panel workspace"
aria-labelledby="generator-heading"
>
<div>
<p className="eyebrow">Generator</p>
<h2 id="generator-heading">
{tabs.find(([value]) => value === tab)?.[1]}
</h2>
</div>
{tab === "numbers" && (
<>
<div className="form-grid">
<label className="field">
<span>Distribution</span>
<select
value={distribution}
onChange={(event) =>
setDistribution(
event.target.value as "uniform" | "normal",
)
}
>
<option value="uniform">Uniform integers</option>
<option value="normal">Normal (BoxMuller)</option>
</select>
</label>
<label className="field">
<span>Count</span>
<input
type="number"
min="1"
max="100000"
value={count}
onChange={(event) => setCount(event.target.valueAsNumber)}
/>
</label>
{distribution === "uniform" ? (
<>
<label className="field">
<span>Minimum</span>
<input
type="number"
value={minimum}
onChange={(event) =>
setMinimum(event.target.valueAsNumber)
}
/>
</label>
<label className="field">
<span>Maximum (inclusive)</span>
<input
type="number"
value={maximum}
onChange={(event) =>
setMaximum(event.target.valueAsNumber)
}
/>
</label>
</>
) : (
<>
<label className="field">
<span>Mean</span>
<input
type="number"
value={mean}
onChange={(event) =>
setMean(event.target.valueAsNumber)
}
/>
</label>
<label className="field">
<span>Standard deviation</span>
<input
type="number"
min="0"
step="any"
value={deviation}
onChange={(event) =>
setDeviation(event.target.valueAsNumber)
}
/>
</label>
</>
)}
</div>
<button
className="primary"
type="button"
onClick={generateNumbers}
>
Generate numbers
</button>
</>
)}
{tab === "strings" && (
<>
<label className="field">
<span>Length (code points or bytes)</span>
<input
type="number"
min="1"
max="1000000"
value={length}
onChange={(event) => setLength(event.target.valueAsNumber)}
/>
</label>
<label className="field">
<span>Unique-character alphabet</span>
<textarea
value={alphabet}
onChange={(event) => setAlphabet(event.target.value)}
/>
</label>
<div className="actions">
<button
className="primary"
type="button"
onClick={generateString}
>
Generate string
</button>
<button type="button" onClick={generateBytes}>
Generate hex bytes
</button>
</div>
</>
)}
{tab === "identifiers" && (
<>
<div className="form-grid">
<label className="field">
<span>Identifier</span>
<select
value={identifier}
onChange={(event) =>
setIdentifier(event.target.value as typeof identifier)
}
>
<option value="uuid4">UUIDv4</option>
<option value="uuid7">UUIDv7</option>
<option value="ulid">ULID (random)</option>
</select>
</label>
<label className="field">
<span>Count</span>
<input
type="number"
min="1"
max="100000"
value={count}
onChange={(event) => setCount(event.target.valueAsNumber)}
/>
</label>
</div>
<button
className="primary"
type="button"
onClick={generateIdentifiers}
>
Generate identifiers
</button>
</>
)}
{tab === "dice" && (
<>
<label className="field">
<span>Dice expression</span>
<input
value={dice}
onChange={(event) => setDice(event.target.value)}
placeholder="4d6+2"
/>
</label>
<button className="primary" type="button" onClick={generateDice}>
Roll
</button>
</>
)}
{tab === "lists" && (
<>
<label className="field">
<span>One item per line</span>
<textarea
value={list}
onChange={(event) => setList(event.target.value)}
/>
</label>
<div className="form-grid">
<label className="check">
<input
type="checkbox"
checked={shuffle}
onChange={(event) => setShuffle(event.target.checked)}
/>{" "}
Shuffle every item
</label>
{!shuffle && (
<label className="field">
<span>Unique sample count</span>
<input
type="number"
min="1"
value={sampleCount}
onChange={(event) =>
setSampleCount(event.target.valueAsNumber)
}
/>
</label>
)}
</div>
<button className="primary" type="button" onClick={generateList}>
{shuffle ? "Shuffle" : "Sample"}
</button>
</>
)}
{tab === "passphrases" && (
<>
<label className="field">
<span>Words</span>
<input
type="number"
min="1"
max="100"
value={wordCount}
onChange={(event) => setWordCount(event.target.valueAsNumber)}
/>
</label>
<label className="field">
<span>
Optional custom word list (one unique word per line)
</span>
<textarea
value={customWords}
onChange={(event) => setCustomWords(event.target.value)}
placeholder={`Leave empty for the bundled ${DEFAULT_WORDS.length}-word demonstration list.`}
/>
</label>
<button
className="primary"
type="button"
onClick={generatePassphrase}
>
Generate passphrase
</button>
</>
)}
{tab === "remote" && (
<>
<p className="warning">
This optional action contacts <strong>www.random.org</strong>,
revealing your IP address and request parameters to that
service. Browser JavaScript cannot set the contact-email
User-Agent requested by its automated-client guidance, so this
integration cannot claim complete guideline adherence.
</p>
<label className="check">
<input
type="checkbox"
checked={remoteConsent}
onChange={(event) => setRemoteConsent(event.target.checked)}
/>{" "}
I understand and want to make this one external request.
</label>
<div className="form-grid">
<label className="field">
<span>Count (max 1,000)</span>
<input
type="number"
min="1"
max="1000"
value={count}
onChange={(event) => setCount(event.target.valueAsNumber)}
/>
</label>
<label className="field">
<span>Minimum</span>
<input
type="number"
value={minimum}
onChange={(event) => setMinimum(event.target.valueAsNumber)}
/>
</label>
<label className="field">
<span>Maximum</span>
<input
type="number"
value={maximum}
onChange={(event) => setMaximum(event.target.valueAsNumber)}
/>
</label>
</div>
<button
className="primary"
type="button"
disabled={!remoteConsent || remoteBusy}
onClick={() => void requestRemote()}
>
{remoteBusy ? "Requesting…" : "Request remote integers"}
</button>
</>
)}
{error && (
<p className="error" role="alert">
{error}
</p>
)}
</section>
<Result output={output} />
</div>
<section className="panel workspace">
<p className="notice">
No generator is presented as certified for lotteries, gambling,
regulated drawings, or password-strength evaluation. Secure,
deterministic, and remote sources never silently substitute for one
another.
</p>
</section>
</main>
);
}