Release Random Tools 0.1.0
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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: "Box–Muller 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 1–100,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
|
||||
? "Fisher–Yates 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 (Box–Muller)</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user