Invalid XML
@@ -87,7 +150,10 @@ function ConverterPanel() {
const computed = useMemo(() => {
try {
return {
- value: mode === "XML → JSON" ? xmlToJson(input) : jsonToXml(input, root.trim() || "root"),
+ value:
+ mode === "XML → JSON"
+ ? xmlToJson(input)
+ : jsonToXml(input, root.trim() || "root"),
error: null as string | null,
};
} catch (e) {
@@ -99,19 +165,45 @@ function ConverterPanel() {
- setMode(e.target.value)} options={["XML → JSON", "JSON → XML"]} />
+ setMode(e.target.value)}
+ options={["XML → JSON", "JSON → XML"]}
+ />
{mode === "JSON → XML" && (
- setRoot(e.target.value)} spellCheck={false} placeholder="root" />
+ setRoot(e.target.value)}
+ spellCheck={false}
+ placeholder="root"
+ />
)}
-
- {computed.error ?
{computed.error}
:
}
-
Works for simple XML without mixed content or namespaces. Errors from either direction are shown inline.
+ {computed.error ? (
+
{computed.error}
+ ) : (
+
+ )}
+
+ Works for simple XML without mixed content or namespaces. Errors from
+ either direction are shown inline.
+
);
}
@@ -129,4 +221,4 @@ export default function XmlTools({ tool }: ToolPageProps) {
default:
return null;
}
-}
\ No newline at end of file
+}
diff --git a/artifacts/personal-tool-console/src/hooks/use-page-title.ts b/artifacts/personal-tool-console/src/hooks/use-page-title.ts
index e66132d..db855b9 100644
--- a/artifacts/personal-tool-console/src/hooks/use-page-title.ts
+++ b/artifacts/personal-tool-console/src/hooks/use-page-title.ts
@@ -6,4 +6,4 @@ export function usePageTitle(title?: string) {
useEffect(() => {
document.title = title ? `${title} · ${SITE_NAME}` : SITE_NAME;
}, [title]);
-}
\ No newline at end of file
+}
diff --git a/artifacts/personal-tool-console/src/lib/format.ts b/artifacts/personal-tool-console/src/lib/format.ts
index a798ba6..b5baab9 100644
--- a/artifacts/personal-tool-console/src/lib/format.ts
+++ b/artifacts/personal-tool-console/src/lib/format.ts
@@ -1,10 +1,11 @@
export function formatNumber(value: number, digits = 8) {
- if (!Number.isFinite(value)) return '—';
+ if (!Number.isFinite(value)) return "—";
const abs = Math.abs(value);
- if (abs !== 0 && (abs >= 1e9 || abs < 1e-6)) return value.toExponential(6).replace(/\+/, '');
+ if (abs !== 0 && (abs >= 1e9 || abs < 1e-6))
+ return value.toExponential(6).replace(/\+/, "");
return Number.parseFloat(value.toFixed(digits)).toString();
}
export function normalize(input: string) {
- return input.trim().replace(/\s+/g, ' ').toLowerCase();
+ return input.trim().replace(/\s+/g, " ").toLowerCase();
}
diff --git a/artifacts/personal-tool-console/src/lib/registry/core-tools.ts b/artifacts/personal-tool-console/src/lib/registry/core-tools.ts
index b764063..05cf60f 100644
--- a/artifacts/personal-tool-console/src/lib/registry/core-tools.ts
+++ b/artifacts/personal-tool-console/src/lib/registry/core-tools.ts
@@ -1,148 +1,360 @@
import type { ToolDefinition } from "@/lib/tool-registry";
import { tf } from "@/lib/registry/lazy";
import {
- Calculator, Ruler, Banknote, Timer, KeyRound, Sparkles, ScanBarcode, Hash,
- Globe, Activity, Braces, SquareCheck, GitFork, Binary, Link, CodeXml,
- WandSparkles, Palette, FileJson, Fingerprint, MousePointer2, Box,
+ Calculator,
+ Ruler,
+ Banknote,
+ Timer,
+ KeyRound,
+ Sparkles,
+ ScanBarcode,
+ Hash,
+ Globe,
+ Activity,
+ Braces,
+ SquareCheck,
+ GitFork,
+ Binary,
+ Link,
+ CodeXml,
+ WandSparkles,
+ Palette,
+ FileJson,
+ Fingerprint,
+ MousePointer2,
+ Box,
} from "lucide-react";
export const CORE_TOOLS: ToolDefinition[] = [
// --- Core: math / converter / generator / network ---
{
- slug: "calculator", name: "Calculator", description: "Run an interactive expression with variables and history.",
- category: "math", tier: "core", icon: Calculator, keywords: ["calculator", "expression", "math", "arithmetic"],
- commands: ["calc", "calculate"], status: "ready", component: tf(() => import("@/components/tools/calculator")),
+ slug: "calculator",
+ name: "Calculator",
+ description: "Run an interactive expression with variables and history.",
+ category: "math",
+ tier: "core",
+ icon: Calculator,
+ keywords: ["calculator", "expression", "math", "arithmetic"],
+ commands: ["calc", "calculate"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/calculator")),
},
{
- slug: "unit-converter", name: "Unit Converter", description: "Convert between length, mass, time, speed, area, volume & data.",
- category: "converter", tier: "core", icon: Ruler, keywords: ["units", "convert", "length", "metric", "imperial", "cm", "inches"],
- commands: ["units", "convert"], status: "ready", component: tf(() => import("@/components/tools/unit-converter")),
+ slug: "unit-converter",
+ name: "Unit Converter",
+ description:
+ "Convert between length, mass, time, speed, area, volume & data.",
+ category: "converter",
+ tier: "core",
+ icon: Ruler,
+ keywords: [
+ "units",
+ "convert",
+ "length",
+ "metric",
+ "imperial",
+ "cm",
+ "inches",
+ ],
+ commands: ["units", "convert"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/unit-converter")),
},
{
- slug: "currency-converter", name: "Currency Converter", description: "Convert between 30+ world currencies with offline rates.",
- category: "converter", tier: "core", icon: Banknote, keywords: ["currency", "money", "exchange", "usd", "eur", "foreign"],
- commands: ["currency", "money"], status: "ready", component: tf(() => import("@/components/tools/currency-converter")),
+ slug: "currency-converter",
+ name: "Currency Converter",
+ description: "Convert between 30+ world currencies with offline rates.",
+ category: "converter",
+ tier: "core",
+ icon: Banknote,
+ keywords: ["currency", "money", "exchange", "usd", "eur", "foreign"],
+ commands: ["currency", "money"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/currency-converter")),
},
{
- slug: "timestamp", name: "Epoch Timestamp", description: "Convert between unix epoch and human-readable dates & timezones.",
- category: "converter", tier: "core", icon: Timer, keywords: ["timestamp", "epoch", "unix", "date", "time", "utc"],
- commands: ["time", "timestamp"], status: "ready", component: tf(() => import("@/components/tools/timestamp")),
+ slug: "timestamp",
+ name: "Epoch Timestamp",
+ description:
+ "Convert between unix epoch and human-readable dates & timezones.",
+ category: "converter",
+ tier: "core",
+ icon: Timer,
+ keywords: ["timestamp", "epoch", "unix", "date", "time", "utc"],
+ commands: ["time", "timestamp"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/timestamp")),
},
{
- slug: "password-generator", name: "Password Generator", description: "Generate strong random passwords and passphrases.",
- category: "generator", tier: "core", icon: KeyRound, keywords: ["password", "passphrase", "random", "secure", "strength"],
- commands: ["password", "passphrase"], status: "ready", component: tf(() => import("@/components/tools/password-generator")),
+ slug: "password-generator",
+ name: "Password Generator",
+ description: "Generate strong random passwords and passphrases.",
+ category: "generator",
+ tier: "core",
+ icon: KeyRound,
+ keywords: ["password", "passphrase", "random", "secure", "strength"],
+ commands: ["password", "passphrase"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/password-generator")),
},
{
- slug: "uuid-generator", name: "UUID Generator", description: "Generate v1, v4 and v7 UUIDs in bulk.",
- category: "generator", tier: "core", icon: Sparkles, keywords: ["uuid", "guid", "random", "identifier", "v4", "generator"],
- commands: ["uuid"], status: "ready", component: tf(() => import("@/components/tools/uuid-generator")),
+ slug: "uuid-generator",
+ name: "UUID Generator",
+ description: "Generate v1, v4 and v7 UUIDs in bulk.",
+ category: "generator",
+ tier: "core",
+ icon: Sparkles,
+ keywords: ["uuid", "guid", "random", "identifier", "v4", "generator"],
+ commands: ["uuid"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/uuid-generator")),
},
{
- slug: "qr-generator", name: "QR Code Generator", description: "Generate scannable QR codes from URLs, text, Wi-Fi and more.",
- category: "generator", tier: "core", icon: ScanBarcode, keywords: ["qr", "qrcode", "barcode", "scan", "wifi"],
- commands: ["qr"], status: "ready", component: tf(() => import("@/components/tools/qr-generator")),
+ slug: "qr-generator",
+ name: "QR Code Generator",
+ description: "Generate scannable QR codes from URLs, text, Wi-Fi and more.",
+ category: "generator",
+ tier: "core",
+ icon: ScanBarcode,
+ keywords: ["qr", "qrcode", "barcode", "scan", "wifi"],
+ commands: ["qr"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/qr-generator")),
},
{
- slug: "hash", name: "Hash Generator", description: "Compute MD5, SHA-1, SHA-256, SHA-384, SHA-512 and more.",
- category: "crypto", tier: "core", icon: Hash, keywords: ["hash", "md5", "sha", "checksum", "digest", "verify"],
- commands: ["hash"], status: "ready", component: tf(() => import("@/components/tools/hash")),
+ slug: "hash",
+ name: "Hash Generator",
+ description: "Compute MD5, SHA-1, SHA-256, SHA-384, SHA-512 and more.",
+ category: "crypto",
+ tier: "core",
+ icon: Hash,
+ keywords: ["hash", "md5", "sha", "checksum", "digest", "verify"],
+ commands: ["hash"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/hash")),
},
{
- slug: "dns-lookup", name: "DNS Lookup", description: "Resolve A, AAAA, MX, TXT, NS, CNAME and SOA records.",
- category: "network", tier: "core", icon: Globe, keywords: ["dns", "lookup", "domain", "mx", "record", "resolve"],
- commands: ["dns"], status: "ready", component: tf(() => import("@/components/tools/dns-lookup")),
+ slug: "dns-lookup",
+ name: "DNS Lookup",
+ description: "Resolve A, AAAA, MX, TXT, NS, CNAME and SOA records.",
+ category: "network",
+ tier: "core",
+ icon: Globe,
+ keywords: ["dns", "lookup", "domain", "mx", "record", "resolve"],
+ commands: ["dns"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/dns-lookup")),
},
{
- slug: "speed-test", name: "Speed Test", description: "Measure download, upload, latency and jitter against a CDN.",
- category: "network", tier: "core", icon: Activity, keywords: ["speed", "bandwidth", "internet", "ping", "latency", "test"],
- commands: ["speed"], status: "ready", component: tf(() => import("@/components/tools/speed-test")),
+ slug: "speed-test",
+ name: "Speed Test",
+ description: "Measure download, upload, latency and jitter against a CDN.",
+ category: "network",
+ tier: "core",
+ icon: Activity,
+ keywords: ["speed", "bandwidth", "internet", "ping", "latency", "test"],
+ commands: ["speed"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/speed-test")),
},
// --- Coding / developer tools ---
{
- slug: "json-formatter", name: "JSON Formatter", description: "Pretty-print and validate JSON with syntax highlighting.",
- category: "coding", tier: "core", icon: Braces, keywords: ["json", "format", "pretty", "prettify", "beautify"],
- commands: ["json format", "json pretty"], status: "ready", component: tf(() => import("@/components/tools/json-formatter")),
+ slug: "json-formatter",
+ name: "JSON Formatter",
+ description: "Pretty-print and validate JSON with syntax highlighting.",
+ category: "coding",
+ tier: "core",
+ icon: Braces,
+ keywords: ["json", "format", "pretty", "prettify", "beautify"],
+ commands: ["json format", "json pretty"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/json-formatter")),
},
{
- slug: "json-minifier", name: "JSON Minifier", description: "Strip whitespace from JSON to a single compact line.",
- category: "coding", icon: Braces, keywords: ["json", "minify", "compact", "strip", "size"],
- commands: ["json minify"], status: "ready", component: tf(() => import("@/components/tools/coding-tools")),
+ slug: "json-minifier",
+ name: "JSON Minifier",
+ description: "Strip whitespace from JSON to a single compact line.",
+ category: "coding",
+ icon: Braces,
+ keywords: ["json", "minify", "compact", "strip", "size"],
+ commands: ["json minify"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/coding-tools")),
},
{
- slug: "json-validator", name: "JSON Validator", description: "Validate JSON and see the exact line/column/offset of errors.",
- category: "coding", icon: SquareCheck, keywords: ["json", "validate", "check", "error", "syntax"],
- commands: ["json validate"], status: "ready", component: tf(() => import("@/components/tools/coding-tools")),
+ slug: "json-validator",
+ name: "JSON Validator",
+ description:
+ "Validate JSON and see the exact line/column/offset of errors.",
+ category: "coding",
+ icon: SquareCheck,
+ keywords: ["json", "validate", "check", "error", "syntax"],
+ commands: ["json validate"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/coding-tools")),
},
{
- slug: "json-tree-viewer", name: "JSON Tree Viewer", description: "Explore nested JSON with collapsible nodes and search.",
- category: "coding", icon: GitFork, keywords: ["json", "tree", "viewer", "browse", "nested", "explorer"],
- commands: ["json tree"], status: "ready", component: tf(() => import("@/components/tools/json-tree-viewer")),
+ slug: "json-tree-viewer",
+ name: "JSON Tree Viewer",
+ description: "Explore nested JSON with collapsible nodes and search.",
+ category: "coding",
+ icon: GitFork,
+ keywords: ["json", "tree", "viewer", "browse", "nested", "explorer"],
+ commands: ["json tree"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/json-tree-viewer")),
},
{
- slug: "base64", name: "Base64 Encode/Decode", description: "Encode and decode Base64 strings and files.",
- category: "coding", tier: "core", icon: Binary, keywords: ["base64", "encode", "decode", "binary", "encode64"],
- commands: ["base64"], status: "ready", component: tf(() => import("@/components/tools/base64")),
+ slug: "base64",
+ name: "Base64 Encode/Decode",
+ description: "Encode and decode Base64 strings and files.",
+ category: "coding",
+ tier: "core",
+ icon: Binary,
+ keywords: ["base64", "encode", "decode", "binary", "encode64"],
+ commands: ["base64"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/base64")),
},
{
- slug: "url-encoder", name: "URL Encoder", description: "Percent-encode or decode any string for safe URLs.",
- category: "coding", tier: "core", icon: Link, keywords: ["url", "encode", "decode", "percent", "quotestr", "uri"],
- commands: ["url encode"], status: "ready", component: tf(() => import("@/components/tools/url-encoder")),
+ slug: "url-encoder",
+ name: "URL Encoder",
+ description: "Percent-encode or decode any string for safe URLs.",
+ category: "coding",
+ tier: "core",
+ icon: Link,
+ keywords: ["url", "encode", "decode", "percent", "quotestr", "uri"],
+ commands: ["url encode"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/url-encoder")),
},
{
- slug: "url-slug", name: "URL Slug Generator", description: "Turn any text into a clean, lowercase, dash-separated slug.",
- category: "coding", icon: Link, keywords: ["slug", "url", "seo", "permalink", "normalize"],
- commands: ["slug"], status: "ready", component: tf(() => import("@/components/tools/coding-tools")),
+ slug: "url-slug",
+ name: "URL Slug Generator",
+ description: "Turn any text into a clean, lowercase, dash-separated slug.",
+ category: "coding",
+ icon: Link,
+ keywords: ["slug", "url", "seo", "permalink", "normalize"],
+ commands: ["slug"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/coding-tools")),
},
{
- slug: "html-encoder", name: "HTML Encoder", description: "Escape or unescape HTML entities (< & etc.).",
- category: "coding", icon: CodeXml, keywords: ["html", "escape", "unescape", "entities", "encode"],
- commands: ["html encode"], status: "ready", component: tf(() => import("@/components/tools/coding-tools")),
+ slug: "html-encoder",
+ name: "HTML Encoder",
+ description: "Escape or unescape HTML entities (< & etc.).",
+ category: "coding",
+ icon: CodeXml,
+ keywords: ["html", "escape", "unescape", "entities", "encode"],
+ commands: ["html encode"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/coding-tools")),
},
{
- slug: "html-formatter", name: "HTML Formatter", description: "Pretty-print messy HTML with proper indentation.",
- category: "coding", icon: WandSparkles, keywords: ["html", "format", "indent", "pretty", "beautify"],
- commands: ["html format"], status: "ready", component: tf(() => import("@/components/tools/coding-tools")),
+ slug: "html-formatter",
+ name: "HTML Formatter",
+ description: "Pretty-print messy HTML with proper indentation.",
+ category: "coding",
+ icon: WandSparkles,
+ keywords: ["html", "format", "indent", "pretty", "beautify"],
+ commands: ["html format"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/coding-tools")),
},
{
- slug: "html-minifier", name: "HTML Minifier", description: "Compress HTML by removing whitespace and comments.",
- category: "coding", icon: WandSparkles, keywords: ["html", "minify", "compress", "size", "speed"],
- commands: ["html minify"], status: "ready", component: tf(() => import("@/components/tools/coding-tools")),
+ slug: "html-minifier",
+ name: "HTML Minifier",
+ description: "Compress HTML by removing whitespace and comments.",
+ category: "coding",
+ icon: WandSparkles,
+ keywords: ["html", "minify", "compress", "size", "speed"],
+ commands: ["html minify"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/coding-tools")),
},
{
- slug: "css-formatter", name: "CSS Formatter", description: "Beautify and normalize CSS formatting.",
- category: "coding", icon: Palette, keywords: ["css", "format", "indent", "pretty", "beautify"],
- commands: ["css format"], status: "ready", component: tf(() => import("@/components/tools/coding-tools")),
+ slug: "css-formatter",
+ name: "CSS Formatter",
+ description: "Beautify and normalize CSS formatting.",
+ category: "coding",
+ icon: Palette,
+ keywords: ["css", "format", "indent", "pretty", "beautify"],
+ commands: ["css format"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/coding-tools")),
},
{
- slug: "css-minifier", name: "CSS Minifier", description: "Compact CSS to save bytes and speed up pages.",
- category: "coding", icon: Palette, keywords: ["css", "minify", "compress", "size"],
- commands: ["css minify"], status: "ready", component: tf(() => import("@/components/tools/coding-tools")),
+ slug: "css-minifier",
+ name: "CSS Minifier",
+ description: "Compact CSS to save bytes and speed up pages.",
+ category: "coding",
+ icon: Palette,
+ keywords: ["css", "minify", "compress", "size"],
+ commands: ["css minify"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/coding-tools")),
},
{
- slug: "js-formatter", name: "JS Formatter", description: "Pretty-print JavaScript with proper indentation.",
- category: "coding", icon: FileJson, keywords: ["javascript", "format", "beautify", "indent", "prettier"],
- commands: ["js format"], status: "ready", component: tf(() => import("@/components/tools/coding-tools")),
+ slug: "js-formatter",
+ name: "JS Formatter",
+ description: "Pretty-print JavaScript with proper indentation.",
+ category: "coding",
+ icon: FileJson,
+ keywords: ["javascript", "format", "beautify", "indent", "prettier"],
+ commands: ["js format"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/coding-tools")),
},
{
- slug: "js-minifier", name: "JS Minifier", description: "Minify JavaScript to reduce file size.",
- category: "coding", icon: FileJson, keywords: ["javascript", "minify", "compress", "terser"],
- commands: ["js minify"], status: "ready", component: tf(() => import("@/components/tools/coding-tools")),
+ slug: "js-minifier",
+ name: "JS Minifier",
+ description: "Minify JavaScript to reduce file size.",
+ category: "coding",
+ icon: FileJson,
+ keywords: ["javascript", "minify", "compress", "terser"],
+ commands: ["js minify"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/coding-tools")),
},
{
- slug: "md5-hash", name: "MD5 Hash Calculator", description: "Compute the RFC 1321 MD5 digest of any text.",
- category: "coding", icon: Fingerprint, keywords: ["md5", "hash", "digest", "checksum", "rfc1321"],
- commands: ["md5"], status: "ready", component: tf(() => import("@/components/tools/coding-tools")),
+ slug: "md5-hash",
+ name: "MD5 Hash Calculator",
+ description: "Compute the RFC 1321 MD5 digest of any text.",
+ category: "coding",
+ icon: Fingerprint,
+ keywords: ["md5", "hash", "digest", "checksum", "rfc1321"],
+ commands: ["md5"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/coding-tools")),
},
{
- slug: "code-to-image", name: "Code to Image", description: "Turn a code snippet into a stylish shareable image.",
- category: "image", icon: MousePointer2, keywords: ["code", "snippet", "image", "share", "screenshot", "carbon"],
- commands: ["code image"], status: "ready", component: tf(() => import("@/components/tools/code-to-image")),
+ slug: "code-to-image",
+ name: "Code to Image",
+ description: "Turn a code snippet into a stylish shareable image.",
+ category: "image",
+ icon: MousePointer2,
+ keywords: ["code", "snippet", "image", "share", "screenshot", "carbon"],
+ commands: ["code image"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/code-to-image")),
},
{
- slug: "rn-shadow", name: "React Native Shadow Generator", description: "Craft iOS and Android box-shadows as RN style objects.",
- category: "coding", icon: Box, keywords: ["react native", "shadow", "elevation", "ios", "android", "styles"],
- commands: ["rn shadow"], status: "ready", component: tf(() => import("@/components/tools/rn-shadow")),
+ slug: "rn-shadow",
+ name: "React Native Shadow Generator",
+ description: "Craft iOS and Android box-shadows as RN style objects.",
+ category: "coding",
+ icon: Box,
+ keywords: [
+ "react native",
+ "shadow",
+ "elevation",
+ "ios",
+ "android",
+ "styles",
+ ],
+ commands: ["rn shadow"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/rn-shadow")),
},
-];
\ No newline at end of file
+];
diff --git a/artifacts/personal-tool-console/src/lib/registry/css-tools.ts b/artifacts/personal-tool-console/src/lib/registry/css-tools.ts
index 1682be0..c91450e 100644
--- a/artifacts/personal-tool-console/src/lib/registry/css-tools.ts
+++ b/artifacts/personal-tool-console/src/lib/registry/css-tools.ts
@@ -1,74 +1,164 @@
import type { ToolDefinition } from "@/lib/tool-registry";
import { tf } from "@/lib/registry/lazy";
import {
- Palette, Triangle, Droplets, Box, Rows3, Shapes, Spline, LoaderCircle,
- SwitchCamera, SquareCheck, Zap, Columns3,
+ Palette,
+ Triangle,
+ Droplets,
+ Box,
+ Rows3,
+ Shapes,
+ Spline,
+ LoaderCircle,
+ SwitchCamera,
+ SquareCheck,
+ Zap,
+ Columns3,
} from "lucide-react";
export const CSS_TOOLS: ToolDefinition[] = [
{
- slug: "css-gradient-generator", name: "CSS Gradient Generator", description: "Build linear and radial gradients with live preview.",
- category: "css", tier: "standard", icon: Palette, keywords: ["gradient", "css", "linear", "radial", "background", "preview"],
- commands: ["gradient"], status: "ready", component: tf(() => import("@/components/tools/css/css-generators")),
+ slug: "css-gradient-generator",
+ name: "CSS Gradient Generator",
+ description: "Build linear and radial gradients with live preview.",
+ category: "css",
+ tier: "standard",
+ icon: Palette,
+ keywords: ["gradient", "css", "linear", "radial", "background", "preview"],
+ commands: ["gradient"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/css/css-generators")),
},
{
- slug: "css-glassmorphism", name: "Glassmorphism Generator", description: "Create frosted-glass UI effects with backdrop blur.",
- category: "css", icon: Droplets, keywords: ["glass", "glassmorphism", "blur", "frosted", "translucent"],
- commands: ["glass"], status: "ready", component: tf(() => import("@/components/tools/css/css-generators")),
+ slug: "css-glassmorphism",
+ name: "Glassmorphism Generator",
+ description: "Create frosted-glass UI effects with backdrop blur.",
+ category: "css",
+ icon: Droplets,
+ keywords: ["glass", "glassmorphism", "blur", "frosted", "translucent"],
+ commands: ["glass"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/css/css-generators")),
},
{
- slug: "css-triangle-generator", name: "CSS Triangle Generator", description: "Pure-CSS triangles in any direction, size and color.",
- category: "css", icon: Triangle, keywords: ["triangle", "css", "shape", "caret", "arrow"],
- commands: ["triangle"], status: "ready", component: tf(() => import("@/components/tools/css/css-generators")),
+ slug: "css-triangle-generator",
+ name: "CSS Triangle Generator",
+ description: "Pure-CSS triangles in any direction, size and color.",
+ category: "css",
+ icon: Triangle,
+ keywords: ["triangle", "css", "shape", "caret", "arrow"],
+ commands: ["triangle"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/css/css-generators")),
},
{
- slug: "css-box-shadow", name: "Box Shadow Generator", description: "Design layered box-shadows for cards and buttons.",
- category: "css", icon: Box, keywords: ["box shadow", "shadow", "elevation", "css", "layer"],
- commands: ["shadow"], status: "ready", component: tf(() => import("@/components/tools/css/css-generators")),
+ slug: "css-box-shadow",
+ name: "Box Shadow Generator",
+ description: "Design layered box-shadows for cards and buttons.",
+ category: "css",
+ icon: Box,
+ keywords: ["box shadow", "shadow", "elevation", "css", "layer"],
+ commands: ["shadow"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/css/css-generators")),
},
{
- slug: "css-border-radius", name: "Border Radius Generator", description: "Build smooth asymmetric border-radius shapes.",
- category: "css", icon: Rows3, keywords: ["border radius", "rounded", "curve", "css", "shape"],
- commands: ["radius"], status: "ready", component: tf(() => import("@/components/tools/css/css-generators")),
+ slug: "css-border-radius",
+ name: "Border Radius Generator",
+ description: "Build smooth asymmetric border-radius shapes.",
+ category: "css",
+ icon: Rows3,
+ keywords: ["border radius", "rounded", "curve", "css", "shape"],
+ commands: ["radius"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/css/css-generators")),
},
{
- slug: "css-background-patterns", name: "CSS Background Patterns", description: "Seamless repeating patterns made of pure gradients.",
- category: "css", icon: Shapes, keywords: ["pattern", "background", "seamless", "repeating", "css"],
- commands: ["pattern"], status: "ready", component: tf(() => import("@/components/tools/css/css-generators")),
+ slug: "css-background-patterns",
+ name: "CSS Background Patterns",
+ description: "Seamless repeating patterns made of pure gradients.",
+ category: "css",
+ icon: Shapes,
+ keywords: ["pattern", "background", "seamless", "repeating", "css"],
+ commands: ["pattern"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/css/css-generators")),
},
{
- slug: "css-clip-path", name: "CSS Clip Path Generator", description: "Visual polygons to clip any element.",
- category: "css", icon: Spline, keywords: ["clip path", "polygon", "shape", "clip", "css"],
- commands: ["clip"], status: "ready", component: tf(() => import("@/components/tools/css/css-generators")),
+ slug: "css-clip-path",
+ name: "CSS Clip Path Generator",
+ description: "Visual polygons to clip any element.",
+ category: "css",
+ icon: Spline,
+ keywords: ["clip path", "polygon", "shape", "clip", "css"],
+ commands: ["clip"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/css/css-generators")),
},
{
- slug: "css-cubic-bezier", name: "Cubic Bezier Generator", description: "Tune easing curves for buttery animations.",
- category: "css", icon: Spline, keywords: ["cubic bezier", "easing", "curve", "animation", "transition"],
- commands: ["bezier"], status: "ready", component: tf(() => import("@/components/tools/css/css-generators")),
+ slug: "css-cubic-bezier",
+ name: "Cubic Bezier Generator",
+ description: "Tune easing curves for buttery animations.",
+ category: "css",
+ icon: Spline,
+ keywords: ["cubic bezier", "easing", "curve", "animation", "transition"],
+ commands: ["bezier"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/css/css-generators")),
},
{
- slug: "css-loader-generator", name: "CSS Loader Generator", description: "Spinners and loaders with customizable colors.",
- category: "css", icon: LoaderCircle, keywords: ["loader", "spinner", "loading", "animation", "css"],
- commands: ["loader"], status: "ready", component: tf(() => import("@/components/tools/css/css-generators")),
+ slug: "css-loader-generator",
+ name: "CSS Loader Generator",
+ description: "Spinners and loaders with customizable colors.",
+ category: "css",
+ icon: LoaderCircle,
+ keywords: ["loader", "spinner", "loading", "animation", "css"],
+ commands: ["loader"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/css/css-generators")),
},
{
- slug: "css-switch-generator", name: "CSS Toggle Switch", description: "Generate styled checkbox-based toggle switches.",
- category: "css", icon: SwitchCamera, keywords: ["switch", "toggle", "ios", "checkbox", "css"],
- commands: ["switch"], status: "ready", component: tf(() => import("@/components/tools/css/css-generators")),
+ slug: "css-switch-generator",
+ name: "CSS Toggle Switch",
+ description: "Generate styled checkbox-based toggle switches.",
+ category: "css",
+ icon: SwitchCamera,
+ keywords: ["switch", "toggle", "ios", "checkbox", "css"],
+ commands: ["switch"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/css/css-generators")),
},
{
- slug: "css-checkbox-generator", name: "CSS Checkbox Generator", description: "Custom checkboxes with animations and colors.",
- category: "css", icon: SquareCheck, keywords: ["checkbox", "check", "custom", "css", "form"],
- commands: ["checkbox"], status: "ready", component: tf(() => import("@/components/tools/css/css-generators")),
+ slug: "css-checkbox-generator",
+ name: "CSS Checkbox Generator",
+ description: "Custom checkboxes with animations and colors.",
+ category: "css",
+ icon: SquareCheck,
+ keywords: ["checkbox", "check", "custom", "css", "form"],
+ commands: ["checkbox"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/css/css-generators")),
},
{
- slug: "css-text-glitch", name: "Text Glitch Generator", description: "Cyberpunk RGB-split glitch effects with clip-path.",
- category: "css", icon: Zap, keywords: ["glitch", "text", "effect", "rgb", "cyberpunk", "css"],
- commands: ["glitch"], status: "ready", component: tf(() => import("@/components/tools/css/css-generators")),
+ slug: "css-text-glitch",
+ name: "Text Glitch Generator",
+ description: "Cyberpunk RGB-split glitch effects with clip-path.",
+ category: "css",
+ icon: Zap,
+ keywords: ["glitch", "text", "effect", "rgb", "cyberpunk", "css"],
+ commands: ["glitch"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/css/css-generators")),
},
{
- slug: "css-grid-generator", name: "CSS Grid Generator", description: "Lay out a css-grid with configurable columns, gaps and templates.",
- category: "css", icon: Columns3, keywords: ["grid", "columns", "layout", "template", "gaps", "css"],
- commands: ["grid"], status: "ready", component: tf(() => import("@/components/tools/css/css-generators")),
+ slug: "css-grid-generator",
+ name: "CSS Grid Generator",
+ description:
+ "Lay out a css-grid with configurable columns, gaps and templates.",
+ category: "css",
+ icon: Columns3,
+ keywords: ["grid", "columns", "layout", "template", "gaps", "css"],
+ commands: ["grid"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/css/css-generators")),
},
-];
\ No newline at end of file
+];
diff --git a/artifacts/personal-tool-console/src/lib/registry/dev-tools.ts b/artifacts/personal-tool-console/src/lib/registry/dev-tools.ts
index aeb02d4..bdf59df 100644
--- a/artifacts/personal-tool-console/src/lib/registry/dev-tools.ts
+++ b/artifacts/personal-tool-console/src/lib/registry/dev-tools.ts
@@ -1,195 +1,440 @@
import type { ToolDefinition } from "@/lib/tool-registry";
import { tf } from "@/lib/registry/lazy";
import {
- Lock, KeySquare, Shield, KeyRound, Binary, ListChecks, ScanText, Combine,
- GitCompareArrows, Split, SwitchCamera, Search, Eraser, Terminal, FileSearch,
- Timer, WandSparkles, FolderTree, FileCode2, FileDiff, SquareCheck, Braces,
- TextCursorInput, Hexagon, Spline, Droplets, Palette,
+ Lock,
+ KeySquare,
+ Shield,
+ KeyRound,
+ Binary,
+ ListChecks,
+ ScanText,
+ Combine,
+ GitCompareArrows,
+ Split,
+ SwitchCamera,
+ Search,
+ Eraser,
+ Terminal,
+ FileSearch,
+ Timer,
+ WandSparkles,
+ FolderTree,
+ FileCode2,
+ FileDiff,
+ SquareCheck,
+ Braces,
+ TextCursorInput,
+ Hexagon,
+ Spline,
+ Droplets,
+ Palette,
} from "lucide-react";
export const DEV_TOOLS: ToolDefinition[] = [
-
// --- Crypto (WebCrypto, fully in-browser) ---
{
- slug: "aes-cipher", name: "AES Encrypt / Decrypt", description: "Encrypt and decrypt text with AES-128/192/256 in CBC or GCM mode.",
- category: "crypto", tier: "standard", icon: Lock, keywords: ["aes", "encrypt", "decrypt", "cipher", "cbc", "gcm", "aes256"],
- commands: ["aes"], status: "ready", component: tf(() => import("@/components/tools/crypto/crypto-tools")),
- },
- {
- slug: "hmac-generator", name: "HMAC Generator", description: "Compute HMAC-MD5/SHA1/SHA256/SHA512 signatures with any secret key.",
- category: "crypto", icon: KeySquare, keywords: ["hmac", "signature", "auth", "sha", "secret", "key"],
- commands: ["hmac"], status: "ready", component: tf(() => import("@/components/tools/crypto/crypto-tools")),
- },
- {
- slug: "jwt-decoder", name: "JWT Decoder", description: "Decode, segment and verify JWT tokens without sending them anywhere.",
- category: "crypto", icon: Shield, keywords: ["jwt", "token", "decode", "verify", "authentication", "payload"],
- commands: ["jwt"], status: "ready", component: tf(() => import("@/components/tools/crypto/crypto-tools")),
- },
- {
- slug: "crypto-key-gen", name: "Crypto Key Generator", description: "Generate random keys, salts, IVs and secrets of any length.",
- category: "crypto", icon: KeyRound, keywords: ["key", "salt", "iv", "secret", "random", "entropy", "hex"],
- commands: ["keygen", "secret"], status: "ready", component: tf(() => import("@/components/tools/crypto/crypto-tools")),
+ slug: "aes-cipher",
+ name: "AES Encrypt / Decrypt",
+ description:
+ "Encrypt and decrypt text with AES-128/192/256 in CBC or GCM mode.",
+ category: "crypto",
+ tier: "standard",
+ icon: Lock,
+ keywords: ["aes", "encrypt", "decrypt", "cipher", "cbc", "gcm", "aes256"],
+ commands: ["aes"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/crypto/crypto-tools")),
+ },
+ {
+ slug: "hmac-generator",
+ name: "HMAC Generator",
+ description:
+ "Compute HMAC-MD5/SHA1/SHA256/SHA512 signatures with any secret key.",
+ category: "crypto",
+ icon: KeySquare,
+ keywords: ["hmac", "signature", "auth", "sha", "secret", "key"],
+ commands: ["hmac"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/crypto/crypto-tools")),
+ },
+ {
+ slug: "jwt-decoder",
+ name: "JWT Decoder",
+ description:
+ "Decode, segment and verify JWT tokens without sending them anywhere.",
+ category: "crypto",
+ icon: Shield,
+ keywords: ["jwt", "token", "decode", "verify", "authentication", "payload"],
+ commands: ["jwt"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/crypto/crypto-tools")),
+ },
+ {
+ slug: "crypto-key-gen",
+ name: "Crypto Key Generator",
+ description: "Generate random keys, salts, IVs and secrets of any length.",
+ category: "crypto",
+ icon: KeyRound,
+ keywords: ["key", "salt", "iv", "secret", "random", "entropy", "hex"],
+ commands: ["keygen", "secret"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/crypto/crypto-tools")),
},
// --- Bitmap / binary ---
{
- slug: "radix-converter", name: "Base / Radix Converter", description: "Convert numbers between binary, octal, decimal and hex (2–36).",
- category: "bitmap", tier: "standard", icon: Binary, keywords: ["radix", "base", "binary", "hex", "octal", "decimal", "base64"],
- commands: ["radix", "base"], status: "ready", component: tf(() => import("@/components/tools/bitmap/bitmap-tools")),
- },
- {
- slug: "bitwise-calculator", name: "Bitwise Calculator", description: "AND, OR, XOR, NOT and shifts on int8/16/32/64 values.",
- category: "bitmap", icon: Binary, keywords: ["bitwise", "binary", "and", "xor", "or", "shift", "mask"],
- commands: ["bitwise"], status: "ready", component: tf(() => import("@/components/tools/bitmap/bitmap-tools")),
- },
- {
- slug: "bit-flags", name: "Bit Flags Builder", description: "Combine and decode 8/16/32-bit permission-style bit flags.",
- category: "bitmap", icon: ListChecks, keywords: ["flags", "bitmask", "permissions", "bits", "enum", "flags builder"],
- commands: ["flags"], status: "ready", component: tf(() => import("@/components/tools/bitmap/bitmap-tools")),
- },
- {
- slug: "binary-viewer", name: "Binary Viewer", description: "Inspect a string as UTF-8 bytes, codepoints and binary.",
- category: "bitmap", icon: ScanText, keywords: ["binary", "bytes", "hex", "viewer", "dump", "bits"],
- commands: ["binary"], status: "ready", component: tf(() => import("@/components/tools/bitmap/bitmap-tools")),
+ slug: "radix-converter",
+ name: "Base / Radix Converter",
+ description:
+ "Convert numbers between binary, octal, decimal and hex (2–36).",
+ category: "bitmap",
+ tier: "standard",
+ icon: Binary,
+ keywords: ["radix", "base", "binary", "hex", "octal", "decimal", "base64"],
+ commands: ["radix", "base"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/bitmap/bitmap-tools")),
+ },
+ {
+ slug: "bitwise-calculator",
+ name: "Bitwise Calculator",
+ description: "AND, OR, XOR, NOT and shifts on int8/16/32/64 values.",
+ category: "bitmap",
+ icon: Binary,
+ keywords: ["bitwise", "binary", "and", "xor", "or", "shift", "mask"],
+ commands: ["bitwise"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/bitmap/bitmap-tools")),
+ },
+ {
+ slug: "bit-flags",
+ name: "Bit Flags Builder",
+ description: "Combine and decode 8/16/32-bit permission-style bit flags.",
+ category: "bitmap",
+ icon: ListChecks,
+ keywords: [
+ "flags",
+ "bitmask",
+ "permissions",
+ "bits",
+ "enum",
+ "flags builder",
+ ],
+ commands: ["flags"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/bitmap/bitmap-tools")),
+ },
+ {
+ slug: "binary-viewer",
+ name: "Binary Viewer",
+ description: "Inspect a string as UTF-8 bytes, codepoints and binary.",
+ category: "bitmap",
+ icon: ScanText,
+ keywords: ["binary", "bytes", "hex", "viewer", "dump", "bits"],
+ commands: ["binary"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/bitmap/bitmap-tools")),
},
// --- Set operations ---
{
- slug: "set-union", name: "Set Union", description: "Merge two lists of lines, keeping each unique item once.",
- category: "set", tier: "advanced", icon: Combine, keywords: ["set", "union", "merge", "combine", "unique"],
- commands: ["set union"], status: "ready", component: tf(() => import("@/components/tools/set/set-tools")),
- },
- {
- slug: "set-intersection", name: "Set Intersection", description: "Items present in both lists of lines.",
- category: "set", icon: GitCompareArrows, keywords: ["set", "intersection", "common", "both", "overlap"],
- commands: ["set intersection"], status: "ready", component: tf(() => import("@/components/tools/set/set-tools")),
- },
- {
- slug: "set-difference", name: "Set Difference", description: "Items in list A that are NOT in list B.",
- category: "set", icon: Split, keywords: ["set", "difference", "subtract", "minus"],
- commands: ["set difference"], status: "ready", component: tf(() => import("@/components/tools/set/set-tools")),
- },
- {
- slug: "set-symmetric-difference", name: "Symmetric Difference", description: "Items in either list but NOT in both (A △ B).",
- category: "set", icon: SwitchCamera, keywords: ["set", "symmetric", "xor", "exclusive", "not both"],
- commands: ["set xor"], status: "ready", component: tf(() => import("@/components/tools/set/set-tools")),
- },
- {
- slug: "set-membership", name: "Set Membership Checker", description: "Test which items of a list exist in another list.",
- category: "set", icon: Search, keywords: ["set", "membership", "contains", "include", "filter", "exists"],
- commands: ["set membership"], status: "ready", component: tf(() => import("@/components/tools/set/set-tools")),
- },
- {
- slug: "set-dedupe", name: "Dedupe Lines", description: "Remove duplicate lines while preserving order.",
- category: "set", icon: Eraser, keywords: ["dedupe", "deduplicate", "unique", "duplicate", "distinct"],
- commands: ["dedupe"], status: "ready", component: tf(() => import("@/components/tools/set/set-tools")),
+ slug: "set-union",
+ name: "Set Union",
+ description: "Merge two lists of lines, keeping each unique item once.",
+ category: "set",
+ tier: "advanced",
+ icon: Combine,
+ keywords: ["set", "union", "merge", "combine", "unique"],
+ commands: ["set union"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/set/set-tools")),
+ },
+ {
+ slug: "set-intersection",
+ name: "Set Intersection",
+ description: "Items present in both lists of lines.",
+ category: "set",
+ icon: GitCompareArrows,
+ keywords: ["set", "intersection", "common", "both", "overlap"],
+ commands: ["set intersection"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/set/set-tools")),
+ },
+ {
+ slug: "set-difference",
+ name: "Set Difference",
+ description: "Items in list A that are NOT in list B.",
+ category: "set",
+ icon: Split,
+ keywords: ["set", "difference", "subtract", "minus"],
+ commands: ["set difference"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/set/set-tools")),
+ },
+ {
+ slug: "set-symmetric-difference",
+ name: "Symmetric Difference",
+ description: "Items in either list but NOT in both (A △ B).",
+ category: "set",
+ icon: SwitchCamera,
+ keywords: ["set", "symmetric", "xor", "exclusive", "not both"],
+ commands: ["set xor"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/set/set-tools")),
+ },
+ {
+ slug: "set-membership",
+ name: "Set Membership Checker",
+ description: "Test which items of a list exist in another list.",
+ category: "set",
+ icon: Search,
+ keywords: ["set", "membership", "contains", "include", "filter", "exists"],
+ commands: ["set membership"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/set/set-tools")),
+ },
+ {
+ slug: "set-dedupe",
+ name: "Dedupe Lines",
+ description: "Remove duplicate lines while preserving order.",
+ category: "set",
+ icon: Eraser,
+ keywords: ["dedupe", "deduplicate", "unique", "duplicate", "distinct"],
+ commands: ["dedupe"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/set/set-tools")),
},
// --- Linux ---
{
- slug: "chmod-calculator", name: "chmod Calculator", description: "Convert symbolic (rwx) permissions to octal and back.",
- category: "linux", tier: "standard", icon: Shield, keywords: ["chmod", "permissions", "rwx", "octal", "symbolic", "770"],
- commands: ["chmod"], status: "ready", component: tf(() => import("@/components/tools/linux/linux-tools")),
- },
- {
- slug: "permissions-decoder", name: "ls -l Permissions Decoder", description: "Decode a drwxr-xr-x permission string into its meaning.",
- category: "linux", icon: FileSearch, keywords: ["ls", "permissions", "decode", "drwx", "owner", "group"],
- commands: ["ls -l"], status: "ready", component: tf(() => import("@/components/tools/linux/linux-tools")),
- },
- {
- slug: "cron-parser", name: "Cron Expression Parser", description: "Parse any cron expression and list the next five runs.",
- category: "linux", tier: "advanced", icon: Timer, keywords: ["cron", "parser", "schedule", "expression", "job"],
- commands: ["cron parse"], status: "ready", component: tf(() => import("@/components/tools/linux/linux-tools")),
- },
- {
- slug: "cron-generator", name: "Cron Generator", description: "Build cron expressions visually minute by minute.",
- category: "linux", icon: WandSparkles, keywords: ["cron", "generator", "schedule", "every", "expression"],
- commands: ["cron generate"], status: "ready", component: tf(() => import("@/components/tools/linux/linux-tools")),
- },
- {
- slug: "path-utils", name: "POSIX Path Utilities", description: "Join, resolve, normalize, dirname & basename paths.",
- category: "linux", icon: FolderTree, keywords: ["path", "posix", "dirname", "basename", "join", "linux"],
- commands: ["path"], status: "ready", component: tf(() => import("@/components/tools/linux/linux-tools")),
- },
- {
- slug: "uid-gid-table", name: "UID & GID Table", description: "Well-known Linux UIDs/GIDs for users, daemons and groups.",
- category: "linux", icon: ListChecks, keywords: ["uid", "gid", "linux", "users", "root", "daemon"],
- commands: ["uid", "gid"], status: "ready", component: tf(() => import("@/components/tools/linux/linux-tools")),
+ slug: "chmod-calculator",
+ name: "chmod Calculator",
+ description: "Convert symbolic (rwx) permissions to octal and back.",
+ category: "linux",
+ tier: "standard",
+ icon: Shield,
+ keywords: ["chmod", "permissions", "rwx", "octal", "symbolic", "770"],
+ commands: ["chmod"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/linux/linux-tools")),
+ },
+ {
+ slug: "permissions-decoder",
+ name: "ls -l Permissions Decoder",
+ description: "Decode a drwxr-xr-x permission string into its meaning.",
+ category: "linux",
+ icon: FileSearch,
+ keywords: ["ls", "permissions", "decode", "drwx", "owner", "group"],
+ commands: ["ls -l"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/linux/linux-tools")),
+ },
+ {
+ slug: "cron-parser",
+ name: "Cron Expression Parser",
+ description: "Parse any cron expression and list the next five runs.",
+ category: "linux",
+ tier: "advanced",
+ icon: Timer,
+ keywords: ["cron", "parser", "schedule", "expression", "job"],
+ commands: ["cron parse"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/linux/linux-tools")),
+ },
+ {
+ slug: "cron-generator",
+ name: "Cron Generator",
+ description: "Build cron expressions visually minute by minute.",
+ category: "linux",
+ icon: WandSparkles,
+ keywords: ["cron", "generator", "schedule", "every", "expression"],
+ commands: ["cron generate"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/linux/linux-tools")),
+ },
+ {
+ slug: "path-utils",
+ name: "POSIX Path Utilities",
+ description: "Join, resolve, normalize, dirname & basename paths.",
+ category: "linux",
+ icon: FolderTree,
+ keywords: ["path", "posix", "dirname", "basename", "join", "linux"],
+ commands: ["path"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/linux/linux-tools")),
+ },
+ {
+ slug: "uid-gid-table",
+ name: "UID & GID Table",
+ description: "Well-known Linux UIDs/GIDs for users, daemons and groups.",
+ category: "linux",
+ icon: ListChecks,
+ keywords: ["uid", "gid", "linux", "users", "root", "daemon"],
+ commands: ["uid", "gid"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/linux/linux-tools")),
},
// --- XML ---
{
- slug: "xml-formatter", name: "XML Formatter", description: "Pretty-print XML with configurable indentation.",
- category: "xml", tier: "standard", icon: FileCode2, keywords: ["xml", "format", "pretty", "indent", "beautify"],
- commands: ["xml format"], status: "ready", component: tf(() => import("@/components/tools/xml/xml-tools")),
- },
- {
- slug: "xml-minifier", name: "XML Minifier", description: "Compact XML by removing ignorable whitespace.",
- category: "xml", icon: FileDiff, keywords: ["xml", "minify", "compact", "space", "size"],
- commands: ["xml minify"], status: "ready", component: tf(() => import("@/components/tools/xml/xml-tools")),
- },
- {
- slug: "xml-validator", name: "XML Validator", description: "Validate XML well-formedness and show the exact error.",
- category: "xml", icon: SquareCheck, keywords: ["xml", "validate", "wellformed", "error", "parse"],
- commands: ["xml validate"], status: "ready", component: tf(() => import("@/components/tools/xml/xml-tools")),
- },
- {
- slug: "xml-json-converter", name: "XML ⇄ JSON Converter", description: "Convert simple XML documents to JSON and back.",
- category: "xml", icon: Braces, keywords: ["xml", "json", "convert", "transform"],
- commands: ["xml json"], status: "ready", component: tf(() => import("@/components/tools/xml/xml-tools")),
+ slug: "xml-formatter",
+ name: "XML Formatter",
+ description: "Pretty-print XML with configurable indentation.",
+ category: "xml",
+ tier: "standard",
+ icon: FileCode2,
+ keywords: ["xml", "format", "pretty", "indent", "beautify"],
+ commands: ["xml format"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/xml/xml-tools")),
+ },
+ {
+ slug: "xml-minifier",
+ name: "XML Minifier",
+ description: "Compact XML by removing ignorable whitespace.",
+ category: "xml",
+ icon: FileDiff,
+ keywords: ["xml", "minify", "compact", "space", "size"],
+ commands: ["xml minify"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/xml/xml-tools")),
+ },
+ {
+ slug: "xml-validator",
+ name: "XML Validator",
+ description: "Validate XML well-formedness and show the exact error.",
+ category: "xml",
+ icon: SquareCheck,
+ keywords: ["xml", "validate", "wellformed", "error", "parse"],
+ commands: ["xml validate"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/xml/xml-tools")),
+ },
+ {
+ slug: "xml-json-converter",
+ name: "XML ⇄ JSON Converter",
+ description: "Convert simple XML documents to JSON and back.",
+ category: "xml",
+ icon: Braces,
+ keywords: ["xml", "json", "convert", "transform"],
+ commands: ["xml json"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/xml/xml-tools")),
},
// --- UTF-8 ---
{
- slug: "utf8-encoder", name: "UTF-8 Encoder", description: "Encode and decode UTF-8 bytes from text.",
- category: "utf8", tier: "advanced", icon: TextCursorInput, keywords: ["utf8", "utf-8", "encode", "decode", "bytes"],
- commands: ["utf8"], status: "ready", component: tf(() => import("@/components/tools/utf8/utf8-tools")),
- },
- {
- slug: "codepoint-inspector", name: "Codepoint Inspector", description: "Inspect every Unicode codepoint and its decomposition.",
- category: "utf8", icon: ScanText, keywords: ["codepoint", "unicode", "char", "utf-16", "utf-32"],
- commands: ["codepoint"], status: "ready", component: tf(() => import("@/components/tools/utf8/utf8-tools")),
- },
- {
- slug: "utf8-validator", name: "UTF-8 Validator", description: "Check whether a byte sequence is valid UTF-8.",
- category: "utf8", icon: SquareCheck, keywords: ["utf8", "validate", "bytes", "invalid", "mojibake"],
- commands: ["utf8 validate"], status: "ready", component: tf(() => import("@/components/tools/utf8/utf8-tools")),
- },
- {
- slug: "bom-utilities", name: "BOM Utilities", description: "Add, strip and inspect UTF-8/16/32 byte-order marks.",
- category: "utf8", icon: FileSearch, keywords: ["bom", "byte order mark", "ef bb bf", "utf8", "header"],
- commands: ["bom"], status: "ready", component: tf(() => import("@/components/tools/utf8/utf8-tools")),
- },
- {
- slug: "escape-sequences", name: "Escape / Unescape", description: "Escape & unescape \\n \\t \\uXXXX and hex sequences.",
- category: "utf8", icon: Braces, keywords: ["escape", "unescape", "backslash", "newline", "unicode escape"],
- commands: ["escape"], status: "ready", component: tf(() => import("@/components/tools/utf8/utf8-tools")),
+ slug: "utf8-encoder",
+ name: "UTF-8 Encoder",
+ description: "Encode and decode UTF-8 bytes from text.",
+ category: "utf8",
+ tier: "advanced",
+ icon: TextCursorInput,
+ keywords: ["utf8", "utf-8", "encode", "decode", "bytes"],
+ commands: ["utf8"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/utf8/utf8-tools")),
+ },
+ {
+ slug: "codepoint-inspector",
+ name: "Codepoint Inspector",
+ description: "Inspect every Unicode codepoint and its decomposition.",
+ category: "utf8",
+ icon: ScanText,
+ keywords: ["codepoint", "unicode", "char", "utf-16", "utf-32"],
+ commands: ["codepoint"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/utf8/utf8-tools")),
+ },
+ {
+ slug: "utf8-validator",
+ name: "UTF-8 Validator",
+ description: "Check whether a byte sequence is valid UTF-8.",
+ category: "utf8",
+ icon: SquareCheck,
+ keywords: ["utf8", "validate", "bytes", "invalid", "mojibake"],
+ commands: ["utf8 validate"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/utf8/utf8-tools")),
+ },
+ {
+ slug: "bom-utilities",
+ name: "BOM Utilities",
+ description: "Add, strip and inspect UTF-8/16/32 byte-order marks.",
+ category: "utf8",
+ icon: FileSearch,
+ keywords: ["bom", "byte order mark", "ef bb bf", "utf8", "header"],
+ commands: ["bom"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/utf8/utf8-tools")),
+ },
+ {
+ slug: "escape-sequences",
+ name: "Escape / Unescape",
+ description: "Escape & unescape \\n \\t \\uXXXX and hex sequences.",
+ category: "utf8",
+ icon: Braces,
+ keywords: ["escape", "unescape", "backslash", "newline", "unicode escape"],
+ commands: ["escape"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/utf8/utf8-tools")),
},
// --- Color ---
{
- slug: "color-palette", name: "Color Palette Generator", description: "Build a cohesive palette from a seed color.",
- category: "color", tier: "standard", icon: Palette, keywords: ["palette", "color", "scheme", "design", "harmony"],
- commands: ["palette"], status: "ready", component: tf(() => import("@/components/tools/color/color-tools")),
- },
- {
- slug: "hex-to-rgba", name: "Hex → RGBA", description: "Convert HEX colors to rgba() effortlessly.",
- category: "color", icon: Hexagon, keywords: ["hex", "rgba", "convert", "color", "css"],
- commands: ["hex to rgba"], status: "ready", component: tf(() => import("@/components/tools/color/color-tools")),
- },
- {
- slug: "rgba-to-hex", name: "RGBA → Hex", description: "Convert rgba() colors back to #rrggbb or #rrggbbaa.",
- category: "color", icon: Hexagon, keywords: ["rgba", "hex", "convert", "color", "css"],
- commands: ["rgba to hex"], status: "ready", component: tf(() => import("@/components/tools/color/color-tools")),
- },
- {
- slug: "color-shades", name: "Color Shades Generator", description: "Generate tints and shades from 10% to 90%.",
- category: "color", icon: Spline, keywords: ["shades", "tints", "lighten", "darken", "color", "scale"],
- commands: ["shades"], status: "ready", component: tf(() => import("@/components/tools/color/color-tools")),
- },
- {
- slug: "color-mixer", name: "Color Mixer", description: "Blend two colors with a mixing ratio.",
- category: "color", icon: Droplets, keywords: ["mix", "blend", "color", "ratio", "gradient"],
- commands: ["mix colors"], status: "ready", component: tf(() => import("@/components/tools/color/color-tools")),
- },
-];
\ No newline at end of file
+ slug: "color-palette",
+ name: "Color Palette Generator",
+ description: "Build a cohesive palette from a seed color.",
+ category: "color",
+ tier: "standard",
+ icon: Palette,
+ keywords: ["palette", "color", "scheme", "design", "harmony"],
+ commands: ["palette"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/color/color-tools")),
+ },
+ {
+ slug: "hex-to-rgba",
+ name: "Hex → RGBA",
+ description: "Convert HEX colors to rgba() effortlessly.",
+ category: "color",
+ icon: Hexagon,
+ keywords: ["hex", "rgba", "convert", "color", "css"],
+ commands: ["hex to rgba"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/color/color-tools")),
+ },
+ {
+ slug: "rgba-to-hex",
+ name: "RGBA → Hex",
+ description: "Convert rgba() colors back to #rrggbb or #rrggbbaa.",
+ category: "color",
+ icon: Hexagon,
+ keywords: ["rgba", "hex", "convert", "color", "css"],
+ commands: ["rgba to hex"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/color/color-tools")),
+ },
+ {
+ slug: "color-shades",
+ name: "Color Shades Generator",
+ description: "Generate tints and shades from 10% to 90%.",
+ category: "color",
+ icon: Spline,
+ keywords: ["shades", "tints", "lighten", "darken", "color", "scale"],
+ commands: ["shades"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/color/color-tools")),
+ },
+ {
+ slug: "color-mixer",
+ name: "Color Mixer",
+ description: "Blend two colors with a mixing ratio.",
+ category: "color",
+ icon: Droplets,
+ keywords: ["mix", "blend", "color", "ratio", "gradient"],
+ commands: ["mix colors"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/color/color-tools")),
+ },
+];
diff --git a/artifacts/personal-tool-console/src/lib/registry/lazy.ts b/artifacts/personal-tool-console/src/lib/registry/lazy.ts
index 5835a5e..61d417b 100644
--- a/artifacts/personal-tool-console/src/lib/registry/lazy.ts
+++ b/artifacts/personal-tool-console/src/lib/registry/lazy.ts
@@ -4,4 +4,4 @@ import type { ToolComponent } from "@/lib/tool-registry";
export function tf
(loader: () => Promise): ToolComponent {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return lazy(loader as any) as unknown as ToolComponent;
-}
\ No newline at end of file
+}
diff --git a/artifacts/personal-tool-console/src/lib/registry/media-tools.ts b/artifacts/personal-tool-console/src/lib/registry/media-tools.ts
index 9774a2b..6b42667 100644
--- a/artifacts/personal-tool-console/src/lib/registry/media-tools.ts
+++ b/artifacts/personal-tool-console/src/lib/registry/media-tools.ts
@@ -1,162 +1,357 @@
import type { ToolDefinition } from "@/lib/tool-registry";
import { tf } from "@/lib/registry/lazy";
import {
- Crop, SlidersHorizontal, Droplets, MousePointer2, Shapes, Grip, ScanText,
- FileText, Scissors, Download, Share2, SwitchCamera, MessageCircle, Globe,
- WandSparkles, Quote, Star, Repeat, Volume2, Image, Shuffle, CreditCard,
- ScanBarcode, Dices, User, ListOrdered,
+ Crop,
+ SlidersHorizontal,
+ Droplets,
+ MousePointer2,
+ Shapes,
+ Grip,
+ ScanText,
+ FileText,
+ Scissors,
+ Download,
+ Share2,
+ SwitchCamera,
+ MessageCircle,
+ Globe,
+ WandSparkles,
+ Quote,
+ Star,
+ Repeat,
+ Volume2,
+ Image,
+ Shuffle,
+ CreditCard,
+ ScanBarcode,
+ Dices,
+ User,
+ ListOrdered,
} from "lucide-react";
export const MEDIA_TOOLS: ToolDefinition[] = [
-
// --- Image ---
{
- slug: "image-resizer", name: "Image Resizer", description: "Resize and scale images by pixels or percentage in your browser.",
- category: "image", icon: Crop, keywords: ["resize", "image", "scale", "dimension", "width", "height"],
- commands: ["resize"], status: "ready", component: tf(() => import("@/components/tools/image/image-tools")),
+ slug: "image-resizer",
+ name: "Image Resizer",
+ description:
+ "Resize and scale images by pixels or percentage in your browser.",
+ category: "image",
+ icon: Crop,
+ keywords: ["resize", "image", "scale", "dimension", "width", "height"],
+ commands: ["resize"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/image/image-tools")),
},
{
- slug: "image-filters", name: "Image Filters", description: "Apply brightness, contrast, blur, sepia and more.",
- category: "image", icon: SlidersHorizontal, keywords: ["filter", "image", "effect", "brightness", "contrast", "sepia"],
- commands: ["filters"], status: "ready", component: tf(() => import("@/components/tools/image/image-tools")),
+ slug: "image-filters",
+ name: "Image Filters",
+ description: "Apply brightness, contrast, blur, sepia and more.",
+ category: "image",
+ icon: SlidersHorizontal,
+ keywords: ["filter", "image", "effect", "brightness", "contrast", "sepia"],
+ commands: ["filters"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/image/image-tools")),
},
{
- slug: "image-avg-color", name: "Average Image Color", description: "Extract the dominant average color from an image.",
- category: "image", icon: Droplets, keywords: ["average", "color", "dominant", "extract", "image", "palette"],
- commands: ["avg color"], status: "ready", component: tf(() => import("@/components/tools/image/image-tools")),
+ slug: "image-avg-color",
+ name: "Average Image Color",
+ description: "Extract the dominant average color from an image.",
+ category: "image",
+ icon: Droplets,
+ keywords: ["average", "color", "dominant", "extract", "image", "palette"],
+ commands: ["avg color"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/image/image-tools")),
},
{
- slug: "image-color-picker", name: "Color Picker from Image", description: "Pick any pixel color from your own image.",
- category: "image", icon: MousePointer2, keywords: ["picker", "eyedropper", "color", "pixel", "image"],
- commands: ["pick color"], status: "ready", component: tf(() => import("@/components/tools/image/image-tools")),
+ slug: "image-color-picker",
+ name: "Color Picker from Image",
+ description: "Pick any pixel color from your own image.",
+ category: "image",
+ icon: MousePointer2,
+ keywords: ["picker", "eyedropper", "color", "pixel", "image"],
+ commands: ["pick color"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/image/image-tools")),
},
{
- slug: "svg-blob-generator", name: "SVG Blob Generator", description: "Organic blob shapes as SVG paths and CSS.",
- category: "image", icon: Shapes, keywords: ["blob", "svg", "organic", "shape", "path", "fluid"],
- commands: ["blob"], status: "ready", component: tf(() => import("@/components/tools/image/image-tools")),
+ slug: "svg-blob-generator",
+ name: "SVG Blob Generator",
+ description: "Organic blob shapes as SVG paths and CSS.",
+ category: "image",
+ icon: Shapes,
+ keywords: ["blob", "svg", "organic", "shape", "path", "fluid"],
+ commands: ["blob"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/image/image-tools")),
},
{
- slug: "svg-pattern-generator", name: "SVG Pattern Generator", description: "Tiled geometric SVG patterns with colors.",
- category: "image", icon: Grip, keywords: ["pattern", "svg", "tile", "seamless", "texture"],
- commands: ["svg pattern"], status: "ready", component: tf(() => import("@/components/tools/image/image-tools")),
+ slug: "svg-pattern-generator",
+ name: "SVG Pattern Generator",
+ description: "Tiled geometric SVG patterns with colors.",
+ category: "image",
+ icon: Grip,
+ keywords: ["pattern", "svg", "tile", "seamless", "texture"],
+ commands: ["svg pattern"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/image/image-tools")),
},
{
- slug: "image-to-base64", name: "Image to Base64", description: "Convert images to base64 data URIs to inline in HTML/CSS.",
- category: "image", icon: ScanText, keywords: ["image", "base64", "data uri", "inline", "embed"],
- commands: ["image base64"], status: "ready", component: tf(() => import("@/components/tools/image/image-tools")),
+ slug: "image-to-base64",
+ name: "Image to Base64",
+ description: "Convert images to base64 data URIs to inline in HTML/CSS.",
+ category: "image",
+ icon: ScanText,
+ keywords: ["image", "base64", "data uri", "inline", "embed"],
+ commands: ["image base64"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/image/image-tools")),
},
{
- slug: "online-sentence-picker", name: "Sentence Picker", description: "Pick a random sentence from a block of text.",
- category: "misc", icon: ListOrdered, keywords: ["sentence", "random", "pick", "choose", "randomizer"],
- commands: ["pick sentence"], status: "ready", component: tf(() => import("@/components/tools/misc/misc-tools")),
+ slug: "online-sentence-picker",
+ name: "Sentence Picker",
+ description: "Pick a random sentence from a block of text.",
+ category: "misc",
+ icon: ListOrdered,
+ keywords: ["sentence", "random", "pick", "choose", "randomizer"],
+ commands: ["pick sentence"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/misc/misc-tools")),
},
{
- slug: "pdf-merger", name: "PDF Merger", description: "Combine multiple PDF files into one document.",
- category: "image", icon: FileText, keywords: ["pdf", "merge", "combine", "join", "documents"],
- commands: ["pdf merge"], status: "coming-soon",
+ slug: "pdf-merger",
+ name: "PDF Merger",
+ description: "Combine multiple PDF files into one document.",
+ category: "image",
+ icon: FileText,
+ keywords: ["pdf", "merge", "combine", "join", "documents"],
+ commands: ["pdf merge"],
+ status: "coming-soon",
},
{
- slug: "pdf-splitter", name: "PDF Splitter", description: "Split pages out of a PDF into separate files.",
- category: "image", icon: Scissors, keywords: ["pdf", "split", "pages", "extract", "separate"],
- commands: ["pdf split"], status: "coming-soon",
+ slug: "pdf-splitter",
+ name: "PDF Splitter",
+ description: "Split pages out of a PDF into separate files.",
+ category: "image",
+ icon: Scissors,
+ keywords: ["pdf", "split", "pages", "extract", "separate"],
+ commands: ["pdf split"],
+ status: "coming-soon",
},
{
- slug: "pdf-compressor", name: "PDF Compressor", description: "Reduce PDF file size by recompressing images.",
- category: "image", icon: Download, keywords: ["pdf", "compress", "reduce", "size", "optimize"],
- commands: ["pdf compress"], status: "coming-soon",
+ slug: "pdf-compressor",
+ name: "PDF Compressor",
+ description: "Reduce PDF file size by recompressing images.",
+ category: "image",
+ icon: Download,
+ keywords: ["pdf", "compress", "reduce", "size", "optimize"],
+ commands: ["pdf compress"],
+ status: "coming-soon",
},
// --- Social ---
{
- slug: "og-meta-generator", name: "Open Graph Meta Generator", description: "Generate OG and Twitter card meta tags for any URL.",
- category: "social", tier: "standard", icon: Share2, keywords: ["og", "open graph", "meta", "twitter card", "seo", "preview"],
- commands: ["og meta"], status: "ready", component: tf(() => import("@/components/tools/social/social-tools")),
+ slug: "og-meta-generator",
+ name: "Open Graph Meta Generator",
+ description: "Generate OG and Twitter card meta tags for any URL.",
+ category: "social",
+ tier: "standard",
+ icon: Share2,
+ keywords: ["og", "open graph", "meta", "twitter card", "seo", "preview"],
+ commands: ["og meta"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/social/social-tools")),
},
{
- slug: "instagram-filters", name: "Instagram Filter Preview", description: "Preview Instagram-style filters on your photos.",
- category: "social", icon: SwitchCamera, keywords: ["instagram", "filter", "photo", "preset", "likes"],
- commands: ["insta filter"], status: "ready", component: tf(() => import("@/components/tools/social/social-tools")),
+ slug: "instagram-filters",
+ name: "Instagram Filter Preview",
+ description: "Preview Instagram-style filters on your photos.",
+ category: "social",
+ icon: SwitchCamera,
+ keywords: ["instagram", "filter", "photo", "preset", "likes"],
+ commands: ["insta filter"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/social/social-tools")),
},
{
- slug: "youtube-thumbnail", name: "YouTube Thumbnail Size", description: "Get the correct YouTube thumbnail sizes and URLs.",
- category: "social", icon: Volume2, keywords: ["youtube", "thumbnail", "size", "1280", "720", "maxres"],
- commands: ["yt thumbnail"], status: "ready", component: tf(() => import("@/components/tools/social/social-tools")),
+ slug: "youtube-thumbnail",
+ name: "YouTube Thumbnail Size",
+ description: "Get the correct YouTube thumbnail sizes and URLs.",
+ category: "social",
+ icon: Volume2,
+ keywords: ["youtube", "thumbnail", "size", "1280", "720", "maxres"],
+ commands: ["yt thumbnail"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/social/social-tools")),
},
{
- slug: "vimeo-thumbnail", name: "Vimeo Thumbnail Size", description: "Fetch Vimeo thumbnail URLs at every available size.",
- category: "social", icon: Globe, keywords: ["vimeo", "thumbnail", "video", "size"],
- commands: ["vimeo thumbnail"], status: "ready", component: tf(() => import("@/components/tools/social/social-tools")),
+ slug: "vimeo-thumbnail",
+ name: "Vimeo Thumbnail Size",
+ description: "Fetch Vimeo thumbnail URLs at every available size.",
+ category: "social",
+ icon: Globe,
+ keywords: ["vimeo", "thumbnail", "video", "size"],
+ commands: ["vimeo thumbnail"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/social/social-tools")),
},
{
- slug: "custom-twitter-card", name: "Twitter Card Generator", description: "Generate summary and summary_large_image card tags.",
- category: "social", icon: MessageCircle, keywords: ["twitter", "card", "meta", "x", "summary", "snippet"],
- commands: ["twitter card"], status: "ready", component: tf(() => import("@/components/tools/social/social-tools")),
+ slug: "custom-twitter-card",
+ name: "Twitter Card Generator",
+ description: "Generate summary and summary_large_image card tags.",
+ category: "social",
+ icon: MessageCircle,
+ keywords: ["twitter", "card", "meta", "x", "summary", "snippet"],
+ commands: ["twitter card"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/social/social-tools")),
},
{
- slug: "og-image-generator", name: "OG Image Generator", description: "Design a shareable 1200×630 social image in-browser.",
- category: "image", icon: Image, keywords: ["og", "image", "social", "1200", "630", "share"],
- commands: ["og image"], status: "ready", component: tf(() => import("@/components/tools/social/social-tools")),
+ slug: "og-image-generator",
+ name: "OG Image Generator",
+ description: "Design a shareable 1200×630 social image in-browser.",
+ category: "image",
+ icon: Image,
+ keywords: ["og", "image", "social", "1200", "630", "share"],
+ commands: ["og image"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/social/social-tools")),
},
{
- slug: "quote-generator", name: "Quote Card Generator", description: "Beautiful quote cards for Instagram and X.",
- category: "social", icon: Quote, keywords: ["quote", "card", "instagram", "motivational", "poster"],
- commands: ["quote"], status: "ready", component: tf(() => import("@/components/tools/social/social-tools")),
+ slug: "quote-generator",
+ name: "Quote Card Generator",
+ description: "Beautiful quote cards for Instagram and X.",
+ category: "social",
+ icon: Quote,
+ keywords: ["quote", "card", "instagram", "motivational", "poster"],
+ commands: ["quote"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/social/social-tools")),
},
{
- slug: "caption-generator", name: "Instagram Caption Generator", description: "Generate catchy captions and hashtags from your topic.",
- category: "social", icon: Star, keywords: ["caption", "instagram", "hashtag", "copy", "post"],
- commands: ["caption"], status: "ready", component: tf(() => import("@/components/tools/social/social-tools")),
+ slug: "caption-generator",
+ name: "Instagram Caption Generator",
+ description: "Generate catchy captions and hashtags from your topic.",
+ category: "social",
+ icon: Star,
+ keywords: ["caption", "instagram", "hashtag", "copy", "post"],
+ commands: ["caption"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/social/social-tools")),
},
{
- slug: "bio-generator", name: "Social Bio Generator", description: "Craft short, punchy bios for any platform.",
- category: "social", icon: Repeat, keywords: ["bio", "profile", "instagram", "twitter", "about"],
- commands: ["bio"], status: "ready", component: tf(() => import("@/components/tools/social/social-tools")),
+ slug: "bio-generator",
+ name: "Social Bio Generator",
+ description: "Craft short, punchy bios for any platform.",
+ category: "social",
+ icon: Repeat,
+ keywords: ["bio", "profile", "instagram", "twitter", "about"],
+ commands: ["bio"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/social/social-tools")),
},
{
- slug: "linkedin-header-generator", name: "LinkedIn Banner Generator", description: "Create a 1128×191 LinkedIn banner with a headline.",
- category: "social", icon: WandSparkles, keywords: ["linkedin", "banner", "header", "1128", "cover"],
- commands: ["linkedin"], status: "ready", component: tf(() => import("@/components/tools/social/social-tools")),
+ slug: "linkedin-header-generator",
+ name: "LinkedIn Banner Generator",
+ description: "Create a 1128×191 LinkedIn banner with a headline.",
+ category: "social",
+ icon: WandSparkles,
+ keywords: ["linkedin", "banner", "header", "1128", "cover"],
+ commands: ["linkedin"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/social/social-tools")),
},
{
- slug: "link-preview-generator", name: "Link Preview Mockup", description: "Preview how your link will look shared on social apps.",
- category: "social", icon: Share2, keywords: ["preview", "link", "mockup", "og", "share"],
- commands: ["link preview"], status: "ready", component: tf(() => import("@/components/tools/social/social-tools")),
+ slug: "link-preview-generator",
+ name: "Link Preview Mockup",
+ description: "Preview how your link will look shared on social apps.",
+ category: "social",
+ icon: Share2,
+ keywords: ["preview", "link", "mockup", "og", "share"],
+ commands: ["link preview"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/social/social-tools")),
},
{
- slug: "social-mockup", name: "Social Post Mockup", description: "Frame text into a realistic social media post.",
- category: "social", icon: MessageCircle, keywords: ["mockup", "post", "frame", "social", "screenshot"],
- commands: ["mockup"], status: "ready", component: tf(() => import("@/components/tools/social/social-tools")),
+ slug: "social-mockup",
+ name: "Social Post Mockup",
+ description: "Frame text into a realistic social media post.",
+ category: "social",
+ icon: MessageCircle,
+ keywords: ["mockup", "post", "frame", "social", "screenshot"],
+ commands: ["mockup"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/social/social-tools")),
},
{
- slug: "yt-title-generator", name: "YouTube Title Generator", description: "Generate clickable titles from keyword combos.",
- category: "social", icon: Volume2, keywords: ["youtube", "title", "clickbait", "keyword", "video"],
- commands: ["yt title"], status: "ready", component: tf(() => import("@/components/tools/social/social-tools")),
+ slug: "yt-title-generator",
+ name: "YouTube Title Generator",
+ description: "Generate clickable titles from keyword combos.",
+ category: "social",
+ icon: Volume2,
+ keywords: ["youtube", "title", "clickbait", "keyword", "video"],
+ commands: ["yt title"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/social/social-tools")),
},
// --- Misc ---
{
- slug: "list-randomizer", name: "List Randomizer", description: "Shuffle or randomize any newline-separated list.",
- category: "misc", tier: "standard", icon: Shuffle, keywords: ["randomizer", "shuffle", "list", "raffle", "random order"],
- commands: ["shuffle"], status: "ready", component: tf(() => import("@/components/tools/misc/misc-tools")),
+ slug: "list-randomizer",
+ name: "List Randomizer",
+ description: "Shuffle or randomize any newline-separated list.",
+ category: "misc",
+ tier: "standard",
+ icon: Shuffle,
+ keywords: ["randomizer", "shuffle", "list", "raffle", "random order"],
+ commands: ["shuffle"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/misc/misc-tools")),
},
{
- slug: "fake-iban-generator", name: "Fake IBAN Generator", description: "Generate structural, checksum-valid IBANs for 13 countries.",
- category: "misc", icon: CreditCard, keywords: ["iban", "bank", "account", "fake", "valid", "european"],
- commands: ["iban"], status: "ready", component: tf(() => import("@/components/tools/misc/misc-tools")),
+ slug: "fake-iban-generator",
+ name: "Fake IBAN Generator",
+ description: "Generate structural, checksum-valid IBANs for 13 countries.",
+ category: "misc",
+ icon: CreditCard,
+ keywords: ["iban", "bank", "account", "fake", "valid", "european"],
+ commands: ["iban"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/misc/misc-tools")),
},
{
- slug: "barcode-generator", name: "Barcode Generator", description: "Generate Code 39 and EAN-13 barcodes as SVG.",
- category: "misc", icon: ScanBarcode, keywords: ["barcode", "code39", "ean13", "ean", "svg", "scan"],
- commands: ["barcode"], status: "ready", component: tf(() => import("@/components/tools/misc/misc-tools")),
+ slug: "barcode-generator",
+ name: "Barcode Generator",
+ description: "Generate Code 39 and EAN-13 barcodes as SVG.",
+ category: "misc",
+ icon: ScanBarcode,
+ keywords: ["barcode", "code39", "ean13", "ean", "svg", "scan"],
+ commands: ["barcode"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/misc/misc-tools")),
},
{
- slug: "unicode-rain", name: "Unicode Rain", description: "A Matrix-style animated stream of any character set.",
- category: "misc", icon: Dices, keywords: ["rain", "matrix", "unicode", "animation", "symbols"],
- commands: ["rain"], status: "ready", component: tf(() => import("@/components/tools/misc/misc-tools")),
+ slug: "unicode-rain",
+ name: "Unicode Rain",
+ description: "A Matrix-style animated stream of any character set.",
+ category: "misc",
+ icon: Dices,
+ keywords: ["rain", "matrix", "unicode", "animation", "symbols"],
+ commands: ["rain"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/misc/misc-tools")),
},
{
- slug: "fake-user-generator", name: "Fake User Generator", description: "Bulk-generate fake names, emails, countries and phones.",
- category: "misc", icon: User, keywords: ["fake", "user", "names", "email", "dummy", "test data"],
- commands: ["fake user"], status: "ready", component: tf(() => import("@/components/tools/misc/misc-tools")),
+ slug: "fake-user-generator",
+ name: "Fake User Generator",
+ description: "Bulk-generate fake names, emails, countries and phones.",
+ category: "misc",
+ icon: User,
+ keywords: ["fake", "user", "names", "email", "dummy", "test data"],
+ commands: ["fake user"],
+ status: "ready",
+ component: tf(() => import("@/components/tools/misc/misc-tools")),
},
-];
\ No newline at end of file
+];
diff --git a/artifacts/personal-tool-console/src/lib/registry/text.ts b/artifacts/personal-tool-console/src/lib/registry/text.ts
index 64c7374..2d6a377 100644
--- a/artifacts/personal-tool-console/src/lib/registry/text.ts
+++ b/artifacts/personal-tool-console/src/lib/registry/text.ts
@@ -2,19 +2,37 @@ import type { ToolDefinition } from "@/lib/tool-registry";
import { TEXT_GROUPS } from "@/lib/tools/text";
import { tf } from "@/lib/registry/lazy";
import {
- Scissors, AlignStartVertical, MoveHorizontal, WandSparkles, Quote, ListFilter,
- ArrowUpDown, Dices, ChartColumn, CaseSensitive, TextWrap, TextQuote, Braces,
- Hash, Highlighter, Type, Table, Binary, TextSelect, Pencil, TypeOutline,
+ Scissors,
+ AlignStartVertical,
+ MoveHorizontal,
+ WandSparkles,
+ Quote,
+ ListFilter,
+ ArrowUpDown,
+ Dices,
+ ChartColumn,
+ CaseSensitive,
+ TextWrap,
+ TextQuote,
+ Braces,
+ Hash,
+ Highlighter,
+ Type,
+ Table,
+ Binary,
+ TextSelect,
+ Pencil,
+ TypeOutline,
} from "lucide-react";
const GROUP_ICONS: Record = {
"split-combine": Scissors,
"alignment-padding": AlignStartVertical,
- "reordering": MoveHorizontal,
+ reordering: MoveHorizontal,
"randomize-fake": WandSparkles,
"prefix-suffix-symbols": Quote,
- "filtering": ListFilter,
- "sorting": ArrowUpDown,
+ filtering: ListFilter,
+ sorting: ArrowUpDown,
"randomize-order": Dices,
"stats-structure": ChartColumn,
"case-visual": CaseSensitive,
@@ -26,17 +44,26 @@ const GROUP_ICONS: Record = {
"vowels-consonants": Type,
"columns-ngrams": Table,
"misc-encoding": Binary,
- "existing": TextSelect,
+ existing: TextSelect,
};
function keywordsFor(name: string, description: string): string[] {
const parts = new Set();
- for (const w of name.toLowerCase().split(/[^a-z0-9]+/)) if (w.length > 1) parts.add(w);
- description.toLowerCase().split(/[^a-z0-9]+/).slice(0, 6).forEach((w) => { if (w.length > 2) parts.add(w); });
+ for (const w of name.toLowerCase().split(/[^a-z0-9]+/))
+ if (w.length > 1) parts.add(w);
+ description
+ .toLowerCase()
+ .split(/[^a-z0-9]+/)
+ .slice(0, 6)
+ .forEach((w) => {
+ if (w.length > 2) parts.add(w);
+ });
return [...parts];
}
-export const TEXT_SHARED_PAGE = tf(() => import("@/components/tools/text/text-tool-page"));
+export const TEXT_SHARED_PAGE = tf(
+ () => import("@/components/tools/text/text-tool-page"),
+);
export const TEXT_TOOL_DEFS: ToolDefinition[] = TEXT_GROUPS.flatMap((g) =>
g.tools.map((t) => ({
@@ -57,7 +84,8 @@ export const TEXT_COMING_SOON: ToolDefinition[] = [
{
slug: "text-handwriting",
name: "Handwriting Generator",
- description: "Convert text into realistic handwriting-style fonts and PDF worksheets.",
+ description:
+ "Convert text into realistic handwriting-style fonts and PDF worksheets.",
category: "text",
icon: Pencil,
keywords: ["handwriting", "font", "cursive", "script", "worksheet"],
@@ -67,11 +95,12 @@ export const TEXT_COMING_SOON: ToolDefinition[] = [
{
slug: "font-pair-finder",
name: "Font Pair Finder",
- description: "Discover harmonious Google Font pairings for your next design.",
+ description:
+ "Discover harmonious Google Font pairings for your next design.",
category: "text",
icon: TypeOutline,
keywords: ["font", "pairing", "typography", "google fonts", "match"],
commands: ["case-visual"],
status: "coming-soon",
},
-];
\ No newline at end of file
+];
diff --git a/artifacts/personal-tool-console/src/lib/storage.ts b/artifacts/personal-tool-console/src/lib/storage.ts
index cb065ff..089c60f 100644
--- a/artifacts/personal-tool-console/src/lib/storage.ts
+++ b/artifacts/personal-tool-console/src/lib/storage.ts
@@ -1,19 +1,19 @@
export const STORAGE_KEYS = {
- history: 'ptc_history_v1',
- pins: 'ptc_pins_v1',
- prefs: 'ptc_prefs_v1',
- recentTools: 'recent_tools_v1',
+ history: "ptc_history_v1",
+ pins: "ptc_pins_v1",
+ prefs: "ptc_prefs_v1",
+ recentTools: "recent_tools_v1",
} as const;
export const DEFAULT_PREFS = {
passwordLength: 16,
- passwordMode: 'strong',
- defaultCurrencyFrom: 'usd',
- defaultCurrencyTo: 'myr',
+ passwordMode: "strong",
+ defaultCurrencyFrom: "usd",
+ defaultCurrencyTo: "myr",
};
export function loadJson(key: string, fallback: T): T {
- if (typeof window === 'undefined') return fallback;
+ if (typeof window === "undefined") return fallback;
try {
const raw = window.localStorage.getItem(key);
return raw ? JSON.parse(raw) : fallback;
@@ -23,7 +23,7 @@ export function loadJson(key: string, fallback: T): T {
}
export function saveJson(key: string, value: unknown) {
- if (typeof window === 'undefined') return;
+ if (typeof window === "undefined") return;
try {
window.localStorage.setItem(key, JSON.stringify(value));
} catch {
diff --git a/artifacts/personal-tool-console/src/lib/tool-registry.ts b/artifacts/personal-tool-console/src/lib/tool-registry.ts
index eb2ca3a..c13814c 100644
--- a/artifacts/personal-tool-console/src/lib/tool-registry.ts
+++ b/artifacts/personal-tool-console/src/lib/tool-registry.ts
@@ -1,7 +1,21 @@
import type { ComponentType, LazyExoticComponent } from "react";
import {
- Calculator, ArrowLeftRight, Sparkles, Globe, Lock, Binary, Combine, Terminal,
- FileCode2, TextCursorInput, CodeXml, Type, Image, Palette, Share2, Wrench,
+ Calculator,
+ ArrowLeftRight,
+ Sparkles,
+ Globe,
+ Lock,
+ Binary,
+ Combine,
+ Terminal,
+ FileCode2,
+ TextCursorInput,
+ CodeXml,
+ Type,
+ Image,
+ Palette,
+ Share2,
+ Wrench,
type LucideIcon,
} from "lucide-react";
import { loadJson, STORAGE_KEYS } from "@/lib/storage";
@@ -12,9 +26,23 @@ import { MEDIA_TOOLS } from "@/lib/registry/media-tools";
import { TEXT_TOOL_DEFS, TEXT_COMING_SOON } from "@/lib/registry/text";
export type ToolCategoryId =
- | "math" | "converter" | "generator" | "network"
- | "crypto" | "bitmap" | "set" | "linux" | "xml" | "utf8" | "coding"
- | "text" | "image" | "css" | "color" | "social" | "misc";
+ | "math"
+ | "converter"
+ | "generator"
+ | "network"
+ | "crypto"
+ | "bitmap"
+ | "set"
+ | "linux"
+ | "xml"
+ | "utf8"
+ | "coding"
+ | "text"
+ | "image"
+ | "css"
+ | "color"
+ | "social"
+ | "misc";
export type ToolStatus = "ready" | "coming-soon";
@@ -46,23 +74,108 @@ export interface ToolCategoryMeta {
}
export const TOOL_CATEGORIES: Record = {
- math: { id: "math", label: "Math", icon: Calculator, blurb: "Calculators and expressions" },
- converter: { id: "converter", label: "Converters", icon: ArrowLeftRight, blurb: "Units, currency, time & radix" },
- generator: { id: "generator", label: "Generators", icon: Sparkles, blurb: "Passwords, QR codes, UUIDs & keys" },
- network: { id: "network", label: "Network", icon: Globe, blurb: "DNS, speed and connectivity" },
- crypto: { id: "crypto", label: "Crypto", icon: Lock, blurb: "Encryption, HMAC, JWT (in-browser)" },
- bitmap: { id: "bitmap", label: "Bitmap", icon: Binary, blurb: "Bit-level and radix utilities" },
- set: { id: "set", label: "Sets", icon: Combine, blurb: "Union, intersection & dedupe" },
- linux: { id: "linux", label: "Linux", icon: Terminal, blurb: "chmod, cron, paths & permissions" },
- xml: { id: "xml", label: "XML", icon: FileCode2, blurb: "Format, validate & convert XML" },
- utf8: { id: "utf8", label: "UTF-8", icon: TextCursorInput, blurb: "Bytes, codepoints, BOM & escapes" },
- coding: { id: "coding", label: "Coding", icon: CodeXml, blurb: "Formatters, encoders & dev aids" },
- text: { id: "text", label: "Text", icon: Type, blurb: "200+ text manipulation utilities" },
- image: { id: "image", label: "Image", icon: Image, blurb: "Canvas-based image editing" },
- css: { id: "css", label: "CSS", icon: Palette, blurb: "CSS generators with live preview" },
- color: { id: "color", label: "Color", icon: Palette, blurb: "Palettes, shades and converters" },
- social: { id: "social", label: "Social", icon: Share2, blurb: "Mockups, thumbnails & OG meta" },
- misc: { id: "misc", label: "Misc", icon: Wrench, blurb: "PDF, barcodes, IBAN, randomizers" },
+ math: {
+ id: "math",
+ label: "Math",
+ icon: Calculator,
+ blurb: "Calculators and expressions",
+ },
+ converter: {
+ id: "converter",
+ label: "Converters",
+ icon: ArrowLeftRight,
+ blurb: "Units, currency, time & radix",
+ },
+ generator: {
+ id: "generator",
+ label: "Generators",
+ icon: Sparkles,
+ blurb: "Passwords, QR codes, UUIDs & keys",
+ },
+ network: {
+ id: "network",
+ label: "Network",
+ icon: Globe,
+ blurb: "DNS, speed and connectivity",
+ },
+ crypto: {
+ id: "crypto",
+ label: "Crypto",
+ icon: Lock,
+ blurb: "Encryption, HMAC, JWT (in-browser)",
+ },
+ bitmap: {
+ id: "bitmap",
+ label: "Bitmap",
+ icon: Binary,
+ blurb: "Bit-level and radix utilities",
+ },
+ set: {
+ id: "set",
+ label: "Sets",
+ icon: Combine,
+ blurb: "Union, intersection & dedupe",
+ },
+ linux: {
+ id: "linux",
+ label: "Linux",
+ icon: Terminal,
+ blurb: "chmod, cron, paths & permissions",
+ },
+ xml: {
+ id: "xml",
+ label: "XML",
+ icon: FileCode2,
+ blurb: "Format, validate & convert XML",
+ },
+ utf8: {
+ id: "utf8",
+ label: "UTF-8",
+ icon: TextCursorInput,
+ blurb: "Bytes, codepoints, BOM & escapes",
+ },
+ coding: {
+ id: "coding",
+ label: "Coding",
+ icon: CodeXml,
+ blurb: "Formatters, encoders & dev aids",
+ },
+ text: {
+ id: "text",
+ label: "Text",
+ icon: Type,
+ blurb: "200+ text manipulation utilities",
+ },
+ image: {
+ id: "image",
+ label: "Image",
+ icon: Image,
+ blurb: "Canvas-based image editing",
+ },
+ css: {
+ id: "css",
+ label: "CSS",
+ icon: Palette,
+ blurb: "CSS generators with live preview",
+ },
+ color: {
+ id: "color",
+ label: "Color",
+ icon: Palette,
+ blurb: "Palettes, shades and converters",
+ },
+ social: {
+ id: "social",
+ label: "Social",
+ icon: Share2,
+ blurb: "Mockups, thumbnails & OG meta",
+ },
+ misc: {
+ id: "misc",
+ label: "Misc",
+ icon: Wrench,
+ blurb: "PDF, barcodes, IBAN, randomizers",
+ },
};
export const CATEGORY_ORDER = Object.keys(TOOL_CATEGORIES) as ToolCategoryId[];
@@ -76,7 +189,9 @@ export const ALL_TOOLS: ToolDefinition[] = [
...TEXT_COMING_SOON,
];
-export const TOOLS_BY_SLUG = new Map(ALL_TOOLS.map((t) => [t.slug, t]));
+export const TOOLS_BY_SLUG = new Map(
+ ALL_TOOLS.map((t) => [t.slug, t]),
+);
export function getToolDefinition(slug: string): ToolDefinition | undefined {
return TOOLS_BY_SLUG.get(slug);
@@ -86,7 +201,9 @@ export function getToolsByCategory(category: ToolCategoryId): ToolDefinition[] {
return ALL_TOOLS.filter((t) => t.category === category);
}
-export function getCategoriesWithTools(): Array {
+export function getCategoriesWithTools(): Array<
+ ToolCategoryMeta & { tools: ToolDefinition[]; ready: number }
+> {
return CATEGORY_ORDER.map((id) => {
const tools = getToolsByCategory(id);
return {
@@ -100,19 +217,18 @@ export function getCategoriesWithTools(): Array {
- let score = 0;
- if (t.slug.includes(q)) score += 10;
- if (t.name.toLowerCase().includes(q)) score += 8;
- if (t.description.toLowerCase().includes(q)) score += 3;
- for (const k of t.keywords) {
- if (k.includes(q)) score += 6;
- else if (k.startsWith(q)) score += 5;
- }
- if (t.commands?.some((c) => c.toLowerCase().includes(q))) score += 4;
- return { t, score };
- })
+ const scored = ALL_TOOLS.map((t) => {
+ let score = 0;
+ if (t.slug.includes(q)) score += 10;
+ if (t.name.toLowerCase().includes(q)) score += 8;
+ if (t.description.toLowerCase().includes(q)) score += 3;
+ for (const k of t.keywords) {
+ if (k.includes(q)) score += 6;
+ else if (k.startsWith(q)) score += 5;
+ }
+ if (t.commands?.some((c) => c.toLowerCase().includes(q))) score += 4;
+ return { t, score };
+ })
.filter((x) => x.score > 0)
.sort((a, b) => b.score - a.score || a.t.name.localeCompare(b.t.name));
return scored.slice(0, limit).map((x) => x.t);
@@ -120,11 +236,18 @@ export function searchTools(query: string, limit = 20): ToolDefinition[] {
export function getRecentTools(): ToolDefinition[] {
const slugs = loadJson(STORAGE_KEYS.recentTools, []);
- const found = slugs.map((s) => TOOLS_BY_SLUG.get(s)).filter((t): t is ToolDefinition => Boolean(t));
+ const found = slugs
+ .map((s) => TOOLS_BY_SLUG.get(s))
+ .filter((t): t is ToolDefinition => Boolean(t));
return found.slice(0, 12);
}
export function toOption(value: unknown): string {
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
+ if (
+ typeof value === "string" ||
+ typeof value === "number" ||
+ typeof value === "boolean"
+ )
+ return String(value);
return "";
-}
\ No newline at end of file
+}
diff --git a/artifacts/personal-tool-console/src/lib/tools/base64.ts b/artifacts/personal-tool-console/src/lib/tools/base64.ts
index ab0f7e7..cc55d73 100644
--- a/artifacts/personal-tool-console/src/lib/tools/base64.ts
+++ b/artifacts/personal-tool-console/src/lib/tools/base64.ts
@@ -6,6 +6,6 @@ export function base64Decode(text: string) {
try {
return decodeURIComponent(escape(atob(text)));
} catch {
- throw new Error('Invalid base64 string');
+ throw new Error("Invalid base64 string");
}
}
diff --git a/artifacts/personal-tool-console/src/lib/tools/bitmap/index.ts b/artifacts/personal-tool-console/src/lib/tools/bitmap/index.ts
index a0b8975..ad36076 100644
--- a/artifacts/personal-tool-console/src/lib/tools/bitmap/index.ts
+++ b/artifacts/personal-tool-console/src/lib/tools/bitmap/index.ts
@@ -1,19 +1,27 @@
export function convertRadix(value: string, fromBase: number, toBase: number) {
const n = parseInt(value, fromBase);
- if (isNaN(n)) throw new Error('Invalid input for source radix');
+ if (isNaN(n)) throw new Error("Invalid input for source radix");
return n.toString(toBase).toUpperCase();
}
export function bitwiseOp(a: number, b: number, op: string) {
switch (op) {
- case 'AND': return a & b;
- case 'OR': return a | b;
- case 'XOR': return a ^ b;
- case 'NOT': return ~a;
- case '<<': return a << b;
- case '>>': return a >> b;
- case '>>>': return a >>> b;
- default: throw new Error('Unknown operation');
+ case "AND":
+ return a & b;
+ case "OR":
+ return a | b;
+ case "XOR":
+ return a ^ b;
+ case "NOT":
+ return ~a;
+ case "<<":
+ return a << b;
+ case ">>":
+ return a >> b;
+ case ">>>":
+ return a >>> b;
+ default:
+ throw new Error("Unknown operation");
}
}
@@ -26,8 +34,8 @@ export function inspectBits(n: number, bits = 32) {
export function binaryDump(text: string) {
const bytes = new TextEncoder().encode(text);
return Array.from(bytes).map((b) => ({
- hex: b.toString(16).padStart(2, '0'),
- binary: b.toString(2).padStart(8, '0'),
+ hex: b.toString(16).padStart(2, "0"),
+ binary: b.toString(2).padStart(8, "0"),
dec: b,
}));
}
diff --git a/artifacts/personal-tool-console/src/lib/tools/calculator.ts b/artifacts/personal-tool-console/src/lib/tools/calculator.ts
index fd55630..bf8a704 100644
--- a/artifacts/personal-tool-console/src/lib/tools/calculator.ts
+++ b/artifacts/personal-tool-console/src/lib/tools/calculator.ts
@@ -3,9 +3,11 @@ export function isMathExpression(input: string) {
}
export function safeMathEval(expression: string) {
- const cleaned = expression.replace(/,/g, '.').replace(/\^/g, '**');
- if (!/^[0-9+\-*/%().*\s**]+$/.test(cleaned.replace(/\*\*/g, ''))) throw new Error('Invalid expression');
+ const cleaned = expression.replace(/,/g, ".").replace(/\^/g, "**");
+ if (!/^[0-9+\-*/%().*\s**]+$/.test(cleaned.replace(/\*\*/g, "")))
+ throw new Error("Invalid expression");
const result = Function(`"use strict"; return (${cleaned});`)();
- if (typeof result !== 'number' || !Number.isFinite(result)) throw new Error('Invalid result');
+ if (typeof result !== "number" || !Number.isFinite(result))
+ throw new Error("Invalid result");
return result as number;
}
diff --git a/artifacts/personal-tool-console/src/lib/tools/coding.ts b/artifacts/personal-tool-console/src/lib/tools/coding.ts
index 756d3ba..c70d355 100644
--- a/artifacts/personal-tool-console/src/lib/tools/coding.ts
+++ b/artifacts/personal-tool-console/src/lib/tools/coding.ts
@@ -11,14 +11,22 @@ export function minifyJson(text: string): string {
return JSON.stringify(JSON.parse(text));
}
-export function validateJson(text: string): { valid: boolean; error?: string; line?: number } {
+export function validateJson(text: string): {
+ valid: boolean;
+ error?: string;
+ line?: number;
+} {
try {
JSON.parse(text);
return { valid: true };
} catch (err) {
const message = (err as Error).message;
const match = message.match(/line (\d+)/);
- return { valid: false, error: message, line: match ? Number(match[1]) : undefined };
+ return {
+ valid: false,
+ error: message,
+ line: match ? Number(match[1]) : undefined,
+ };
}
}
@@ -29,12 +37,16 @@ export function uuidv4(): string {
crypto.getRandomValues(bytes);
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
- const h = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
+ const h = Array.from(bytes)
+ .map((b) => b.toString(16).padStart(2, "0"))
+ .join("");
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
}
export function uuidBulk(count: number, uppercase: boolean): string[] {
- return Array.from({ length: count }, () => (uppercase ? uuidv4().toUpperCase() : uuidv4()));
+ return Array.from({ length: count }, () =>
+ uppercase ? uuidv4().toUpperCase() : uuidv4(),
+ );
}
// --- URL / HTML encoders ---
@@ -85,32 +97,71 @@ function md5RotateLeft(x: number, n: number) {
}
export function md5(input: string): string {
- const M = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476];
- const K = Array.from({ length: 64 }, (_, i) => Math.abs(Math.sin(i + 1)) * 0x100000000 >>> 0);
- const s = [7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21];
+ const M = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476];
+ const K = Array.from(
+ { length: 64 },
+ (_, i) => (Math.abs(Math.sin(i + 1)) * 0x100000000) >>> 0,
+ );
+ const s = [
+ 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5,
+ 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11,
+ 16, 23, 4, 11, 16, 23, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10,
+ 15, 21,
+ ];
const bytes: number[] = [];
for (let i = 0; i < input.length; i++) {
const c = input.charCodeAt(i);
- bytes.push(c & 0xff, (c >>> 8) & 0xff, (c >>> 16) & 0xff, (c >>> 24) & 0xff);
+ bytes.push(
+ c & 0xff,
+ (c >>> 8) & 0xff,
+ (c >>> 16) & 0xff,
+ (c >>> 24) & 0xff,
+ );
}
const bitLen = bytes.length * 8;
bytes.push(0x80);
while (bytes.length % 64 !== 56) bytes.push(0);
- bytes.push(bitLen & 0xff, (bitLen >>> 8) & 0xff, (bitLen >>> 16) & 0xff, (bitLen >>> 24) & 0xff, 0, 0, 0, 0);
+ bytes.push(
+ bitLen & 0xff,
+ (bitLen >>> 8) & 0xff,
+ (bitLen >>> 16) & 0xff,
+ (bitLen >>> 24) & 0xff,
+ 0,
+ 0,
+ 0,
+ 0,
+ );
for (let off = 0; off < bytes.length; off += 64) {
const X: number[] = [];
for (let i = 0; i < 16; i++) {
- X[i] = (bytes[off + i * 4] | (bytes[off + i * 4 + 1] << 8) | (bytes[off + i * 4 + 2] << 16) | (bytes[off + i * 4 + 3] << 24)) >>> 0;
+ X[i] =
+ (bytes[off + i * 4] |
+ (bytes[off + i * 4 + 1] << 8) |
+ (bytes[off + i * 4 + 2] << 16) |
+ (bytes[off + i * 4 + 3] << 24)) >>>
+ 0;
}
let [a, b, c, d] = M;
for (let i = 0; i < 64; i++) {
let f: number, g: number;
- if (i < 16) { f = (b & c) | (~b & d); g = i; }
- else if (i < 32) { f = (d & b) | (~d & c); g = (5 * i + 1) % 16; }
- else if (i < 48) { f = b ^ c ^ d; g = (3 * i + 5) % 16; }
- else { f = c ^ (b | ~d); g = (7 * i) % 16; }
+ if (i < 16) {
+ f = (b & c) | (~b & d);
+ g = i;
+ } else if (i < 32) {
+ f = (d & b) | (~d & c);
+ g = (5 * i + 1) % 16;
+ } else if (i < 48) {
+ f = b ^ c ^ d;
+ g = (3 * i + 5) % 16;
+ } else {
+ f = c ^ (b | ~d);
+ g = (7 * i) % 16;
+ }
const tmp = (d + md5RotateLeft((a + f + K[i] + X[g]) >>> 0, s[i])) >>> 0;
- d = c; c = b; b = (b + tmp) >>> 0; a = tmp;
+ d = c;
+ c = b;
+ b = (b + tmp) >>> 0;
+ a = tmp;
}
M[0] = (M[0] + a) >>> 0;
M[1] = (M[1] + b) >>> 0;
@@ -118,7 +169,11 @@ export function md5(input: string): string {
M[3] = (M[3] + d) >>> 0;
}
const wordToHex = (word: number) => {
- const little = ((word & 0xff) << 24) | ((word >>> 8) & 0xff) << 16 | ((word >>> 16) & 0xff) << 8 | ((word >>> 24) & 0xff);
+ const little =
+ ((word & 0xff) << 24) |
+ (((word >>> 8) & 0xff) << 16) |
+ (((word >>> 16) & 0xff) << 8) |
+ ((word >>> 24) & 0xff);
return (little >>> 0).toString(16).padStart(8, "0");
};
return M.map(wordToHex).join("");
@@ -128,12 +183,18 @@ export function md5(input: string): string {
export function minifyHtml(html: string): string {
const noComments = html.replace(//g, "");
- return noComments.replace(/>\s+<").replace(/^\s+|\s+$/g, "").trim();
+ return noComments
+ .replace(/>\s+<")
+ .replace(/^\s+|\s+$/g, "")
+ .trim();
}
export function minifyCss(css: string): string {
const noComments = css.replace(/\/\*[\s\S]*?\*\//g, "");
- return noComments.replace(/\s+/g, " ").replace(/\s*([{}:;,])\s*/g, "$1").trim();
+ return noComments
+ .replace(/\s+/g, " ")
+ .replace(/\s*([{}:;,])\s*/g, "$1")
+ .trim();
}
export function minifyJs(code: string): string {
@@ -147,29 +208,55 @@ export function minifyJs(code: string): string {
const ch = code[i];
const next = code[i + 1];
if (inLineComment) {
- if (ch === "\n") { inLineComment = false; result.push("\n"); }
+ if (ch === "\n") {
+ inLineComment = false;
+ result.push("\n");
+ }
i++;
continue;
}
if (inBlockComment) {
- if (ch === "*" && next === "/") { inBlockComment = false; i += 2; }
- else i++;
+ if (ch === "*" && next === "/") {
+ inBlockComment = false;
+ i += 2;
+ } else i++;
continue;
}
if (inString) {
result.push(ch);
- if (ch === "\\") { result.push(next ?? ""); i += 2; continue; }
+ if (ch === "\\") {
+ result.push(next ?? "");
+ i += 2;
+ continue;
+ }
if (ch === inString) inString = null;
i++;
continue;
}
- if (ch === "/" && next === "/") { inLineComment = true; i += 2; continue; }
- if (ch === "/" && next === "*") { inBlockComment = true; i += 2; continue; }
- if (ch === '"' || ch === "'" || ch === "`") { inString = ch; result.push(ch); i++; continue; }
+ if (ch === "/" && next === "/") {
+ inLineComment = true;
+ i += 2;
+ continue;
+ }
+ if (ch === "/" && next === "*") {
+ inBlockComment = true;
+ i += 2;
+ continue;
+ }
+ if (ch === '"' || ch === "'" || ch === "`") {
+ inString = ch;
+ result.push(ch);
+ i++;
+ continue;
+ }
result.push(ch);
i++;
}
- out = result.join("").replace(/[ \t]+/g, " ").replace(/\n\s*\n+/g, "\n").trim();
+ out = result
+ .join("")
+ .replace(/[ \t]+/g, " ")
+ .replace(/\n\s*\n+/g, "\n")
+ .trim();
return out;
}
@@ -182,14 +269,31 @@ export function formatCss(css: string): string {
const line = raw.trim();
if (!line) continue;
if (line.startsWith("}")) indent = Math.max(0, indent - 1);
- lines.push(" ".repeat(indent) + line.replace(/\s*[{;]\s*$/, (m) => m).trim());
+ lines.push(
+ " ".repeat(indent) + line.replace(/\s*[{;]\s*$/, (m) => m).trim(),
+ );
if (line.endsWith("{")) indent++;
}
return lines.join("\n");
}
export function formatHtml(html: string): string {
- const voidTags = new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]);
+ const voidTags = new Set([
+ "area",
+ "base",
+ "br",
+ "col",
+ "embed",
+ "hr",
+ "img",
+ "input",
+ "link",
+ "meta",
+ "param",
+ "source",
+ "track",
+ "wbr",
+ ]);
const tokens = html.replace(/>\n<").split("\n");
let indent = 0;
const lines: string[] = [];
@@ -202,7 +306,8 @@ export function formatHtml(html: string): string {
const isVoid = tagName ? voidTags.has(tagName[1].toLowerCase()) : false;
const selfClosing = /\/>$/.test(trimmed);
lines.push(" ".repeat(indent) + trimmed);
- if (!closeOnly && !isVoid && !selfClosing && trimmed.startsWith("<")) indent++;
+ if (!closeOnly && !isVoid && !selfClosing && trimmed.startsWith("<"))
+ indent++;
}
return lines.join("\n");
}
@@ -220,7 +325,9 @@ export interface RnShadowOptions {
export function rnShadowStyle(o: RnShadowOptions): string {
return [
"shadowColor: '" + o.color + "',",
- "shadowOffset: { width: 0, height: " + Math.round(Math.max(1, o.depth / 2)) + " },",
+ "shadowOffset: { width: 0, height: " +
+ Math.round(Math.max(1, o.depth / 2)) +
+ " },",
"shadowOpacity: " + o.iosOpacity + ",",
"shadowRadius: " + Math.max(1, o.depth) + ",",
"elevation: " + Math.max(1, o.androidElevation) + ",",
@@ -230,4 +337,4 @@ export function rnShadowStyle(o: RnShadowOptions): string {
export function jsObjectToInline(source: string): string {
// minimal "style string" renderer used by RN shadow + CSS-like outputs
return source;
-}
\ No newline at end of file
+}
diff --git a/artifacts/personal-tool-console/src/lib/tools/color.ts b/artifacts/personal-tool-console/src/lib/tools/color.ts
index f8622db..1815fb5 100644
--- a/artifacts/personal-tool-console/src/lib/tools/color.ts
+++ b/artifacts/personal-tool-console/src/lib/tools/color.ts
@@ -14,8 +14,13 @@ export interface Hsl {
export function normalizeHex(input: string): string {
let hex = input.trim().replace(/^#/, "");
- if (/^[0-9a-fA-F]{3}$/.test(hex)) hex = hex.split("").map((c) => c + c).join("");
- if (!/^[0-9a-fA-F]{6}$/.test(hex)) throw new Error("Expected a hex color like #RRGGBB");
+ if (/^[0-9a-fA-F]{3}$/.test(hex))
+ hex = hex
+ .split("")
+ .map((c) => c + c)
+ .join("");
+ if (!/^[0-9a-fA-F]{6}$/.test(hex))
+ throw new Error("Expected a hex color like #RRGGBB");
return hex.toLowerCase();
}
@@ -34,11 +39,17 @@ export function hexToRgbaString(hex: string, alpha: number): string {
}
export function isRgba(value: string): boolean {
- return /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/i.test(value.trim());
+ return /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/i.test(
+ value.trim(),
+ );
}
export function rgbaToRgb(value: string): { rgb: Rgb; alpha: number } {
- const m = value.trim().match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/i);
+ const m = value
+ .trim()
+ .match(
+ /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/i,
+ );
if (!m) throw new Error("Expected an rgb()/rgba() value");
return {
rgb: { r: Number(m[1]), g: Number(m[2]), b: Number(m[3]) },
@@ -48,22 +59,32 @@ export function rgbaToRgb(value: string): { rgb: Rgb; alpha: number } {
export function rgbaToHex(value: string): string {
const { rgb, alpha } = rgbaToRgb(value);
- const toHex = (n: number) => Math.round(Math.max(0, Math.min(255, n))).toString(16).padStart(2, "0");
+ const toHex = (n: number) =>
+ Math.round(Math.max(0, Math.min(255, n)))
+ .toString(16)
+ .padStart(2, "0");
const base = `#${toHex(rgb.r)}${toHex(rgb.g)}${toHex(rgb.b)}`;
if (alpha < 1) {
- const a = Math.round(alpha * 255).toString(16).padStart(2, "0");
+ const a = Math.round(alpha * 255)
+ .toString(16)
+ .padStart(2, "0");
return `${base}${a}`;
}
return base;
}
export function rgbToHex(rgb: Rgb): string {
- const toHex = (n: number) => Math.round(Math.max(0, Math.min(255, n))).toString(16).padStart(2, "0");
+ const toHex = (n: number) =>
+ Math.round(Math.max(0, Math.min(255, n)))
+ .toString(16)
+ .padStart(2, "0");
return `#${toHex(rgb.r)}${toHex(rgb.g)}${toHex(rgb.b)}`;
}
export function rgbToHsl({ r, g, b }: Rgb): Hsl {
- r /= 255; g /= 255; b /= 255;
+ r /= 255;
+ g /= 255;
+ b /= 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const l = (max + min) / 2;
@@ -79,11 +100,14 @@ export function rgbToHsl({ r, g, b }: Rgb): Hsl {
export function hslToRgb({ h, s, l }: Hsl): Rgb {
h = ((h % 360) + 360) % 360;
- s /= 100; l /= 100;
+ s /= 100;
+ l /= 100;
const c = (1 - Math.abs(2 * l - 1)) * s;
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
const m = l - c / 2;
- let r = 0, g = 0, b = 0;
+ let r = 0,
+ g = 0,
+ b = 0;
if (h < 60) [r, g, b] = [c, x, 0];
else if (h < 120) [r, g, b] = [x, c, 0];
else if (h < 180) [r, g, b] = [0, c, x];
@@ -121,24 +145,48 @@ export function shadeScale(hex: string, steps = 10): string[] {
return out;
}
-export type PaletteScheme = "monochromatic" | "complementary" | "analogous" | "triadic" | "tetradic";
-
-export function generatePalette(seedHex: string, scheme: PaletteScheme, count = 5): string[] {
+export type PaletteScheme =
+ | "monochromatic"
+ | "complementary"
+ | "analogous"
+ | "triadic"
+ | "tetradic";
+
+export function generatePalette(
+ seedHex: string,
+ scheme: PaletteScheme,
+ count = 5,
+): string[] {
const { h, s, l } = rgbToHsl(hexToRgb(seedHex));
const hues = (): number[] => {
switch (scheme) {
- case "monochromatic": return Array.from({ length: count }, (_, i) => h);
- case "complementary": return Array.from({ length: count }, (_, i) => h + (i % 2 === 0 ? 0 : 180));
- case "analogous": return Array.from({ length: count }, (_, i) => h + (i - Math.floor(count / 2)) * 30);
- case "triadic": return Array.from({ length: count }, (_, i) => h + (i % 3) * 120);
- case "tetradic": return Array.from({ length: count }, (_, i) => h + (i % 4) * 90);
+ case "monochromatic":
+ return Array.from({ length: count }, (_, i) => h);
+ case "complementary":
+ return Array.from(
+ { length: count },
+ (_, i) => h + (i % 2 === 0 ? 0 : 180),
+ );
+ case "analogous":
+ return Array.from(
+ { length: count },
+ (_, i) => h + (i - Math.floor(count / 2)) * 30,
+ );
+ case "triadic":
+ return Array.from({ length: count }, (_, i) => h + (i % 3) * 120);
+ case "tetradic":
+ return Array.from({ length: count }, (_, i) => h + (i % 4) * 90);
}
};
const hslValues = (): Hsl[] => {
const hs = hues();
return hs.map((hh, i) => {
- const lt = scheme === "monochromatic" ? 25 + (i / Math.max(count - 1, 1)) * 50 : Math.max(35, Math.min(70, l + (i % 2 === 0 ? 0 : 12)));
- const st = scheme === "monochromatic" ? Math.max(40, s - 10) : Math.max(45, s);
+ const lt =
+ scheme === "monochromatic"
+ ? 25 + (i / Math.max(count - 1, 1)) * 50
+ : Math.max(35, Math.min(70, l + (i % 2 === 0 ? 0 : 12)));
+ const st =
+ scheme === "monochromatic" ? Math.max(40, s - 10) : Math.max(45, s);
return { h: hh, s: st, l: lt };
});
};
@@ -149,4 +197,4 @@ export function randomHexColor(): string {
const bytes = new Uint8Array(3);
crypto.getRandomValues(bytes);
return rgbToHex({ r: bytes[0], g: bytes[1], b: bytes[2] });
-}
\ No newline at end of file
+}
diff --git a/artifacts/personal-tool-console/src/lib/tools/crypto.ts b/artifacts/personal-tool-console/src/lib/tools/crypto.ts
index 5bdbfe4..3880b40 100644
--- a/artifacts/personal-tool-console/src/lib/tools/crypto.ts
+++ b/artifacts/personal-tool-console/src/lib/tools/crypto.ts
@@ -26,7 +26,12 @@ export function utf8Encode(text: string): Uint8Array {
}
function toBuf(u8: Uint8Array): Uint8Array {
- return new Uint8Array(u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength) as ArrayBuffer);
+ return new Uint8Array(
+ u8.buffer.slice(
+ u8.byteOffset,
+ u8.byteOffset + u8.byteLength,
+ ) as ArrayBuffer,
+ );
}
export function utf8Decode(bytes: Uint8Array): string {
@@ -55,32 +60,58 @@ async function aesImportKey(keyB64: string, mode: AesMode) {
);
}
-export async function aesEncrypt(text: string, keyB64: string, ivInput: string, mode: AesMode) {
+export async function aesEncrypt(
+ text: string,
+ keyB64: string,
+ ivInput: string,
+ mode: AesMode,
+) {
const key = await aesImportKey(keyB64, mode);
const iv = deriveIv(mode, ivInput);
- const buf = await crypto.subtle.encrypt({ name: mode, iv }, key, toBuf(utf8Encode(text)));
+ const buf = await crypto.subtle.encrypt(
+ { name: mode, iv },
+ key,
+ toBuf(utf8Encode(text)),
+ );
return bytesToBase64(new Uint8Array(buf));
}
-export async function aesDecrypt(b64: string, keyB64: string, ivInput: string, mode: AesMode) {
+export async function aesDecrypt(
+ b64: string,
+ keyB64: string,
+ ivInput: string,
+ mode: AesMode,
+) {
try {
const key = await aesImportKey(keyB64, mode);
const iv = deriveIv(mode, ivInput);
- const buf = await crypto.subtle.decrypt({ name: mode, iv }, key, toBuf(base64ToBytes(b64)));
+ const buf = await crypto.subtle.decrypt(
+ { name: mode, iv },
+ key,
+ toBuf(base64ToBytes(b64)),
+ );
return utf8Decode(new Uint8Array(buf));
} catch {
- throw new Error("Decryption failed — wrong key, IV, or corrupted ciphertext");
+ throw new Error(
+ "Decryption failed — wrong key, IV, or corrupted ciphertext",
+ );
}
}
export function generateAesKey(bits: number, format: "base64" | "hex") {
const bytes = randomBytes(bits / 8);
return format === "hex"
- ? Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("")
+ ? Array.from(bytes)
+ .map((b) => b.toString(16).padStart(2, "0"))
+ .join("")
: bytesToBase64(bytes);
}
-export async function hmacHash(message: string, secret: string, algo: "SHA-256" | "SHA-384" | "SHA-512") {
+export async function hmacHash(
+ message: string,
+ secret: string,
+ algo: "SHA-256" | "SHA-384" | "SHA-512",
+) {
const key = await crypto.subtle.importKey(
"raw",
toBuf(utf8Encode(secret)),
@@ -91,7 +122,9 @@ export async function hmacHash(message: string, secret: string, algo: "SHA-256"
const sig = await crypto.subtle.sign("HMAC", key, toBuf(utf8Encode(message)));
const bytes = new Uint8Array(sig);
return {
- hex: Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join(""),
+ hex: Array.from(bytes)
+ .map((b) => b.toString(16).padStart(2, "0"))
+ .join(""),
base64: bytesToBase64(bytes),
};
}
@@ -108,12 +141,18 @@ export interface DecodedJwt {
export function decodeJwt(token: string): DecodedJwt {
const parts = token.trim().split(".");
- if (parts.length !== 3) throw new Error("A JWT has exactly 3 dot-separated segments: header.payload.signature");
+ if (parts.length !== 3)
+ throw new Error(
+ "A JWT has exactly 3 dot-separated segments: header.payload.signature",
+ );
const b64urlToObj = (seg: string) => {
const normalized = seg.replace(/-/g, "+").replace(/_/g, "/");
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
try {
- return JSON.parse(decodeURIComponent(escape(atob(padded)))) as Record;
+ return JSON.parse(decodeURIComponent(escape(atob(padded)))) as Record<
+ string,
+ unknown
+ >;
} catch {
throw new Error("Invalid base64url JSON segment");
}
@@ -122,7 +161,9 @@ export function decodeJwt(token: string): DecodedJwt {
const payload = b64urlToObj(parts[1]);
const exp = typeof payload.exp === "number" ? payload.exp * 1000 : undefined;
const iat = typeof payload.iat === "number" ? payload.iat * 1000 : undefined;
- const secondsToExpiry = exp ? Math.floor((exp - Date.now()) / 1000) : undefined;
+ const secondsToExpiry = exp
+ ? Math.floor((exp - Date.now()) / 1000)
+ : undefined;
return {
header,
payload,
@@ -134,10 +175,16 @@ export function decodeJwt(token: string): DecodedJwt {
};
}
-export function generateRandomKey(bits: 128 | 192 | 256 | 512, format: "hex" | "base64") {
- if (![128, 192, 256, 512].includes(bits)) throw new Error("Supported sizes: 128/192/256/512 bits");
+export function generateRandomKey(
+ bits: 128 | 192 | 256 | 512,
+ format: "hex" | "base64",
+) {
+ if (![128, 192, 256, 512].includes(bits))
+ throw new Error("Supported sizes: 128/192/256/512 bits");
const bytes = randomBytes(bits / 8);
return format === "hex"
- ? Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("")
+ ? Array.from(bytes)
+ .map((b) => b.toString(16).padStart(2, "0"))
+ .join("")
: bytesToBase64(bytes);
-}
\ No newline at end of file
+}
diff --git a/artifacts/personal-tool-console/src/lib/tools/css-gen.ts b/artifacts/personal-tool-console/src/lib/tools/css-gen.ts
index f34781f..832378b 100644
--- a/artifacts/personal-tool-console/src/lib/tools/css-gen.ts
+++ b/artifacts/personal-tool-console/src/lib/tools/css-gen.ts
@@ -6,22 +6,38 @@ export interface CssGenResult {
html?: string;
}
-export function cssGradient(kind: "linear" | "radial" | "conic", angle: number, stops: string[], position: string): CssGenResult {
+export function cssGradient(
+ kind: "linear" | "radial" | "conic",
+ angle: number,
+ stops: string[],
+ position: string,
+): CssGenResult {
const stopStr = stops.join(", ");
let value = "";
if (kind === "linear") value = `${kind}-gradient(${angle}deg, ${stopStr})`;
- else if (kind === "radial") value = `radial-gradient(circle at ${position}, ${stopStr})`;
+ else if (kind === "radial")
+ value = `radial-gradient(circle at ${position}, ${stopStr})`;
else value = `conic-gradient(from ${angle}deg at ${position}, ${stopStr})`;
return { css: `.gradient {\n background: ${value};\n}` };
}
-export function cssGlassmorphism(blur: number, opacity: number, border: string, radius: number, color: string): CssGenResult {
+export function cssGlassmorphism(
+ blur: number,
+ opacity: number,
+ border: string,
+ radius: number,
+ color: string,
+): CssGenResult {
return {
css: `.glass {\n background: ${color};\n backdrop-filter: blur(${blur}px);\n -webkit-backdrop-filter: blur(${blur}px);\n border: ${border} solid rgba(255,255,255,0.6);\n border-radius: ${radius}px;\n opacity: 0.${Math.round(opacity * 10)};\n}`,
};
}
-export function cssTriangle(direction: "up" | "down" | "left" | "right", size: number, color: string): CssGenResult {
+export function cssTriangle(
+ direction: "up" | "down" | "left" | "right",
+ size: number,
+ color: string,
+): CssGenResult {
const borders: Record = {
up: `border-left: ${size}px solid transparent;\n border-right: ${size}px solid transparent;\n border-bottom: ${size}px solid ${color};`,
down: `border-left: ${size}px solid transparent;\n border-right: ${size}px solid transparent;\n border-top: ${size}px solid ${color};`,
@@ -34,21 +50,48 @@ export function cssTriangle(direction: "up" | "down" | "left" | "right", size: n
};
}
-export function cssBoxShadow(spreads: Array<{ x: number; y: number; blur: number; grow: number; color: string; inset: boolean }>): CssGenResult {
- const parts = spreads.map((s) => `${s.inset ? "inset " : ""}${s.x}px ${s.y}px ${s.blur}px ${s.grow}px ${s.color}`).join(", ");
+export function cssBoxShadow(
+ spreads: Array<{
+ x: number;
+ y: number;
+ blur: number;
+ grow: number;
+ color: string;
+ inset: boolean;
+ }>,
+): CssGenResult {
+ const parts = spreads
+ .map(
+ (s) =>
+ `${s.inset ? "inset " : ""}${s.x}px ${s.y}px ${s.blur}px ${s.grow}px ${s.color}`,
+ )
+ .join(", ");
return { css: `.box {\n box-shadow: ${parts};\n}` };
}
-export function cssBorderRadius(topLeft: number, topRight: number, bottomRight: number, bottomLeft: number, squircle: boolean): CssGenResult {
+export function cssBorderRadius(
+ topLeft: number,
+ topRight: number,
+ bottomRight: number,
+ bottomLeft: number,
+ squircle: boolean,
+): CssGenResult {
if (squircle) {
- return { css: `.squircle {\n border-radius: 40% 40% 40% 40% / 30% 30% 30% 30%;\n}` };
+ return {
+ css: `.squircle {\n border-radius: 40% 40% 40% 40% / 30% 30% 30% 30%;\n}`,
+ };
}
return {
css: `.rounded {\n border-radius: ${topLeft}px ${topRight}px ${bottomRight}px ${bottomLeft}px;\n}`,
};
}
-export function cssRgbBackgroundPattern(kind: "dots" | "stripes" | "grid" | "checker" | "crosshatch", size: number, color: string, bg: string): CssGenResult {
+export function cssRgbBackgroundPattern(
+ kind: "dots" | "stripes" | "grid" | "checker" | "crosshatch",
+ size: number,
+ color: string,
+ bg: string,
+): CssGenResult {
const c = color;
const transparent = "rgba(0,0,0,0)";
let gradient = "";
@@ -68,10 +111,15 @@ export function cssRgbBackgroundPattern(kind: "dots" | "stripes" | "grid" | "che
default:
gradient = `repeating-linear-gradient(45deg, ${c} 0 1px, ${transparent} 1px ${size}px), repeating-linear-gradient(-45deg, ${c} 0 1px, ${transparent} 1px ${size}px);`;
}
- return { css: `.pattern {\n background: ${bg};\n background-image: ${gradient}\n}` };
+ return {
+ css: `.pattern {\n background: ${bg};\n background-image: ${gradient}\n}`,
+ };
}
-export function cssClipPath(slug: "circle" | "ellipse" | "triangle" | "hexagon" | "star" | "custom", points: Array<[number, number]>): CssGenResult {
+export function cssClipPath(
+ slug: "circle" | "ellipse" | "triangle" | "hexagon" | "star" | "custom",
+ points: Array<[number, number]>,
+): CssGenResult {
const shapes: Record = {
circle: "circle(50% at 50% 50%)",
ellipse: "ellipse(25% 40% at 50% 50%)",
@@ -79,15 +127,30 @@ export function cssClipPath(slug: "circle" | "ellipse" | "triangle" | "hexagon"
hexagon: "polygon(25% 5%, 75% 5%, 100% 50%, 75% 95%, 25% 95%, 0% 50%)",
star: "polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)",
};
- const value = slug === "custom" ? `polygon(${points.map(([x, y]) => `${x}% ${y}%`).join(", ")})` : shapes[slug];
+ const value =
+ slug === "custom"
+ ? `polygon(${points.map(([x, y]) => `${x}% ${y}%`).join(", ")})`
+ : shapes[slug];
return { css: `.clipped {\n clip-path: ${value};\n}` };
}
-export function cssCubicBezier(p1x: number, p1y: number, p2x: number, p2y: number): CssGenResult {
- return { css: `.ease {\n transition: all 0.4s cubic-bezier(${p1x}, ${p1y}, ${p2x}, ${p2y});\n}` };
+export function cssCubicBezier(
+ p1x: number,
+ p1y: number,
+ p2x: number,
+ p2y: number,
+): CssGenResult {
+ return {
+ css: `.ease {\n transition: all 0.4s cubic-bezier(${p1x}, ${p1y}, ${p2x}, ${p2y});\n}`,
+ };
}
-export function cssLoader(kind: "ring" | "dual" | "bar" | "dots", color: string, size: number, speed: number): CssGenResult {
+export function cssLoader(
+ kind: "ring" | "dual" | "bar" | "dots",
+ color: string,
+ size: number,
+ speed: number,
+): CssGenResult {
const base = `@keyframes spin { to { transform: rotate(360deg); } }\n@keyframes pulse { 0%,100% { opacity: 0.2 } 50% { opacity: 1 } }`;
let css = "";
switch (kind) {
@@ -106,7 +169,11 @@ export function cssLoader(kind: "ring" | "dual" | "bar" | "dots", color: string,
return { css: `${base}\n\n${css}` };
}
-export function cssSwitch(color: string, size: number, checked: boolean): CssGenResult {
+export function cssSwitch(
+ color: string,
+ size: number,
+ checked: boolean,
+): CssGenResult {
return {
css: `.switch {\n position: relative;\n display: inline-block;\n width: ${Math.round(size * 1.7)}px;\n height: ${size}px;\n border-radius: ${size}px;\n background: ${checked ? color : "#ccc"};\n transition: 0.2s;\n}\n.switch::after {\n content: "";\n position: absolute;\n top: 3px;\n left: 3px;\n width: ${size - 6}px;\n height: ${size - 6}px;\n border-radius: 50%;\n background: #fff;\n transition: 0.2s;\n transform: ${checked ? `translateX(${Math.round(size * 0.7)}px)` : "none"};\n}`,
};
@@ -118,9 +185,13 @@ export function cssCheckbox(color: string, size: number): CssGenResult {
};
}
-export function cssTextGlitch(colorA: string, colorB: string, intensity: number): CssGenResult {
+export function cssTextGlitch(
+ colorA: string,
+ colorB: string,
+ intensity: number,
+): CssGenResult {
const offset = Math.max(1, intensity);
return {
css: `.glitch {\n position: relative;\n}\n.glitch::before, .glitch::after {\n content: attr(data-text);\n position: absolute;\n inset: 0;\n}\n.glitch::before {\n color: ${colorA};\n animation: glitch-a ${0.6 * (2 - intensity / 10)}s infinite linear alternate-reverse;\n transform: translate(${offset}px, 0);\n}\n.glitch::after {\n color: ${colorB};\n animation: glitch-b ${0.4 * (2 - intensity / 10)}s infinite linear alternate-reverse;\n transform: translate(-${offset}px, 0);\n}\n@keyframes glitch-a {\n 0% { clip-path: inset(0 0 85% 0) }\n 25% { clip-path: inset(15% 0 60% 0) }\n 50% { clip-path: inset(40% 0 40% 0) }\n 75% { clip-path: inset(60% 0 15% 0) }\n 100% { clip-path: inset(85% 0 0 0) }\n}\n@keyframes glitch-b {\n 0% { clip-path: inset(85% 0 0 0) }\n 25% { clip-path: inset(60% 0 15% 0) }\n 50% { clip-path: inset(40% 0 40% 0) }\n 75% { clip-path: inset(15% 0 60% 0) }\n 100% { clip-path: inset(0 0 85% 0) }\n}`,
};
-}
\ No newline at end of file
+}
diff --git a/artifacts/personal-tool-console/src/lib/tools/currency-converter.ts b/artifacts/personal-tool-console/src/lib/tools/currency-converter.ts
index 73dc2ed..83b18ff 100644
--- a/artifacts/personal-tool-console/src/lib/tools/currency-converter.ts
+++ b/artifacts/personal-tool-console/src/lib/tools/currency-converter.ts
@@ -1,15 +1,28 @@
export const CURRENCY_RATES: Record = {
- usd: 1, myr: 4.7, eur: 0.92, gbp: 0.78,
- jpy: 154, sgd: 1.35, aud: 1.51, idr: 15850,
+ usd: 1,
+ myr: 4.7,
+ eur: 0.92,
+ gbp: 0.78,
+ jpy: 154,
+ sgd: 1.35,
+ aud: 1.51,
+ idr: 15850,
};
export const CURRENCY_SYMBOLS: Record = {
- usd: '$', myr: 'RM', eur: '€', gbp: '£',
- jpy: '¥', sgd: 'S$', aud: 'A$', idr: 'Rp',
+ usd: "$",
+ myr: "RM",
+ eur: "€",
+ gbp: "£",
+ jpy: "¥",
+ sgd: "S$",
+ aud: "A$",
+ idr: "Rp",
};
export function convertCurrency(value: number, from: string, to: string) {
- if (!CURRENCY_RATES[from] || !CURRENCY_RATES[to]) throw new Error('Unsupported currency');
+ if (!CURRENCY_RATES[from] || !CURRENCY_RATES[to])
+ throw new Error("Unsupported currency");
return (value / CURRENCY_RATES[from]) * CURRENCY_RATES[to];
}
diff --git a/artifacts/personal-tool-console/src/lib/tools/dns.ts b/artifacts/personal-tool-console/src/lib/tools/dns.ts
index 439f8e3..d1782e7 100644
--- a/artifacts/personal-tool-console/src/lib/tools/dns.ts
+++ b/artifacts/personal-tool-console/src/lib/tools/dns.ts
@@ -1,24 +1,42 @@
-interface DnsRecord { name: string; type: number; TTL: number; data: string; }
-interface DnsResponse { Status: number; Answer?: DnsRecord[]; }
+interface DnsRecord {
+ name: string;
+ type: number;
+ TTL: number;
+ data: string;
+}
+interface DnsResponse {
+ Status: number;
+ Answer?: DnsRecord[];
+}
export async function dnsLookup(domain: string) {
const t0 = performance.now();
const [resA, resAAAA] = await Promise.all([
- fetch(`https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(domain)}&type=A`, {
- headers: { Accept: 'application/dns-json' },
- }),
- fetch(`https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(domain)}&type=AAAA`, {
- headers: { Accept: 'application/dns-json' },
- }),
+ fetch(
+ `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(domain)}&type=A`,
+ {
+ headers: { Accept: "application/dns-json" },
+ },
+ ),
+ fetch(
+ `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(domain)}&type=AAAA`,
+ {
+ headers: { Accept: "application/dns-json" },
+ },
+ ),
]);
const responseMs = Math.round(performance.now() - t0);
const [dataA, dataAAAA] = await Promise.all([
resA.json() as Promise,
resAAAA.json() as Promise,
]);
- const ipv4 = (dataA.Answer ?? []).filter((r) => r.type === 1).map((r) => r.data);
- const ipv6 = (dataAAAA.Answer ?? []).filter((r) => r.type === 28).map((r) => r.data);
+ const ipv4 = (dataA.Answer ?? [])
+ .filter((r) => r.type === 1)
+ .map((r) => r.data);
+ const ipv6 = (dataAAAA.Answer ?? [])
+ .filter((r) => r.type === 28)
+ .map((r) => r.data);
const ttl = dataA.Answer?.[0]?.TTL ?? null;
- const status = dataA.Status === 0 ? 'resolved' : `NXDOMAIN (${dataA.Status})`;
+ const status = dataA.Status === 0 ? "resolved" : `NXDOMAIN (${dataA.Status})`;
return { domain, ipv4, ipv6, ttl, responseMs, status };
}
diff --git a/artifacts/personal-tool-console/src/lib/tools/hash.ts b/artifacts/personal-tool-console/src/lib/tools/hash.ts
index 1378fc2..bf017a0 100644
--- a/artifacts/personal-tool-console/src/lib/tools/hash.ts
+++ b/artifacts/personal-tool-console/src/lib/tools/hash.ts
@@ -1,18 +1,23 @@
async function digest(algo: AlgorithmIdentifier, text: string) {
const data = new TextEncoder().encode(text);
const buf = await crypto.subtle.digest(algo, data);
- return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, '0')).join('');
+ return Array.from(new Uint8Array(buf))
+ .map((b) => b.toString(16).padStart(2, "0"))
+ .join("");
}
export async function hashText(text: string) {
const [sha1, sha256, sha512] = await Promise.all([
- digest('SHA-1', text),
- digest('SHA-256', text),
- digest('SHA-512', text),
+ digest("SHA-1", text),
+ digest("SHA-256", text),
+ digest("SHA-512", text),
]);
return { sha1, sha256, sha512 };
}
-export async function hashWithAlgo(text: string, algo: 'SHA-1' | 'SHA-256' | 'SHA-384' | 'SHA-512') {
+export async function hashWithAlgo(
+ text: string,
+ algo: "SHA-1" | "SHA-256" | "SHA-384" | "SHA-512",
+) {
return digest(algo, text);
}
diff --git a/artifacts/personal-tool-console/src/lib/tools/linux.ts b/artifacts/personal-tool-console/src/lib/tools/linux.ts
index ab3171c..775a65f 100644
--- a/artifacts/personal-tool-console/src/lib/tools/linux.ts
+++ b/artifacts/personal-tool-console/src/lib/tools/linux.ts
@@ -1,7 +1,9 @@
// Linux / sysadmin helpers — pure calculators and parsers, no shell execution.
const MODE_BITS: Array<[key: string, value: number]> = [
- ["r", 4], ["w", 2], ["x", 1],
+ ["r", 4],
+ ["w", 2],
+ ["x", 1],
];
function modeSymbolsForOct(oct: number): string {
@@ -27,14 +29,19 @@ function octForSymbols(symbols: string): number {
}
export function chmodSymbolicToOctal(symbolic: string): string {
- const parts = symbolic.split(",").map((p) => p.trim()).filter(Boolean);
+ const parts = symbolic
+ .split(",")
+ .map((p) => p.trim())
+ .filter(Boolean);
let result = 0;
for (const part of parts) {
const m = part.match(/^([ugoa]*)([+\-=])([rwx]*)$/);
if (!m) throw new Error(`Invalid symbolic mode: "${part}"`);
const [, targets, op, perms] = m;
const targetSet = targets === "" ? ["u", "g", "o"] : targets.split("");
- const value = perms.split("").reduce((acc, p) => acc + (p === "r" ? 4 : p === "w" ? 2 : 1), 0);
+ const value = perms
+ .split("")
+ .reduce((acc, p) => acc + (p === "r" ? 4 : p === "w" ? 2 : 1), 0);
const masked = targetSet.reduce((acc, t) => {
const shift = t === "u" ? 2 : t === "g" ? 1 : 0;
return acc | (value << (shift * 3));
@@ -54,14 +61,17 @@ export function chmodSymbolicToOctal(symbolic: string): string {
export function chmodOctalToSymbolic(oct: number | string): string {
const n = typeof oct === "string" ? parseInt(oct, 8) : oct;
- if (isNaN(n) || n < 0 || n > 777) throw new Error("Octal mode must be between 000 and 777");
+ if (isNaN(n) || n < 0 || n > 777)
+ throw new Error("Octal mode must be between 000 and 777");
return modeSymbolsForOct(n);
}
export function chmodOctalToBits(oct: number | string) {
const n = typeof oct === "string" ? parseInt(oct, 8) : oct;
const digits = n.toString(8).padStart(3, "0").slice(-3).split("").map(Number);
- return digits.map((d) => MODE_BITS.map(([ch]) => Boolean(d & (ch === "r" ? 4 : ch === "w" ? 2 : 1))));
+ return digits.map((d) =>
+ MODE_BITS.map(([ch]) => Boolean(d & (ch === "r" ? 4 : ch === "w" ? 2 : 1))),
+ );
}
export function chmodBitsToOctal(bits: boolean[][]) {
@@ -78,16 +88,33 @@ export interface PermissionBreakdown {
type: string;
symbolic: string;
octal: string;
- rows: Array<{ who: string; r: boolean; w: boolean; x: boolean; special: boolean }>;
+ rows: Array<{
+ who: string;
+ r: boolean;
+ w: boolean;
+ x: boolean;
+ special: boolean;
+ }>;
special: { setuid: boolean; setgid: boolean; sticky: boolean };
human: string;
}
export function decodePermissionLine(line: string): PermissionBreakdown {
const m = line.match(/^([-dbclps])([rwxsStT-]{9})(?:\s|$)/);
- if (!m) throw new Error("Not a valid `ls -l` permission line (expected e.g. drwxr-xr-x)");
+ if (!m)
+ throw new Error(
+ "Not a valid `ls -l` permission line (expected e.g. drwxr-xr-x)",
+ );
const [, typeCh, perms] = m;
- const typeMap: Record = { d: "Directory", "-": "File", l: "Symbolic link", b: "Block device", c: "Character device", p: "Named pipe", s: "Socket" };
+ const typeMap: Record = {
+ d: "Directory",
+ "-": "File",
+ l: "Symbolic link",
+ b: "Block device",
+ c: "Character device",
+ p: "Named pipe",
+ s: "Socket",
+ };
const rows: PermissionBreakdown["rows"] = [];
for (let i = 0; i < 3; i++) {
const chunk = perms.slice(i * 3, i * 3 + 3);
@@ -96,7 +123,11 @@ export function decodePermissionLine(line: string): PermissionBreakdown {
r: chunk[0] === "r",
w: chunk[1] === "w",
x: chunk[2] === "x" || chunk[2] === "s" || chunk[2] === "t",
- special: chunk[2] === "s" || chunk[2] === "S" || chunk[2] === "t" || chunk[2] === "T",
+ special:
+ chunk[2] === "s" ||
+ chunk[2] === "S" ||
+ chunk[2] === "t" ||
+ chunk[2] === "T",
});
}
let octal = 0;
@@ -111,8 +142,20 @@ export function decodePermissionLine(line: string): PermissionBreakdown {
if (special.setuid) octal += 4000;
if (special.setgid) octal += 2000;
if (special.sticky) octal += 1000;
- const human = rows.map((r) => `${r.who}: ${r.r ? "read" : "no read"} / ${r.w ? "write" : "no write"} / ${r.x ? "execute" : "no execute"}`).join("\n");
- return { type: typeMap[typeCh] ?? typeCh, symbolic: perms, octal: String(octal).padStart(4, "0"), rows, special, human };
+ const human = rows
+ .map(
+ (r) =>
+ `${r.who}: ${r.r ? "read" : "no read"} / ${r.w ? "write" : "no write"} / ${r.x ? "execute" : "no execute"}`,
+ )
+ .join("\n");
+ return {
+ type: typeMap[typeCh] ?? typeCh,
+ symbolic: perms,
+ octal: String(octal).padStart(4, "0"),
+ rows,
+ special,
+ human,
+ };
}
export function chmodBinaryToOctal(checkboxes: Array>): string {
@@ -122,9 +165,27 @@ export function chmodBinaryToOctal(checkboxes: Array>): string {
// ---- cron ----
const DAY_NAMES = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
-const MONTH_NAMES = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
+const MONTH_NAMES = [
+ "jan",
+ "feb",
+ "mar",
+ "apr",
+ "may",
+ "jun",
+ "jul",
+ "aug",
+ "sep",
+ "oct",
+ "nov",
+ "dec",
+];
-function expandCronField(field: string, min: number, max: number, names: string[] = []): number[] {
+function expandCronField(
+ field: string,
+ min: number,
+ max: number,
+ names: string[] = [],
+): number[] {
const result = new Set();
const named = (v: string) => {
const lower = v.toLowerCase();
@@ -143,7 +204,8 @@ function expandCronField(field: string, min: number, max: number, names: string[
if (stepMatch) {
const [, base, stepStr] = stepMatch;
const step = Math.max(1, Number(stepStr));
- const range = base === "*" ? [min, max] : base.split("-").map((v) => named(v));
+ const range =
+ base === "*" ? [min, max] : base.split("-").map((v) => named(v));
for (let i = range[0]; i <= range[1]; i += step) result.add(i);
continue;
}
@@ -169,7 +231,10 @@ export interface CronSchedule {
export function parseCronExpression(expression: string): CronSchedule {
const parts = expression.trim().split(/\s+/);
const n = parts.length;
- if (n !== 5 && n !== 6) throw new Error("Cron must have 5 or 6 fields: minute hour day month weekday (or seconds minute hour day month weekday)");
+ if (n !== 5 && n !== 6)
+ throw new Error(
+ "Cron must have 5 or 6 fields: minute hour day month weekday (or seconds minute hour day month weekday)",
+ );
const useSeconds = n === 6;
const minuteField = useSeconds ? parts[2] : parts[0];
const hourField = useSeconds ? parts[3] : parts[1];
@@ -186,7 +251,11 @@ export function parseCronExpression(expression: string): CronSchedule {
};
}
-export function cronNextTimes(expression: string, count = 5, from = new Date()): Date[] {
+export function cronNextTimes(
+ expression: string,
+ count = 5,
+ from = new Date(),
+): Date[] {
const s = parseCronExpression(expression);
const matches = (d: Date) => {
const min = d.getMinutes();
@@ -194,7 +263,12 @@ export function cronNextTimes(expression: string, count = 5, from = new Date()):
const day = d.getDate();
const mon = d.getMonth() + 1;
const wd = d.getDay();
- return s.minute.includes(min) && s.hour.includes(hr) && s.month.includes(mon) && (s.day.includes(day) || s.weekday.includes(wd));
+ return (
+ s.minute.includes(min) &&
+ s.hour.includes(hr) &&
+ s.month.includes(mon) &&
+ (s.day.includes(day) || s.weekday.includes(wd))
+ );
};
const out: Date[] = [];
const cur = new Date(from);
@@ -227,18 +301,20 @@ export function buildCronExpression(fields: CronFields): string {
// ---- paths ----
export function posixJoin(...parts: string[]): string {
- return parts
- .flatMap((p) => p.split("/"))
- .reduce((acc, part) => {
- if (!part || part === ".") return acc;
- if (part === "..") {
- const s = acc.split("/");
- s.pop();
- return s.join("/");
- }
- return acc ? `${acc}/${part}` : part;
- }, "")
- .replace(/\/{2,}/g, "/") || ".";
+ return (
+ parts
+ .flatMap((p) => p.split("/"))
+ .reduce((acc, part) => {
+ if (!part || part === ".") return acc;
+ if (part === "..") {
+ const s = acc.split("/");
+ s.pop();
+ return s.join("/");
+ }
+ return acc ? `${acc}/${part}` : part;
+ }, "")
+ .replace(/\/{2,}/g, "/") || "."
+ );
}
export function posixDirname(p: string): string {
@@ -281,11 +357,31 @@ export function posixNormalize(p: string): string {
// ---- uid/gid reference ----
-export const UID_GID_TABLE: Array<{ name: string; uid: number; gid: number; description: string }> = [
- { name: "root", uid: 0, gid: 0, description: "Superuser / system administrator" },
+export const UID_GID_TABLE: Array<{
+ name: string;
+ uid: number;
+ gid: number;
+ description: string;
+}> = [
+ {
+ name: "root",
+ uid: 0,
+ gid: 0,
+ description: "Superuser / system administrator",
+ },
{ name: "bin", uid: 1, gid: 1, description: "Executable binaries owner" },
- { name: "daemon", uid: 2, gid: 2, description: "Background system processes" },
- { name: "adm", uid: 3, gid: 4, description: "Administrative group / log files" },
+ {
+ name: "daemon",
+ uid: 2,
+ gid: 2,
+ description: "Background system processes",
+ },
+ {
+ name: "adm",
+ uid: 3,
+ gid: 4,
+ description: "Administrative group / log files",
+ },
{ name: "lp", uid: 4, gid: 7, description: "Printing subsystem" },
{ name: "sync", uid: 5, gid: 0, description: "Synchronize binaries" },
{ name: "shutdown", uid: 6, gid: 0, description: "Power off the system" },
@@ -297,20 +393,46 @@ export const UID_GID_TABLE: Array<{ name: string; uid: number; gid: number; desc
{ name: "games", uid: 12, gid: 100, description: "Games" },
{ name: "gopher", uid: 13, gid: 30, description: "Gopher server" },
{ name: "ftp", uid: 14, gid: 50, description: "FTP server" },
- { name: "nobody", uid: 65534, gid: 65534, description: "Unprivileged process user" },
- { name: "systemd-network", uid: 100, gid: 102, description: "systemd network management" },
- { name: "systemd-resolve", uid: 101, gid: 103, description: "systemd resolver" },
+ {
+ name: "nobody",
+ uid: 65534,
+ gid: 65534,
+ description: "Unprivileged process user",
+ },
+ {
+ name: "systemd-network",
+ uid: 100,
+ gid: 102,
+ description: "systemd network management",
+ },
+ {
+ name: "systemd-resolve",
+ uid: 101,
+ gid: 103,
+ description: "systemd resolver",
+ },
{ name: "sshd", uid: 74, gid: 74, description: "SSH daemon" },
- { name: "www-data", uid: 33, gid: 33, description: "Web server (Apache/Nginx)" },
+ {
+ name: "www-data",
+ uid: 33,
+ gid: 33,
+ description: "Web server (Apache/Nginx)",
+ },
{ name: "postgres", uid: 999, gid: 999, description: "PostgreSQL server" },
{ name: "mysql", uid: 27, gid: 27, description: "MySQL server" },
{ name: "redis", uid: 999, gid: 999, description: "Redis server" },
- { name: "vboxadd", uid: 999, gid: 999, description: "VirtualBox guest additions" },
+ {
+ name: "vboxadd",
+ uid: 999,
+ gid: 999,
+ description: "VirtualBox guest additions",
+ },
];
export function lookupUidGid(query: string) {
const q = query.toLowerCase();
return UID_GID_TABLE.filter(
- (row) => row.name.includes(q) || String(row.uid) === q || String(row.gid) === q,
+ (row) =>
+ row.name.includes(q) || String(row.uid) === q || String(row.gid) === q,
);
-}
\ No newline at end of file
+}
diff --git a/artifacts/personal-tool-console/src/lib/tools/misc.ts b/artifacts/personal-tool-console/src/lib/tools/misc.ts
index 05b6bf5..4e772f1 100644
--- a/artifacts/personal-tool-console/src/lib/tools/misc.ts
+++ b/artifacts/personal-tool-console/src/lib/tools/misc.ts
@@ -1,7 +1,10 @@
// Miscellaneous pure helpers: list shuffle, barcode encoders, fake IBAN.
export function shuffleList(text: string): string {
- const items = text.split("\n").map((l) => l.trim()).filter(Boolean);
+ const items = text
+ .split("\n")
+ .map((l) => l.trim())
+ .filter(Boolean);
for (let i = items.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[items[i], items[j]] = [items[j], items[i]];
@@ -12,23 +15,60 @@ export function shuffleList(text: string): string {
// --- Code 39 barcode (full ASCII 43-char alphabet) ---
const CODE39: Array<[string, string]> = [
- ["0", "nnnwwnwnn"], ["1", "wnnwnnnnw"], ["2", "nnwwnnnnw"], ["3", "wnwwnnnnn"],
- ["4", "nnnwwnnnw"], ["5", "wnnwwnnnn"], ["6", "nnwwwnnnn"], ["7", "nnnwnnwnw"],
- ["8", "wnnwnnwnn"], ["9", "nnwwnnwnn"], ["A", "wnnnnwnnw"], ["B", "nnwnnwnnw"],
- ["C", "wnwnnwnnn"], ["D", "nnnnwwnnw"], ["E", "wnnnwwnnn"], ["F", "nnwnwwnnn"],
- ["G", "nnnnnwwnw"], ["H", "wnnnnwwnn"], ["I", "nnwnnwwnn"], ["J", "nnnnwwwnn"],
- ["K", "wnnnnnnww"], ["L", "nnwnnnnww"], ["M", "wnwnnnnwn"], ["N", "nnnnwnnww"],
- ["O", "wnnnwnnwn"], ["P", "nnwnwnnwn"], ["Q", "nnnnnnwww"], ["R", "wnnnnnwwn"],
- ["S", "nnwnnnwwn"], ["T", "nnnnwnwwn"], ["U", "wwnnnnnnw"], ["V", "nwwnnnnnw"],
- ["W", "wwwnnnnnn"], ["X", "nwnnwnnnw"], ["Y", "wwnnwnnnn"], ["Z", "nwwnwnnnn"],
- ["-", "nwnnnnwnw"], [".", "wwnnnnwnn"], [" ", "nwwnnnwnn"], ["$", "nwnwnwnnn"],
- ["/", "nwnwnnnwn"], ["+", "nwnnnwnwn"], ["%", "nnnwnwnwn"],
+ ["0", "nnnwwnwnn"],
+ ["1", "wnnwnnnnw"],
+ ["2", "nnwwnnnnw"],
+ ["3", "wnwwnnnnn"],
+ ["4", "nnnwwnnnw"],
+ ["5", "wnnwwnnnn"],
+ ["6", "nnwwwnnnn"],
+ ["7", "nnnwnnwnw"],
+ ["8", "wnnwnnwnn"],
+ ["9", "nnwwnnwnn"],
+ ["A", "wnnnnwnnw"],
+ ["B", "nnwnnwnnw"],
+ ["C", "wnwnnwnnn"],
+ ["D", "nnnnwwnnw"],
+ ["E", "wnnnwwnnn"],
+ ["F", "nnwnwwnnn"],
+ ["G", "nnnnnwwnw"],
+ ["H", "wnnnnwwnn"],
+ ["I", "nnwnnwwnn"],
+ ["J", "nnnnwwwnn"],
+ ["K", "wnnnnnnww"],
+ ["L", "nnwnnnnww"],
+ ["M", "wnwnnnnwn"],
+ ["N", "nnnnwnnww"],
+ ["O", "wnnnwnnwn"],
+ ["P", "nnwnwnnwn"],
+ ["Q", "nnnnnnwww"],
+ ["R", "wnnnnnwwn"],
+ ["S", "nnwnnnwwn"],
+ ["T", "nnnnwnwwn"],
+ ["U", "wwnnnnnnw"],
+ ["V", "nwwnnnnnw"],
+ ["W", "wwwnnnnnn"],
+ ["X", "nwnnwnnnw"],
+ ["Y", "wwnnwnnnn"],
+ ["Z", "nwwnwnnnn"],
+ ["-", "nwnnnnwnw"],
+ [".", "wwnnnnwnn"],
+ [" ", "nwwnnnwnn"],
+ ["$", "nwnwnwnnn"],
+ ["/", "nwnwnnnwn"],
+ ["+", "nwnnnwnwn"],
+ ["%", "nnnwnwnwn"],
];
const CODE39_BY_CODE = new Map(CODE39.map(([ch, code]) => [ch, code]));
const CODE39_BY_PATTERN = new Map(CODE39.map(([ch, code]) => [code, ch]));
-export function code39Encode(text: string): { svg: string; width: number; valid: boolean; error?: string } {
+export function code39Encode(text: string): {
+ svg: string;
+ width: number;
+ valid: boolean;
+ error?: string;
+} {
const upper = text.toUpperCase();
const unknown = [...upper].find((ch) => !CODE39_BY_CODE.has(ch));
if (unknown) {
@@ -39,21 +79,25 @@ export function code39Encode(text: string): { svg: string; width: number; valid:
error: `Code 39 cannot encode "${unknown}" — allowed: A–Z, 0–9, space and - . $ / + %`,
};
}
- const patterns = ["*", ...upper.split(""), "*"].map((ch) => CODE39_BY_CODE.get(ch)!);
+ const patterns = ["*", ...upper.split(""), "*"].map(
+ (ch) => CODE39_BY_CODE.get(ch)!,
+ );
const BARS = patterns.join("0"); // '0' = inter-character narrow gap
const moduleW = 2;
let x = 0;
- const rects = BARS.split("").map((bit, i) => {
- if (bit === "0") {
- x += moduleW;
- return null;
- }
- const wide = bit === "W";
- const w = (wide ? 3 : 1) * moduleW;
- const r = { x, w };
- x += w;
- return r;
- }).filter((r): r is { x: number; w: number } => r !== null);
+ const rects = BARS.split("")
+ .map((bit, i) => {
+ if (bit === "0") {
+ x += moduleW;
+ return null;
+ }
+ const wide = bit === "W";
+ const w = (wide ? 3 : 1) * moduleW;
+ const r = { x, w };
+ x += w;
+ return r;
+ })
+ .filter((r): r is { x: number; w: number } => r !== null);
const svg = `${rects
.map((r) => ` `)
.join("")} `;
@@ -63,23 +107,56 @@ export function code39Encode(text: string): { svg: string; width: number; valid:
// --- EAN-13 ---
const EAN_L_CODE = [
- "0001101", "0011001", "0010011", "0111101", "0100011", "0110001", "0101111", "0111011", "0110111", "0001011",
+ "0001101",
+ "0011001",
+ "0010011",
+ "0111101",
+ "0100011",
+ "0110001",
+ "0101111",
+ "0111011",
+ "0110111",
+ "0001011",
];
const EAN_G_CODE = [
- "0100111", "0110011", "0011011", "0100001", "0011101", "0111001", "0000101", "0010001", "0001001", "0010111",
+ "0100111",
+ "0110011",
+ "0011011",
+ "0100001",
+ "0011101",
+ "0111001",
+ "0000101",
+ "0010001",
+ "0001001",
+ "0010111",
];
const EAN_R_CODE = EAN_L_CODE.map((c) => c.split("").reverse().join(""));
const EAN_FIRST: Record = {
- "0": ["L", "L", "L", "L", "L", "L"], "1": ["L", "L", "G", "L", "G", "G"], "2": ["L", "L", "G", "G", "L", "G"],
- "3": ["L", "L", "G", "G", "G", "L"], "4": ["L", "G", "L", "L", "G", "G"], "5": ["L", "G", "G", "L", "L", "G"],
- "6": ["L", "G", "G", "G", "L", "L"], "7": ["L", "G", "L", "G", "L", "G"], "8": ["L", "G", "L", "G", "G", "L"],
+ "0": ["L", "L", "L", "L", "L", "L"],
+ "1": ["L", "L", "G", "L", "G", "G"],
+ "2": ["L", "L", "G", "G", "L", "G"],
+ "3": ["L", "L", "G", "G", "G", "L"],
+ "4": ["L", "G", "L", "L", "G", "G"],
+ "5": ["L", "G", "G", "L", "L", "G"],
+ "6": ["L", "G", "G", "G", "L", "L"],
+ "7": ["L", "G", "L", "G", "L", "G"],
+ "8": ["L", "G", "L", "G", "G", "L"],
"9": ["L", "G", "G", "L", "G", "L"],
};
-export function ean13(input: string): { svg: string; width: number; valid: boolean; error?: string } {
- let digits = input.replace(/\s+/g, "").replace(/^698/, "").replace(/[^0-9]/g, "");
+export function ean13(input: string): {
+ svg: string;
+ width: number;
+ valid: boolean;
+ error?: string;
+} {
+ let digits = input
+ .replace(/\s+/g, "")
+ .replace(/^698/, "")
+ .replace(/[^0-9]/g, "");
if (digits.length === 12) {
- let odd = 0, even = 0;
+ let odd = 0,
+ even = 0;
for (let i = 0; i < 12; i++) {
const v = Number(digits[i]);
if (i % 2 === 0) odd += v;
@@ -88,22 +165,39 @@ export function ean13(input: string): { svg: string; width: number; valid: boole
const check = (10 - ((odd + even * 3) % 10)) % 10;
digits += String(check);
}
- if (digits.length !== 13) return { svg: "", width: 0, valid: false, error: "Enter 12 digits to auto-compute a checksum, or the full 13-digit EAN." };
+ if (digits.length !== 13)
+ return {
+ svg: "",
+ width: 0,
+ valid: false,
+ error:
+ "Enter 12 digits to auto-compute a checksum, or the full 13-digit EAN.",
+ };
const chars = digits.split("");
- const patterns = [EAN_R_CODE[Number(chars[12])], ...EAN_FIRST[chars[0]].map((kind, i) => {
- const d = Number(chars[i + 1]);
- return kind === "L" ? EAN_L_CODE[d] : EAN_G_CODE[d];
- }), ...chars.slice(7, 12).map((c) => EAN_R_CODE[Number(c)])];
+ const patterns = [
+ EAN_R_CODE[Number(chars[12])],
+ ...EAN_FIRST[chars[0]].map((kind, i) => {
+ const d = Number(chars[i + 1]);
+ return kind === "L" ? EAN_L_CODE[d] : EAN_G_CODE[d];
+ }),
+ ...chars.slice(7, 12).map((c) => EAN_R_CODE[Number(c)]),
+ ];
const full = patterns.join("");
const moduleW = 2;
let x = 0;
- const rects = full.split("").map((bit, i) => {
- if (bit === "0") { x += moduleW; return null; }
- const r = { x, w: moduleW };
- x += moduleW;
- return r;
- }).filter((r): r is { x: number; w: number } => r !== null);
+ const rects = full
+ .split("")
+ .map((bit, i) => {
+ if (bit === "0") {
+ x += moduleW;
+ return null;
+ }
+ const r = { x, w: moduleW };
+ x += moduleW;
+ return r;
+ })
+ .filter((r): r is { x: number; w: number } => r !== null);
const total = x + 14;
const svg = `${rects
.map((r) => ` `)
@@ -129,24 +223,33 @@ const IBAN_COUNTRIES: Array<{ code: string; length: number; bban: string }> = [
];
function replaceAlpha(code: string): string {
- return code.split("").map((ch) => (ch >= "A" && ch <= "Z" ? String(ch.charCodeAt(0) - 55) : ch)).join("");
+ return code
+ .split("")
+ .map((ch) => (ch >= "A" && ch <= "Z" ? String(ch.charCodeAt(0) - 55) : ch))
+ .join("");
}
function ibanChecksum(country: string, bban: string): string {
const moved = bban + country + "00";
const numeric = replaceAlpha(moved);
- const remainder = numeric.split("").reduce((acc, digit) => (acc * 10 + Number(digit)) % 97, 0);
+ const remainder = numeric
+ .split("")
+ .reduce((acc, digit) => (acc * 10 + Number(digit)) % 97, 0);
const check = (98 - remainder) % 97;
return String(check).padStart(2, "0");
}
export function fakeIban(countryCode: string, seed: number): string {
- const country = IBAN_COUNTRIES.find((c) => c.code === countryCode) ?? IBAN_COUNTRIES[0];
+ const country =
+ IBAN_COUNTRIES.find((c) => c.code === countryCode) ?? IBAN_COUNTRIES[0];
const targetLen = country.length - 4;
let bban = country.bban.slice(0, targetLen);
if (bban.length < targetLen) {
const digits = "0123456789";
- const fill = Array.from({ length: targetLen - bban.length }, (_, i) => digits[(seed + (i + 1) * 3) % 10]).join("");
+ const fill = Array.from(
+ { length: targetLen - bban.length },
+ (_, i) => digits[(seed + (i + 1) * 3) % 10],
+ ).join("");
bban += fill;
}
const check = ibanChecksum(country.code, bban);
@@ -160,6 +263,8 @@ export function validateIbanFormat(iban: string): boolean {
if (!/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(cleaned)) return false;
const moved = cleaned.slice(4) + cleaned.slice(0, 4);
const numeric = replaceAlpha(moved);
- const remainder = numeric.split("").reduce((acc, digit) => (acc * 10 + Number(digit)) % 97, 0);
+ const remainder = numeric
+ .split("")
+ .reduce((acc, digit) => (acc * 10 + Number(digit)) % 97, 0);
return remainder === 1;
-}
\ No newline at end of file
+}
diff --git a/artifacts/personal-tool-console/src/lib/tools/password.ts b/artifacts/personal-tool-console/src/lib/tools/password.ts
index c73d342..f0e9359 100644
--- a/artifacts/personal-tool-console/src/lib/tools/password.ts
+++ b/artifacts/personal-tool-console/src/lib/tools/password.ts
@@ -1,13 +1,14 @@
export function randomPassword(length: number, mode: string) {
const sets: Record = {
- weak: 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789',
- medium: 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#$%?_-',
- strong: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{};:,.<>/?',
+ weak: "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789",
+ medium: "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#$%?_-",
+ strong:
+ "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{};:,.<>/?",
};
const chars = sets[mode] || sets.strong;
const bytes = new Uint32Array(length);
window.crypto.getRandomValues(bytes);
- let out = '';
+ let out = "";
for (let i = 0; i < length; i++) out += chars[bytes[i] % chars.length];
return out;
}
diff --git a/artifacts/personal-tool-console/src/lib/tools/set/index.ts b/artifacts/personal-tool-console/src/lib/tools/set/index.ts
index 9acca32..a247af6 100644
--- a/artifacts/personal-tool-console/src/lib/tools/set/index.ts
+++ b/artifacts/personal-tool-console/src/lib/tools/set/index.ts
@@ -1,5 +1,8 @@
export function parseLines(text: string) {
- return text.split('\n').map((l) => l.trim()).filter(Boolean);
+ return text
+ .split("\n")
+ .map((l) => l.trim())
+ .filter(Boolean);
}
export function setUnion(a: string[], b: string[]) {
@@ -19,7 +22,10 @@ export function setDifference(a: string[], b: string[]) {
export function setSymmetricDiff(a: string[], b: string[]) {
const setA = new Set(a);
const setB = new Set(b);
- return [...a.filter((x) => !setB.has(x)), ...b.filter((x) => !setA.has(x))].sort();
+ return [
+ ...a.filter((x) => !setB.has(x)),
+ ...b.filter((x) => !setA.has(x)),
+ ].sort();
}
export function setMembership(item: string, set: string[]) {
diff --git a/artifacts/personal-tool-console/src/lib/tools/speed-test.ts b/artifacts/personal-tool-console/src/lib/tools/speed-test.ts
index 6d17416..97bfe4b 100644
--- a/artifacts/personal-tool-console/src/lib/tools/speed-test.ts
+++ b/artifacts/personal-tool-console/src/lib/tools/speed-test.ts
@@ -13,38 +13,58 @@ export type SpeedProgress = {
message?: string;
};
-export async function runSpeedTest(onProgress: (s: SpeedProgress) => void): Promise {
- const pingUrl = 'https://speed.cloudflare.com/__down?bytes=0';
- const downloadUrl = 'https://speed.cloudflare.com/__down?bytes=5000000';
- const uploadUrl = '/api/upload-test';
+export async function runSpeedTest(
+ onProgress: (s: SpeedProgress) => void,
+): Promise {
+ const pingUrl = "https://speed.cloudflare.com/__down?bytes=0";
+ const downloadUrl = "https://speed.cloudflare.com/__down?bytes=5000000";
+ const uploadUrl = "/api/upload-test";
- onProgress({ running: true, dl: null, ul: null, ping: null, message: 'Testing ping…' });
+ onProgress({
+ running: true,
+ dl: null,
+ ul: null,
+ ping: null,
+ message: "Testing ping…",
+ });
- let pingMs = '—';
- let dlMbps = '—';
- let ulMbps = '—';
+ let pingMs = "—";
+ let dlMbps = "—";
+ let ulMbps = "—";
try {
const pingTimes: number[] = [];
for (let i = 0; i < 5; i++) {
const t0 = performance.now();
- await fetch(pingUrl, { cache: 'no-store' });
+ await fetch(pingUrl, { cache: "no-store" });
pingTimes.push(performance.now() - t0);
}
pingTimes.sort((a, b) => a - b);
const avg = pingTimes.slice(0, 4).reduce((s, v) => s + v, 0) / 4;
pingMs = avg.toFixed(0);
- onProgress({ running: true, dl: null, ul: null, ping: pingMs, message: `Ping ${pingMs} ms — testing download…` });
+ onProgress({
+ running: true,
+ dl: null,
+ ul: null,
+ ping: pingMs,
+ message: `Ping ${pingMs} ms — testing download…`,
+ });
} catch {
- onProgress({ running: true, dl: null, ul: null, ping: '—', message: 'Ping failed — testing download…' });
+ onProgress({
+ running: true,
+ dl: null,
+ ul: null,
+ ping: "—",
+ message: "Ping failed — testing download…",
+ });
}
try {
const dlStart = performance.now();
- const res = await fetch(downloadUrl, { cache: 'no-store' });
+ const res = await fetch(downloadUrl, { cache: "no-store" });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const reader = res.body?.getReader();
- if (!reader) throw new Error('Streaming not supported');
+ if (!reader) throw new Error("Streaming not supported");
let loaded = 0;
while (true) {
const { done, value } = await reader.read();
@@ -53,21 +73,45 @@ export async function runSpeedTest(onProgress: (s: SpeedProgress) => void): Prom
}
const dlSec = (performance.now() - dlStart) / 1000;
dlMbps = ((loaded * 8) / dlSec / 1_000_000).toFixed(2);
- onProgress({ running: true, dl: dlMbps, ul: null, ping: pingMs, message: `↓ ${dlMbps} Mbps — testing upload…` });
+ onProgress({
+ running: true,
+ dl: dlMbps,
+ ul: null,
+ ping: pingMs,
+ message: `↓ ${dlMbps} Mbps — testing upload…`,
+ });
} catch {
- onProgress({ running: true, dl: '—', ul: null, ping: pingMs, message: 'Download failed — testing upload…' });
+ onProgress({
+ running: true,
+ dl: "—",
+ ul: null,
+ ping: pingMs,
+ message: "Download failed — testing upload…",
+ });
}
- let ulError = '';
+ let ulError = "";
try {
- const payload = JSON.stringify({ d: Array.from({ length: 2000 }, () => Math.random().toString(36)).join('') });
+ const payload = JSON.stringify({
+ d: Array.from({ length: 2000 }, () => Math.random().toString(36)).join(
+ "",
+ ),
+ });
const byteCount = new TextEncoder().encode(payload).byteLength;
- await fetch(uploadUrl, { method: 'POST', body: payload, headers: { 'Content-Type': 'application/json' } });
+ await fetch(uploadUrl, {
+ method: "POST",
+ body: payload,
+ headers: { "Content-Type": "application/json" },
+ });
const ROUNDS = 12;
let totalBytes = 0;
const ulStart = performance.now();
for (let i = 0; i < ROUNDS; i++) {
- const res = await fetch(uploadUrl, { method: 'POST', body: payload, headers: { 'Content-Type': 'application/json' } });
+ const res = await fetch(uploadUrl, {
+ method: "POST",
+ body: payload,
+ headers: { "Content-Type": "application/json" },
+ });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
await res.json();
totalBytes += byteCount;
@@ -76,37 +120,37 @@ export async function runSpeedTest(onProgress: (s: SpeedProgress) => void): Prom
ulMbps = ((totalBytes * 8) / ulSec / 1_000_000).toFixed(2);
} catch (e) {
ulError = (e as Error).message;
- ulMbps = '—';
+ ulMbps = "—";
}
- const message = ulError ? `Upload failed: ${ulError}` : 'Test complete';
+ const message = ulError ? `Upload failed: ${ulError}` : "Test complete";
onProgress({ running: false, dl: dlMbps, ul: ulMbps, ping: pingMs, message });
return { downloadMbps: dlMbps, uploadMbps: ulMbps, pingMs, message };
}
export function downloadQuality(mbps: string) {
const v = parseFloat(mbps);
- if (isNaN(v)) return { label: '—', color: 'text-slate-400' };
- if (v >= 100) return { label: 'Excellent', color: 'text-cyan-400' };
- if (v >= 25) return { label: 'Good', color: 'text-green-400' };
- if (v >= 5) return { label: 'Fair', color: 'text-amber-400' };
- return { label: 'Poor', color: 'text-red-400' };
+ if (isNaN(v)) return { label: "—", color: "text-slate-400" };
+ if (v >= 100) return { label: "Excellent", color: "text-cyan-400" };
+ if (v >= 25) return { label: "Good", color: "text-green-400" };
+ if (v >= 5) return { label: "Fair", color: "text-amber-400" };
+ return { label: "Poor", color: "text-red-400" };
}
export function uploadQuality(mbps: string) {
const v = parseFloat(mbps);
- if (isNaN(v)) return { label: '—', color: 'text-slate-400' };
- if (v >= 50) return { label: 'Excellent', color: 'text-cyan-400' };
- if (v >= 10) return { label: 'Good', color: 'text-green-400' };
- if (v >= 2) return { label: 'Fair', color: 'text-amber-400' };
- return { label: 'Poor', color: 'text-red-400' };
+ if (isNaN(v)) return { label: "—", color: "text-slate-400" };
+ if (v >= 50) return { label: "Excellent", color: "text-cyan-400" };
+ if (v >= 10) return { label: "Good", color: "text-green-400" };
+ if (v >= 2) return { label: "Fair", color: "text-amber-400" };
+ return { label: "Poor", color: "text-red-400" };
}
export function pingQuality(ms: string) {
const v = parseFloat(ms);
- if (isNaN(v)) return { label: '—', color: 'text-slate-400' };
- if (v < 20) return { label: 'Excellent', color: 'text-cyan-400' };
- if (v < 50) return { label: 'Good', color: 'text-green-400' };
- if (v < 100) return { label: 'Fair', color: 'text-amber-400' };
- return { label: 'Poor', color: 'text-red-400' };
+ if (isNaN(v)) return { label: "—", color: "text-slate-400" };
+ if (v < 20) return { label: "Excellent", color: "text-cyan-400" };
+ if (v < 50) return { label: "Good", color: "text-green-400" };
+ if (v < 100) return { label: "Fair", color: "text-amber-400" };
+ return { label: "Poor", color: "text-red-400" };
}
diff --git a/artifacts/personal-tool-console/src/lib/tools/text/existing.ts b/artifacts/personal-tool-console/src/lib/tools/text/existing.ts
index 2f7b0ec..51e3187 100644
--- a/artifacts/personal-tool-console/src/lib/tools/text/existing.ts
+++ b/artifacts/personal-tool-console/src/lib/tools/text/existing.ts
@@ -1,14 +1,68 @@
import type { TextToolGroup } from "./types";
const LOREM_WORDS = [
- "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit",
- "sed", "do", "eiusmod", "tempor", "incididunt", "ut", "labore", "et", "dolore",
- "magna", "aliqua", "enim", "ad", "minim", "veniam", "quis", "nostrud",
- "exercitation", "ullamco", "laboris", "nisi", "aliquip", "ex", "ea", "commodo",
- "consequat", "duis", "aute", "irure", "in", "reprehenderit", "voluptate",
- "velit", "esse", "cillum", "fugiat", "nulla", "pariatur", "excepteur", "sint",
- "occaecat", "cupidatat", "non", "proident", "sunt", "culpa", "qui", "officia",
- "deserunt", "mollit", "anim", "id", "est", "laborum",
+ "lorem",
+ "ipsum",
+ "dolor",
+ "sit",
+ "amet",
+ "consectetur",
+ "adipiscing",
+ "elit",
+ "sed",
+ "do",
+ "eiusmod",
+ "tempor",
+ "incididunt",
+ "ut",
+ "labore",
+ "et",
+ "dolore",
+ "magna",
+ "aliqua",
+ "enim",
+ "ad",
+ "minim",
+ "veniam",
+ "quis",
+ "nostrud",
+ "exercitation",
+ "ullamco",
+ "laboris",
+ "nisi",
+ "aliquip",
+ "ex",
+ "ea",
+ "commodo",
+ "consequat",
+ "duis",
+ "aute",
+ "irure",
+ "in",
+ "reprehenderit",
+ "voluptate",
+ "velit",
+ "esse",
+ "cillum",
+ "fugiat",
+ "nulla",
+ "pariatur",
+ "excepteur",
+ "sint",
+ "occaecat",
+ "cupidatat",
+ "non",
+ "proident",
+ "sunt",
+ "culpa",
+ "qui",
+ "officia",
+ "deserunt",
+ "mollit",
+ "anim",
+ "id",
+ "est",
+ "laborum",
];
function loremParagraphs(count: number, wordsPer: number, rand: () => number) {
@@ -16,7 +70,8 @@ function loremParagraphs(count: number, wordsPer: number, rand: () => number) {
for (let p = 0; p < count; p++) {
const parts: string[] = [];
const n = Math.max(8, Math.round(wordsPer * (0.8 + rand() * 0.4)));
- for (let i = 0; i < n; i++) parts.push(LOREM_WORDS[Math.floor(rand() * LOREM_WORDS.length)]);
+ for (let i = 0; i < n; i++)
+ parts.push(LOREM_WORDS[Math.floor(rand() * LOREM_WORDS.length)]);
const sentence = parts.join(" ");
out.push(sentence.charAt(0).toUpperCase() + sentence.slice(1) + ".");
}
@@ -27,15 +82,32 @@ export const texts: TextToolGroup[] = [
{
id: "existing",
title: "Classic text tools",
- description: "Existing generators and text utilities kept alongside the expansion.",
+ description:
+ "Existing generators and text utilities kept alongside the expansion.",
tools: [
{
slug: "lorem-ipsum",
name: "Lorem Ipsum Generator",
description: "Generate classic dummy Latin filler text.",
options: [
- { kind: "number", key: "paragraphs", label: "Paragraphs", default: 4, min: 1, max: 50, step: 1 },
- { kind: "number", key: "words", label: "Words per paragraph", default: 24, min: 4, max: 200, step: 1 },
+ {
+ kind: "number",
+ key: "paragraphs",
+ label: "Paragraphs",
+ default: 4,
+ min: 1,
+ max: 50,
+ step: 1,
+ },
+ {
+ kind: "number",
+ key: "words",
+ label: "Words per paragraph",
+ default: 24,
+ min: 4,
+ max: 200,
+ step: 1,
+ },
],
run: (_input, o) => {
const p = Number(o.paragraphs ?? 4);
@@ -46,9 +118,15 @@ export const texts: TextToolGroup[] = [
{
slug: "letter-counter",
name: "Letter Counter",
- description: "Count characters, letters, words, sentences and lines instantly.",
+ description:
+ "Count characters, letters, words, sentences and lines instantly.",
options: [
- { kind: "toggle", key: "caseInsensitive", label: "Case-insensitive", default: true },
+ {
+ kind: "toggle",
+ key: "caseInsensitive",
+ label: "Case-insensitive",
+ default: true,
+ },
],
run: (input, o) => {
const text = o.caseInsensitive ? input.toLowerCase() : input;
@@ -72,9 +150,18 @@ export const texts: TextToolGroup[] = [
{
slug: "bionic-reading",
name: "Bionic Reading",
- description: "Bold the leading characters of every word to help fast reading.",
+ description:
+ "Bold the leading characters of every word to help fast reading.",
options: [
- { kind: "number", key: "bold", label: "Bold letters per word", default: 2, min: 1, max: 6, step: 1 },
+ {
+ kind: "number",
+ key: "bold",
+ label: "Bold letters per word",
+ default: 2,
+ min: 1,
+ max: 6,
+ step: 1,
+ },
],
run: (input, o) => {
const n = Number(o.bold ?? 2);
@@ -92,4 +179,4 @@ export const texts: TextToolGroup[] = [
},
],
},
-];
\ No newline at end of file
+];
diff --git a/artifacts/personal-tool-console/src/lib/tools/text/helpers.ts b/artifacts/personal-tool-console/src/lib/tools/text/helpers.ts
index d51a118..7071b26 100644
--- a/artifacts/personal-tool-console/src/lib/tools/text/helpers.ts
+++ b/artifacts/personal-tool-console/src/lib/tools/text/helpers.ts
@@ -64,92 +64,530 @@ export const VOWELS = "aeiou";
export const CONSONANTS = "bcdfghjklmnpqrstvwxyz";
export const SUPERSCRIPT_MAP: Record = {
- a: "ᵃ", b: "ᵇ", c: "ᶜ", d: "ᵈ", e: "ᵉ", f: "ᶠ", g: "ᵍ", h: "ʰ", i: "ⁱ", j: "ʲ",
- k: "ᵏ", l: "ˡ", m: "ᵐ", n: "ⁿ", o: "ᵒ", p: "ᵖ", q: "ᑫ", r: "ʳ", s: "ˢ", t: "ᵗ",
- u: "ᵘ", v: "ᵛ", w: "ʷ", x: "ˣ", y: "ʸ", z: "ᶻ",
- A: "ᴬ", B: "ᴮ", C: "ꟲ", D: "ᴰ", E: "ᴱ", F: "ꟳ", G: "ᴳ", H: "ᴴ", I: "ᴵ", J: "ᴶ",
- K: "ᴷ", L: "ᴸ", M: "ᴹ", N: "ᴺ", O: "ᴼ", P: "ᴾ", Q: "ꟴ", R: "ᴿ", S: "ˢ", T: "ᵀ",
- U: "ᵁ", V: "ⱽ", W: "ᵂ", X: "ˣ", Y: "ʸ", Z: "ᶻ",
- "0": "⁰", "1": "¹", "2": "²", "3": "³", "4": "⁴", "5": "⁵", "6": "⁶", "7": "⁷", "8": "⁸", "9": "⁹",
- "+": "⁺", "-": "⁻", "=": "⁼", "(": "⁽", ")": "⁾",
+ a: "ᵃ",
+ b: "ᵇ",
+ c: "ᶜ",
+ d: "ᵈ",
+ e: "ᵉ",
+ f: "ᶠ",
+ g: "ᵍ",
+ h: "ʰ",
+ i: "ⁱ",
+ j: "ʲ",
+ k: "ᵏ",
+ l: "ˡ",
+ m: "ᵐ",
+ n: "ⁿ",
+ o: "ᵒ",
+ p: "ᵖ",
+ q: "ᑫ",
+ r: "ʳ",
+ s: "ˢ",
+ t: "ᵗ",
+ u: "ᵘ",
+ v: "ᵛ",
+ w: "ʷ",
+ x: "ˣ",
+ y: "ʸ",
+ z: "ᶻ",
+ A: "ᴬ",
+ B: "ᴮ",
+ C: "ꟲ",
+ D: "ᴰ",
+ E: "ᴱ",
+ F: "ꟳ",
+ G: "ᴳ",
+ H: "ᴴ",
+ I: "ᴵ",
+ J: "ᴶ",
+ K: "ᴷ",
+ L: "ᴸ",
+ M: "ᴹ",
+ N: "ᴺ",
+ O: "ᴼ",
+ P: "ᴾ",
+ Q: "ꟴ",
+ R: "ᴿ",
+ S: "ˢ",
+ T: "ᵀ",
+ U: "ᵁ",
+ V: "ⱽ",
+ W: "ᵂ",
+ X: "ˣ",
+ Y: "ʸ",
+ Z: "ᶻ",
+ "0": "⁰",
+ "1": "¹",
+ "2": "²",
+ "3": "³",
+ "4": "⁴",
+ "5": "⁵",
+ "6": "⁶",
+ "7": "⁷",
+ "8": "⁸",
+ "9": "⁹",
+ "+": "⁺",
+ "-": "⁻",
+ "=": "⁼",
+ "(": "⁽",
+ ")": "⁾",
};
export const SUBSCRIPT_MAP: Record = {
- a: "ₐ", e: "ₑ", h: "ₕ", i: "ᵢ", j: "ⱼ", k: "ₖ", l: "ₗ", m: "ₘ", n: "ₙ", o: "ₒ",
- p: "ₚ", r: "ᵣ", s: "ₛ", t: "ₜ", u: "ᵤ", v: "ᵥ", x: "ₓ",
- "0": "₀", "1": "₁", "2": "₂", "3": "₃", "4": "₄", "5": "₅", "6": "₆", "7": "₇", "8": "₈", "9": "₉",
- "+": "₊", "-": "₋", "=": "₌", "(": "₍", ")": "₎",
+ a: "ₐ",
+ e: "ₑ",
+ h: "ₕ",
+ i: "ᵢ",
+ j: "ⱼ",
+ k: "ₖ",
+ l: "ₗ",
+ m: "ₘ",
+ n: "ₙ",
+ o: "ₒ",
+ p: "ₚ",
+ r: "ᵣ",
+ s: "ₛ",
+ t: "ₜ",
+ u: "ᵤ",
+ v: "ᵥ",
+ x: "ₓ",
+ "0": "₀",
+ "1": "₁",
+ "2": "₂",
+ "3": "₃",
+ "4": "₄",
+ "5": "₅",
+ "6": "₆",
+ "7": "₇",
+ "8": "₈",
+ "9": "₉",
+ "+": "₊",
+ "-": "₋",
+ "=": "₌",
+ "(": "₍",
+ ")": "₎",
};
export const BOLD_MAP: Record = {
- a: "𝐚", b: "𝐛", c: "𝐜", d: "𝐝", e: "𝐞", f: "𝐟", g: "𝐠", h: "𝐡", i: "𝐢", j: "𝐣",
- k: "𝐤", l: "𝐥", m: "𝐦", n: "𝐧", o: "𝐨", p: "𝐩", q: "𝐪", r: "𝐫", s: "𝐬", t: "𝐭",
- u: "𝐮", v: "𝐯", w: "𝐰", x: "𝐱", y: "𝐲", z: "𝐳",
- A: "𝐀", B: "𝐁", C: "𝐂", D: "𝐃", E: "𝐄", F: "𝐅", G: "𝐆", H: "𝐇", I: "𝐈", J: "𝐉",
- K: "𝐊", L: "𝐋", M: "𝐌", N: "𝐍", O: "𝐎", P: "𝐏", Q: "𝐐", R: "𝐑", S: "𝐒", T: "𝐓",
- U: "𝐔", V: "𝐕", W: "𝐖", X: "𝐗", Y: "𝐘", Z: "𝐙", "0": "𝟎", "1": "𝟏", "2": "𝟐", "3": "𝟑",
- "4": "𝟒", "5": "𝟓", "6": "𝟔", "7": "𝟕", "8": "𝟖", "9": "𝟗",
+ a: "𝐚",
+ b: "𝐛",
+ c: "𝐜",
+ d: "𝐝",
+ e: "𝐞",
+ f: "𝐟",
+ g: "𝐠",
+ h: "𝐡",
+ i: "𝐢",
+ j: "𝐣",
+ k: "𝐤",
+ l: "𝐥",
+ m: "𝐦",
+ n: "𝐧",
+ o: "𝐨",
+ p: "𝐩",
+ q: "𝐪",
+ r: "𝐫",
+ s: "𝐬",
+ t: "𝐭",
+ u: "𝐮",
+ v: "𝐯",
+ w: "𝐰",
+ x: "𝐱",
+ y: "𝐲",
+ z: "𝐳",
+ A: "𝐀",
+ B: "𝐁",
+ C: "𝐂",
+ D: "𝐃",
+ E: "𝐄",
+ F: "𝐅",
+ G: "𝐆",
+ H: "𝐇",
+ I: "𝐈",
+ J: "𝐉",
+ K: "𝐊",
+ L: "𝐋",
+ M: "𝐌",
+ N: "𝐍",
+ O: "𝐎",
+ P: "𝐏",
+ Q: "𝐐",
+ R: "𝐑",
+ S: "𝐒",
+ T: "𝐓",
+ U: "𝐔",
+ V: "𝐕",
+ W: "𝐖",
+ X: "𝐗",
+ Y: "𝐘",
+ Z: "𝐙",
+ "0": "𝟎",
+ "1": "𝟏",
+ "2": "𝟐",
+ "3": "𝟑",
+ "4": "𝟒",
+ "5": "𝟓",
+ "6": "𝟔",
+ "7": "𝟕",
+ "8": "𝟖",
+ "9": "𝟗",
};
export const ITALIC_MAP: Record = {
- a: "𝑎", b: "𝑏", c: "𝑐", d: "𝑑", e: "𝑒", f: "𝑓", g: "𝑔", h: "ℎ", i: "𝑖", j: "𝑗",
- k: "𝑘", l: "𝑙", m: "𝑚", n: "𝑛", o: "𝑜", p: "𝑝", q: "𝑞", r: "𝑟", s: "𝑠", t: "𝑡",
- u: "𝑢", v: "𝑣", w: "𝑤", x: "𝑥", y: "𝑦", z: "𝑧",
- A: "𝐴", B: "𝐵", C: "𝐶", D: "𝐷", E: "𝐸", F: "𝐹", G: "𝐺", H: "𝐻", I: "𝐼", J: "𝐽",
- K: "𝐾", L: "𝐿", M: "𝑀", N: "𝑁", O: "𝑂", P: "𝑃", Q: "𝑄", R: "𝑅", S: "𝑆", T: "𝑇",
- U: "𝑈", V: "𝑉", W: "𝑊", X: "𝑋", Y: "𝑌", Z: "𝑍",
+ a: "𝑎",
+ b: "𝑏",
+ c: "𝑐",
+ d: "𝑑",
+ e: "𝑒",
+ f: "𝑓",
+ g: "𝑔",
+ h: "ℎ",
+ i: "𝑖",
+ j: "𝑗",
+ k: "𝑘",
+ l: "𝑙",
+ m: "𝑚",
+ n: "𝑛",
+ o: "𝑜",
+ p: "𝑝",
+ q: "𝑞",
+ r: "𝑟",
+ s: "𝑠",
+ t: "𝑡",
+ u: "𝑢",
+ v: "𝑣",
+ w: "𝑤",
+ x: "𝑥",
+ y: "𝑦",
+ z: "𝑧",
+ A: "𝐴",
+ B: "𝐵",
+ C: "𝐶",
+ D: "𝐷",
+ E: "𝐸",
+ F: "𝐹",
+ G: "𝐺",
+ H: "𝐻",
+ I: "𝐼",
+ J: "𝐽",
+ K: "𝐾",
+ L: "𝐿",
+ M: "𝑀",
+ N: "𝑁",
+ O: "𝑂",
+ P: "𝑃",
+ Q: "𝑄",
+ R: "𝑅",
+ S: "𝑆",
+ T: "𝑇",
+ U: "𝑈",
+ V: "𝑉",
+ W: "𝑊",
+ X: "𝑋",
+ Y: "𝑌",
+ Z: "𝑍",
};
export const MONO_MAP: Record = {
- a: "𝚊", b: "𝚋", c: "𝚌", d: "𝚍", e: "𝚎", f: "𝚏", g: "𝚐", h: "𝚑", i: "𝚒", j: "𝚓",
- k: "𝚔", l: "𝚕", m: "𝚖", n: "𝚗", o: "𝚘", p: "𝚙", q: "𝚚", r: "𝚛", s: "𝚜", t: "𝚝",
- u: "𝚞", v: "𝚟", w: "𝚠", x: "𝚡", y: "𝚢", z: "𝚣",
- A: "𝙰", B: "𝙱", C: "𝙲", D: "𝙳", E: "𝙴", F: "𝙵", G: "𝙶", H: "𝙷", I: "𝙸", J: "𝙹",
- K: "𝙺", L: "𝙻", M: "𝙼", N: "𝙽", O: "𝙾", P: "𝙿", Q: "𝚀", R: "𝚁", S: "𝚂", T: "𝚃",
- U: "𝚄", V: "𝚅", W: "𝚆", X: "𝚇", Y: "𝚈", Z: "𝚉", "0": "𝟶", "1": "𝟷", "2": "𝟸", "3": "𝟹",
- "4": "𝟺", "5": "𝟻", "6": "𝟼", "7": "𝟽", "8": "𝟾", "9": "𝟿",
+ a: "𝚊",
+ b: "𝚋",
+ c: "𝚌",
+ d: "𝚍",
+ e: "𝚎",
+ f: "𝚏",
+ g: "𝚐",
+ h: "𝚑",
+ i: "𝚒",
+ j: "𝚓",
+ k: "𝚔",
+ l: "𝚕",
+ m: "𝚖",
+ n: "𝚗",
+ o: "𝚘",
+ p: "𝚙",
+ q: "𝚚",
+ r: "𝚛",
+ s: "𝚜",
+ t: "𝚝",
+ u: "𝚞",
+ v: "𝚟",
+ w: "𝚠",
+ x: "𝚡",
+ y: "𝚢",
+ z: "𝚣",
+ A: "𝙰",
+ B: "𝙱",
+ C: "𝙲",
+ D: "𝙳",
+ E: "𝙴",
+ F: "𝙵",
+ G: "𝙶",
+ H: "𝙷",
+ I: "𝙸",
+ J: "𝙹",
+ K: "𝙺",
+ L: "𝙻",
+ M: "𝙼",
+ N: "𝙽",
+ O: "𝙾",
+ P: "𝙿",
+ Q: "𝚀",
+ R: "𝚁",
+ S: "𝚂",
+ T: "𝚃",
+ U: "𝚄",
+ V: "𝚅",
+ W: "𝚆",
+ X: "𝚇",
+ Y: "𝚈",
+ Z: "𝚉",
+ "0": "𝟶",
+ "1": "𝟷",
+ "2": "𝟸",
+ "3": "𝟹",
+ "4": "𝟺",
+ "5": "𝟻",
+ "6": "𝟼",
+ "7": "𝟽",
+ "8": "𝟾",
+ "9": "𝟿",
};
export const SANS_MAP: Record = {
- a: "𝖺", b: "𝖻", c: "𝖼", d: "𝖽", e: "𝖾", f: "𝖿", g: "𝗀", h: "𝗁", i: "𝗂", j: "𝗃",
- k: "𝗄", l: "𝗅", m: "𝗆", n: "𝗇", o: "𝗈", p: "𝗉", q: "𝗊", r: "𝗋", s: "𝗌", t: "𝗍",
- u: "𝗎", v: "𝗏", w: "𝗐", x: "𝗑", y: "𝗒", z: "𝗓",
- A: "𝖠", B: "𝖡", C: "𝖢", D: "𝖣", E: "𝖤", F: "𝖥", G: "𝖦", H: "𝖧", I: "𝖨", J: "𝖩",
- K: "𝖪", L: "𝖫", M: "𝖬", N: "𝖭", O: "𝖮", P: "𝖯", Q: "𝖰", R: "𝖱", S: "𝖲", T: "𝖳",
- U: "𝖴", V: "𝖵", W: "𝖶", X: "𝖷", Y: "𝖸", Z: "𝖹", "0": "𝟢", "1": "𝟣", "2": "𝟤", "3": "𝟥",
- "4": "𝟦", "5": "𝟧", "6": "𝟨", "7": "𝟩", "8": "𝟪", "9": "𝟫",
+ a: "𝖺",
+ b: "𝖻",
+ c: "𝖼",
+ d: "𝖽",
+ e: "𝖾",
+ f: "𝖿",
+ g: "𝗀",
+ h: "𝗁",
+ i: "𝗂",
+ j: "𝗃",
+ k: "𝗄",
+ l: "𝗅",
+ m: "𝗆",
+ n: "𝗇",
+ o: "𝗈",
+ p: "𝗉",
+ q: "𝗊",
+ r: "𝗋",
+ s: "𝗌",
+ t: "𝗍",
+ u: "𝗎",
+ v: "𝗏",
+ w: "𝗐",
+ x: "𝗑",
+ y: "𝗒",
+ z: "𝗓",
+ A: "𝖠",
+ B: "𝖡",
+ C: "𝖢",
+ D: "𝖣",
+ E: "𝖤",
+ F: "𝖥",
+ G: "𝖦",
+ H: "𝖧",
+ I: "𝖨",
+ J: "𝖩",
+ K: "𝖪",
+ L: "𝖫",
+ M: "𝖬",
+ N: "𝖭",
+ O: "𝖮",
+ P: "𝖯",
+ Q: "𝖰",
+ R: "𝖱",
+ S: "𝖲",
+ T: "𝖳",
+ U: "𝖴",
+ V: "𝖵",
+ W: "𝖶",
+ X: "𝖷",
+ Y: "𝖸",
+ Z: "𝖹",
+ "0": "𝟢",
+ "1": "𝟣",
+ "2": "𝟤",
+ "3": "𝟥",
+ "4": "𝟦",
+ "5": "𝟧",
+ "6": "𝟨",
+ "7": "𝟩",
+ "8": "𝟪",
+ "9": "𝟫",
};
export const SCRIPT_MAP: Record = {
- a: "𝒶", b: "𝒷", c: "𝒸", d: "𝒹", e: "𝑒", f: "𝑓", g: "𝑔", h: "𝒽", i: "𝒾", j: "𝒿",
- k: "𝓀", l: "𝓁", m: "𝓂", n: "𝓃", o: "𝑜", p: "𝓅", q: "𝓆", r: "𝓇", s: "𝓈", t: "𝓉",
- u: "𝓊", v: "𝓋", w: "𝓌", x: "𝓍", y: "𝓎", z: "𝓏",
- A: "𝒜", B: "ℬ", C: "𝒞", D: "𝒟", E: "ℰ", F: "ℱ", G: "𝒢", H: "ℋ", I: "ℐ", J: "𝒥",
- K: "𝒦", L: "ℒ", M: "ℳ", N: "𝒩", O: "𝒪", P: "𝒫", Q: "𝒬", R: "ℛ", S: "𝒮", T: "𝒯",
- U: "𝒰", V: "𝒱", W: "𝒲", X: "𝒳", Y: "𝒴", Z: "𝒵",
+ a: "𝒶",
+ b: "𝒷",
+ c: "𝒸",
+ d: "𝒹",
+ e: "𝑒",
+ f: "𝑓",
+ g: "𝑔",
+ h: "𝒽",
+ i: "𝒾",
+ j: "𝒿",
+ k: "𝓀",
+ l: "𝓁",
+ m: "𝓂",
+ n: "𝓃",
+ o: "𝑜",
+ p: "𝓅",
+ q: "𝓆",
+ r: "𝓇",
+ s: "𝓈",
+ t: "𝓉",
+ u: "𝓊",
+ v: "𝓋",
+ w: "𝓌",
+ x: "𝓍",
+ y: "𝓎",
+ z: "𝓏",
+ A: "𝒜",
+ B: "ℬ",
+ C: "𝒞",
+ D: "𝒟",
+ E: "ℰ",
+ F: "ℱ",
+ G: "𝒢",
+ H: "ℋ",
+ I: "ℐ",
+ J: "𝒥",
+ K: "𝒦",
+ L: "ℒ",
+ M: "ℳ",
+ N: "𝒩",
+ O: "𝒪",
+ P: "𝒫",
+ Q: "𝒬",
+ R: "ℛ",
+ S: "𝒮",
+ T: "𝒯",
+ U: "𝒰",
+ V: "𝒱",
+ W: "𝒲",
+ X: "𝒳",
+ Y: "𝒴",
+ Z: "𝒵",
};
export const CURSIVE_MAP: Record = {
- a: "𝓪", b: "𝓫", c: "𝓬", d: "𝓭", e: "𝓮", f: "𝓯", g: "𝓰", h: "𝓱", i: "𝓲", j: "𝓳",
- k: "𝓴", l: "𝓵", m: "𝓶", n: "𝓷", o: "𝓸", p: "𝓹", q: "𝓺", r: "𝓻", s: "𝓼", t: "𝓽",
- u: "𝓾", v: "𝓿", w: "𝔀", x: "𝔁", y: "𝔂", z: "𝔃",
- A: "𝓐", B: "𝓑", C: "𝓒", D: "𝓓", E: "𝓔", F: "𝓕", G: "𝓖", H: "𝓗", I: "𝓘", J: "𝓙",
- K: "𝓚", L: "𝓛", M: "𝓜", N: "𝓝", O: "𝓞", P: "𝓟", Q: "𝓠", R: "𝓡", S: "𝓢", T: "𝓣",
- U: "𝓤", V: "𝓥", W: "𝓦", X: "𝓧", Y: "𝓨", Z: "𝓩",
+ a: "𝓪",
+ b: "𝓫",
+ c: "𝓬",
+ d: "𝓭",
+ e: "𝓮",
+ f: "𝓯",
+ g: "𝓰",
+ h: "𝓱",
+ i: "𝓲",
+ j: "𝓳",
+ k: "𝓴",
+ l: "𝓵",
+ m: "𝓶",
+ n: "𝓷",
+ o: "𝓸",
+ p: "𝓹",
+ q: "𝓺",
+ r: "𝓻",
+ s: "𝓼",
+ t: "𝓽",
+ u: "𝓾",
+ v: "𝓿",
+ w: "𝔀",
+ x: "𝔁",
+ y: "𝔂",
+ z: "𝔃",
+ A: "𝓐",
+ B: "𝓑",
+ C: "𝓒",
+ D: "𝓓",
+ E: "𝓔",
+ F: "𝓕",
+ G: "𝓖",
+ H: "𝓗",
+ I: "𝓘",
+ J: "𝓙",
+ K: "𝓚",
+ L: "𝓛",
+ M: "𝓜",
+ N: "𝓝",
+ O: "𝓞",
+ P: "𝓟",
+ Q: "𝓠",
+ R: "𝓡",
+ S: "𝓢",
+ T: "𝓣",
+ U: "𝓤",
+ V: "𝓥",
+ W: "𝓦",
+ X: "𝓧",
+ Y: "𝓨",
+ Z: "𝓩",
};
export const MORSE: Record = {
- a: ".-", b: "-...", c: "-.-.", d: "-..", e: ".", f: "..-.", g: "--.", h: "....",
- i: "..", j: ".---", k: "-.-", l: ".-..", m: "--", n: "-.", o: "---", p: ".--.",
- q: "--.-", r: ".-.", s: "...", t: "-", u: "..-", v: "...-", w: ".--", x: "-..-",
- y: "-.--", z: "--..", "0": "-----", "1": ".----", "2": "..---", "3": "...--",
- "4": "....-", "5": ".....", "6": "-....", "7": "--...", "8": "---..", "9": "----.",
- ".": ".-.-.-", ",": "--..--", "?": "..--..", "'": ".----.", "!": "-.-.--", "/": "-..-.",
- "(": "-.--.", ")": "-.--.-", "&": ".-...", ":": "---...", ";": "-.-.-.", "=": "-...-",
- "+": ".-.-.", "-": "-....-", "_": "..--.-", '"': ".-..-.", "$": "...-..-", "@": ".--.-.",
+ a: ".-",
+ b: "-...",
+ c: "-.-.",
+ d: "-..",
+ e: ".",
+ f: "..-.",
+ g: "--.",
+ h: "....",
+ i: "..",
+ j: ".---",
+ k: "-.-",
+ l: ".-..",
+ m: "--",
+ n: "-.",
+ o: "---",
+ p: ".--.",
+ q: "--.-",
+ r: ".-.",
+ s: "...",
+ t: "-",
+ u: "..-",
+ v: "...-",
+ w: ".--",
+ x: "-..-",
+ y: "-.--",
+ z: "--..",
+ "0": "-----",
+ "1": ".----",
+ "2": "..---",
+ "3": "...--",
+ "4": "....-",
+ "5": ".....",
+ "6": "-....",
+ "7": "--...",
+ "8": "---..",
+ "9": "----.",
+ ".": ".-.-.-",
+ ",": "--..--",
+ "?": "..--..",
+ "'": ".----.",
+ "!": "-.-.--",
+ "/": "-..-.",
+ "(": "-.--.",
+ ")": "-.--.-",
+ "&": ".-...",
+ ":": "---...",
+ ";": "-.-.-.",
+ "=": "-...-",
+ "+": ".-.-.",
+ "-": "-....-",
+ _: "..--.-",
+ '"': ".-..-.",
+ $: "...-..-",
+ "@": ".--.-.",
};
-const MORSE_REV = new Map(Object.entries(MORSE).map(([k, v]) => [v, k]));
+const MORSE_REV = new Map(
+ Object.entries(MORSE).map(([k, v]) => [v, k]),
+);
export function textToMorse(text: string): string {
return text
@@ -175,21 +613,87 @@ export function morseToText(morse: string): string {
}
export const ZALGO_MARKS = [
- "\u0300", "\u0301", "\u0302", "\u0303", "\u0304", "\u0305", "\u0306", "\u0307",
- "\u0308", "\u0309", "\u030a", "\u030b", "\u030c", "\u030d", "\u030e", "\u0310",
- "\u0311", "\u0312", "\u0313", "\u0314", "\u0315", "\u031a", "\u031b", "\u0333",
- "\u0334", "\u0335", "\u0336", "\u0337", "\u0338",
+ "\u0300",
+ "\u0301",
+ "\u0302",
+ "\u0303",
+ "\u0304",
+ "\u0305",
+ "\u0306",
+ "\u0307",
+ "\u0308",
+ "\u0309",
+ "\u030a",
+ "\u030b",
+ "\u030c",
+ "\u030d",
+ "\u030e",
+ "\u0310",
+ "\u0311",
+ "\u0312",
+ "\u0313",
+ "\u0314",
+ "\u0315",
+ "\u031a",
+ "\u031b",
+ "\u0333",
+ "\u0334",
+ "\u0335",
+ "\u0336",
+ "\u0337",
+ "\u0338",
];
export const NUMBER_WORDS: Record = {
- zero: "0", one: "1", two: "2", three: "3", four: "4", five: "5", six: "6",
- seven: "7", eight: "8", nine: "9", ten: "10",
+ zero: "0",
+ one: "1",
+ two: "2",
+ three: "3",
+ four: "4",
+ five: "5",
+ six: "6",
+ seven: "7",
+ eight: "8",
+ nine: "9",
+ ten: "10",
};
/** Convert a non-negative integer to English words (0–999). */
export function toWords(n: number): string {
- const ones = ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen"];
- const tens = ["", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"];
+ const ones = [
+ "zero",
+ "one",
+ "two",
+ "three",
+ "four",
+ "five",
+ "six",
+ "seven",
+ "eight",
+ "nine",
+ "ten",
+ "eleven",
+ "twelve",
+ "thirteen",
+ "fourteen",
+ "fifteen",
+ "sixteen",
+ "seventeen",
+ "eighteen",
+ "nineteen",
+ ];
+ const tens = [
+ "",
+ "",
+ "twenty",
+ "thirty",
+ "forty",
+ "fifty",
+ "sixty",
+ "seventy",
+ "eighty",
+ "ninety",
+ ];
if (n < 0) return "minus " + toWords(-n);
if (n < 20) return ones[n];
if (n < 100) {
@@ -260,6 +764,17 @@ export function hashCode(text: string): number {
}
export const ROMAN_NUMERALS: [number, string][] = [
- [1000, "M"], [900, "CM"], [500, "D"], [400, "CD"], [100, "C"], [90, "XC"],
- [50, "L"], [40, "XL"], [10, "X"], [9, "IX"], [5, "V"], [4, "IV"], [1, "I"],
-];
\ No newline at end of file
+ [1000, "M"],
+ [900, "CM"],
+ [500, "D"],
+ [400, "CD"],
+ [100, "C"],
+ [90, "XC"],
+ [50, "L"],
+ [40, "XL"],
+ [10, "X"],
+ [9, "IX"],
+ [5, "V"],
+ [4, "IV"],
+ [1, "I"],
+];
diff --git a/artifacts/personal-tool-console/src/lib/tools/text/index.ts b/artifacts/personal-tool-console/src/lib/tools/text/index.ts
index ae5ecf4..001b952 100644
--- a/artifacts/personal-tool-console/src/lib/tools/text/index.ts
+++ b/artifacts/personal-tool-console/src/lib/tools/text/index.ts
@@ -5,7 +5,11 @@ import { texts } from "./existing";
export type { TextToolSpec, TextToolGroup, TextOpt, TextOpts } from "./types";
-export const TEXT_GROUPS: TextToolGroup[] = [...TEXT_PART1, ...TEXT_PART2, ...texts];
+export const TEXT_GROUPS: TextToolGroup[] = [
+ ...TEXT_PART1,
+ ...TEXT_PART2,
+ ...texts,
+];
export const TEXT_TOOLS = TEXT_GROUPS.flatMap((g) => g.tools);
@@ -15,4 +19,4 @@ export function getTextTool(slug: string) {
export function getTextGroupIds() {
return TEXT_GROUPS.map((g) => g.id);
-}
\ No newline at end of file
+}
diff --git a/artifacts/personal-tool-console/src/lib/tools/text/part1.ts b/artifacts/personal-tool-console/src/lib/tools/text/part1.ts
index b513582..d6bafd6 100644
--- a/artifacts/personal-tool-console/src/lib/tools/text/part1.ts
+++ b/artifacts/personal-tool-console/src/lib/tools/text/part1.ts
@@ -27,7 +27,21 @@ export const TEXT_PART1: TextToolGroup[] = [
name: "Split Text",
description: "Break text into items by a chosen separator",
options: [
- { kind: "select", key: "separator", label: "Separator", options: ["Character", "Word", "Line", "Space", "Comma", "Custom", "CustomRegex"], default: "Character" },
+ {
+ kind: "select",
+ key: "separator",
+ label: "Separator",
+ options: [
+ "Character",
+ "Word",
+ "Line",
+ "Space",
+ "Comma",
+ "Custom",
+ "CustomRegex",
+ ],
+ default: "Character",
+ },
{ kind: "text", key: "custom", label: "Custom", default: "" },
],
run: (input, o) => {
@@ -41,7 +55,8 @@ export const TEXT_PART1: TextToolGroup[] = [
else if (sep === "Space") parts = input.split(" ");
else if (sep === "Comma") parts = input.split(",");
else if (sep === "Custom") parts = input.split(custom);
- else if (sep === "CustomRegex") parts = input.split(new RegExp(custom));
+ else if (sep === "CustomRegex")
+ parts = input.split(new RegExp(custom));
return parts.join("\n");
},
},
@@ -50,12 +65,21 @@ export const TEXT_PART1: TextToolGroup[] = [
name: "Join Text",
description: "Join lines or words with a separator",
options: [
- { kind: "select", key: "parts", label: "Parts", options: ["Lines", "Words"], default: "Lines" },
+ {
+ kind: "select",
+ key: "parts",
+ label: "Parts",
+ options: ["Lines", "Words"],
+ default: "Lines",
+ },
{ kind: "text", key: "separator", label: "Separator", default: ", " },
],
run: (input, o) => {
const separator = String(o.separator ?? ", ");
- const parts = String(o.parts ?? "Lines") === "Words" ? tokens(input) : toLines(input);
+ const parts =
+ String(o.parts ?? "Lines") === "Words"
+ ? tokens(input)
+ : toLines(input);
return parts.join(separator);
},
},
@@ -63,7 +87,9 @@ export const TEXT_PART1: TextToolGroup[] = [
slug: "repeat-text",
name: "Repeat Text",
description: "Repeat the whole text a number of times",
- options: [{ kind: "number", key: "count", label: "Count", default: 3, min: 1 }],
+ options: [
+ { kind: "number", key: "count", label: "Count", default: 3, min: 1 },
+ ],
run: (input, o) => {
if (input === "") return "";
const count = Math.max(1, Number(o.count ?? 3) || 1);
@@ -129,7 +155,9 @@ export const TEXT_PART1: TextToolGroup[] = [
run: (input, o) => {
const width = Math.max(0, Number(o.width ?? 20) || 0);
const fill = String(o.fill ?? " ");
- return toLines(input).map((l) => l.padStart(width, fill)).join("\n");
+ return toLines(input)
+ .map((l) => l.padStart(width, fill))
+ .join("\n");
},
},
{
@@ -143,14 +171,19 @@ export const TEXT_PART1: TextToolGroup[] = [
run: (input, o) => {
const width = Math.max(0, Number(o.width ?? 20) || 0);
const fill = String(o.fill ?? " ");
- return toLines(input).map((l) => l.padEnd(width, fill)).join("\n");
+ return toLines(input)
+ .map((l) => l.padEnd(width, fill))
+ .join("\n");
},
},
{
slug: "left-align-text",
name: "Left-align Text",
description: "Strip leading whitespace from each line",
- run: (input) => toLines(input).map((l) => l.replace(/^\s+/, "")).join("\n"),
+ run: (input) =>
+ toLines(input)
+ .map((l) => l.replace(/^\s+/, ""))
+ .join("\n"),
},
{
slug: "right-align-text",
@@ -188,15 +221,22 @@ export const TEXT_PART1: TextToolGroup[] = [
{ kind: "toggle", key: "tabs", label: "Use tabs", default: false },
],
run: (input, o) => {
- const pad = o.tabs === true ? "\t" : " ".repeat(Math.max(0, Number(o.spaces ?? 4) || 0));
- return toLines(input).map((l) => (l === "" ? l : pad + l)).join("\n");
+ const pad =
+ o.tabs === true
+ ? "\t"
+ : " ".repeat(Math.max(0, Number(o.spaces ?? 4) || 0));
+ return toLines(input)
+ .map((l) => (l === "" ? l : pad + l))
+ .join("\n");
},
},
{
slug: "unindent-text",
name: "Unindent Text",
description: "Remove the common leading indentation from lines",
- options: [{ kind: "number", key: "spaces", label: "Spaces", default: 0 }],
+ options: [
+ { kind: "number", key: "spaces", label: "Spaces", default: 0 },
+ ],
run: (input, o) => {
const lines = toLines(input);
let amount = Number(o.spaces ?? 0) || 0;
@@ -242,7 +282,9 @@ export const TEXT_PART1: TextToolGroup[] = [
slug: "wrap-words",
name: "Wrap Words",
description: "Hard wrap the text at a column width",
- options: [{ kind: "number", key: "width", label: "Width", default: 40 }],
+ options: [
+ { kind: "number", key: "width", label: "Width", default: 40 },
+ ],
run: (input, o) => {
const width = Math.max(1, Number(o.width ?? 40) || 1);
const out: string[] = [];
@@ -295,7 +337,9 @@ export const TEXT_PART1: TextToolGroup[] = [
const wordTokens = parts.filter((p) => /^[\p{L}\p{N}'-]+$/u.test(p));
wordTokens.reverse();
let i = 0;
- return parts.map((p) => (/^[\p{L}\p{N}'-]+$/u.test(p) ? wordTokens[i++] : p)).join("");
+ return parts
+ .map((p) => (/^[\p{L}\p{N}'-]+$/u.test(p) ? wordTokens[i++] : p))
+ .join("");
},
},
{
@@ -314,7 +358,8 @@ export const TEXT_PART1: TextToolGroup[] = [
slug: "swap-letters-in-words",
name: "Swap Letters in Words",
description: "Reverse the letters inside every word",
- run: (input) => input.replace(/[\p{L}\p{N}'-]+/gu, (w) => [...w].reverse().join("")),
+ run: (input) =>
+ input.replace(/[\p{L}\p{N}'-]+/gu, (w) => [...w].reverse().join("")),
},
{
slug: "swap-words",
@@ -332,9 +377,11 @@ export const TEXT_PART1: TextToolGroup[] = [
[...b]
.map((ch, k) => {
const ref = m[k] ?? m[m.length - 1];
- return ref && ref === ref.toUpperCase() ? ch.toUpperCase() : ch.toLowerCase();
+ return ref && ref === ref.toUpperCase()
+ ? ch.toUpperCase()
+ : ch.toLowerCase();
})
- .join("")
+ .join(""),
);
},
},
@@ -345,7 +392,9 @@ export const TEXT_PART1: TextToolGroup[] = [
options: [{ kind: "number", key: "times", label: "Times", default: 2 }],
run: (input, o) => {
const times = Math.max(1, Number(o.times ?? 2) || 1);
- return input.replace(/\S+/g, (w) => Array.from({ length: times }, () => w).join(" "));
+ return input.replace(/\S+/g, (w) =>
+ Array.from({ length: times }, () => w).join(" "),
+ );
},
},
{
@@ -363,7 +412,9 @@ export const TEXT_PART1: TextToolGroup[] = [
.map((s) => s.toLowerCase())
.filter(Boolean);
if (list.length) {
- return words(input).filter((w) => !list.includes(w.toLowerCase())).join(" ");
+ return words(input)
+ .filter((w) => !list.includes(w.toLowerCase()))
+ .join(" ");
}
const ws = words(input);
const from = Math.max(0, Number(o.from ?? 0) || 0);
@@ -379,14 +430,18 @@ export const TEXT_PART1: TextToolGroup[] = [
options: [{ kind: "number", key: "times", label: "Times", default: 2 }],
run: (input, o) => {
const times = Math.max(1, Number(o.times ?? 2) || 1);
- return sentences(input).flatMap((s) => Array.from({ length: times }, () => s)).join(" ");
+ return sentences(input)
+ .flatMap((s) => Array.from({ length: times }, () => s))
+ .join(" ");
},
},
{
slug: "remove-sentences",
name: "Remove Sentences",
description: "Remove sentences by their one-based positions",
- options: [{ kind: "text", key: "indexes", label: "Indexes", default: "" }],
+ options: [
+ { kind: "text", key: "indexes", label: "Indexes", default: "" },
+ ],
run: (input, o) => {
const idx = String(o.indexes ?? "")
.split(",")
@@ -404,13 +459,21 @@ export const TEXT_PART1: TextToolGroup[] = [
options: [
{ kind: "text", key: "find", label: "Find", default: "foo" },
{ kind: "text", key: "replace", label: "Replace", default: "bar" },
- { kind: "toggle", key: "caseInsensitive", label: "Case-insensitive", default: true },
+ {
+ kind: "toggle",
+ key: "caseInsensitive",
+ label: "Case-insensitive",
+ default: true,
+ },
],
run: (input, o) => {
const find = String(o.find ?? "foo");
const replace = String(o.replace ?? "bar");
if (!find) return input;
- return input.replace(new RegExp(esc(find), o.caseInsensitive === true ? "gi" : "g"), replace);
+ return input.replace(
+ new RegExp(esc(find), o.caseInsensitive === true ? "gi" : "g"),
+ replace,
+ );
},
},
],
@@ -428,12 +491,21 @@ export const TEXT_PART1: TextToolGroup[] = [
run: (input, o) => {
if (input === "") return "";
const count = Math.max(1, Number(o.count ?? 3) || 1);
- const pool = ["um", "actually", "basically", "literally", "very", "really", "sort of"];
+ const pool = [
+ "um",
+ "actually",
+ "basically",
+ "literally",
+ "very",
+ "really",
+ "sort of",
+ ];
const toks = input.match(/\S+|\s+/g) ?? [];
for (let i = 0; i < count; i++) {
const pos = randInt(0, toks.length);
const before = pos > 0 && toks[pos - 1]?.trim() !== "" ? " " : "";
- const after = pos < toks.length && toks[pos]?.trim() !== "" ? " " : "";
+ const after =
+ pos < toks.length && toks[pos]?.trim() !== "" ? " " : "";
toks.splice(pos, 0, before + pick(pool) + after);
}
return toks.join("");
@@ -448,12 +520,17 @@ export const TEXT_PART1: TextToolGroup[] = [
if (input === "") return "";
const count = Math.max(1, Number(o.count ?? 1) || 1);
const parts = input.match(/\S+|\s+/g) ?? [];
- const wordIdx = parts.map((p, i) => (p.trim() ? i : -1)).filter((i) => i >= 0);
+ const wordIdx = parts
+ .map((p, i) => (p.trim() ? i : -1))
+ .filter((i) => i >= 0);
for (let i = 0; i < count && wordIdx.length; i++) {
const ti = pick(wordIdx);
const w = parts[ti];
const at = randInt(0, w.length);
- parts[ti] = w.slice(0, at) + ALPHABET[randInt(0, ALPHABET.length - 1)] + w.slice(at);
+ parts[ti] =
+ w.slice(0, at) +
+ ALPHABET[randInt(0, ALPHABET.length - 1)] +
+ w.slice(at);
}
return parts.join("");
},
@@ -464,7 +541,12 @@ export const TEXT_PART1: TextToolGroup[] = [
description: "Randomly replace characters or swap adjacent ones",
options: [
{ kind: "number", key: "rate", label: "Rate (%)", default: 5 },
- { kind: "toggle", key: "swap", label: "Swap adjacent letters", default: false },
+ {
+ kind: "toggle",
+ key: "swap",
+ label: "Swap adjacent letters",
+ default: false,
+ },
],
run: (input, o) => {
const rate = Math.max(0, Number(o.rate ?? 5) || 0);
@@ -487,17 +569,46 @@ export const TEXT_PART1: TextToolGroup[] = [
slug: "generate-fake-text",
name: "Generate Fake Text",
description: "Produce plausible dummy prose paragraphs",
- options: [{ kind: "number", key: "paragraphs", label: "Paragraphs", default: 3 }],
+ options: [
+ {
+ kind: "number",
+ key: "paragraphs",
+ label: "Paragraphs",
+ default: 3,
+ },
+ ],
run: (input, o) => {
const n = Math.max(1, Number(o.paragraphs ?? 3) || 1);
- const pool = ["the", "quick", "brown", "fox", "jumps", "over", "lazy", "dog", "stream", "light", "data", "cloud", "value", "system", "user", "table", "signal", "token", "query"];
+ const pool = [
+ "the",
+ "quick",
+ "brown",
+ "fox",
+ "jumps",
+ "over",
+ "lazy",
+ "dog",
+ "stream",
+ "light",
+ "data",
+ "cloud",
+ "value",
+ "system",
+ "user",
+ "table",
+ "signal",
+ "token",
+ "query",
+ ];
const cap = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
const sentence = () =>
- Array.from({ length: randInt(7, 12) }, () => pick(pool)).join(" ") + ".";
+ Array.from({ length: randInt(7, 12) }, () => pick(pool)).join(" ") +
+ ".";
const out: string[] = [];
for (let i = 0; i < n; i++) {
const sents: string[] = [];
- for (let j = 0, m = randInt(3, 6); j < m; j++) sents.push(cap(sentence()));
+ for (let j = 0, m = randInt(3, 6); j < m; j++)
+ sents.push(cap(sentence()));
out.push(sents.join(" "));
}
return out.join("\n\n");
@@ -508,9 +619,23 @@ export const TEXT_PART1: TextToolGroup[] = [
name: "Unfake Text",
description: "Strip common lorem ipsum filler words",
run: (input) => {
- const filler = ["lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit"];
+ const filler = [
+ "lorem",
+ "ipsum",
+ "dolor",
+ "sit",
+ "amet",
+ "consectetur",
+ "adipiscing",
+ "elit",
+ ];
return tokens(input)
- .filter((w) => !filler.includes(w.replace(/[^\p{L}\p{N}]/gu, "").toLowerCase()))
+ .filter(
+ (w) =>
+ !filler.includes(
+ w.replace(/[^\p{L}\p{N}]/gu, "").toLowerCase(),
+ ),
+ )
.join(" ");
},
},
@@ -519,13 +644,28 @@ export const TEXT_PART1: TextToolGroup[] = [
name: "Check If Text Is Fake",
description: "Heuristically score how dummy the text looks",
run: (input) => {
- const filler = ["lorem", "ipsum", "blah", "test", "foo", "bar", "asd", "qwerty", "placeholder", "sample"];
+ const filler = [
+ "lorem",
+ "ipsum",
+ "blah",
+ "test",
+ "foo",
+ "bar",
+ "asd",
+ "qwerty",
+ "placeholder",
+ "sample",
+ ];
const counts = new Map();
for (const w of tokens(input)) {
const base = w.replace(/[^\p{L}\p{N}]/gu, "").toLowerCase();
- if (filler.includes(base)) counts.set(base, (counts.get(base) ?? 0) + 1);
+ if (filler.includes(base))
+ counts.set(base, (counts.get(base) ?? 0) + 1);
}
- let score = Math.min(40, [...counts.values()].reduce((a, b) => a + b, 0) * 10);
+ let score = Math.min(
+ 40,
+ [...counts.values()].reduce((a, b) => a + b, 0) * 10,
+ );
const wds = words(input);
const avgLen = wds.length ? wds.join("").length / wds.length : 0;
if (avgLen < 3.5) score += 20;
@@ -553,10 +693,16 @@ export const TEXT_PART1: TextToolGroup[] = [
const byTok = new Map();
parts.forEach((p, ti) => {
if (!p.trim()) return;
- byTok.set(ti, [...p].map((c, ci) => (/\p{L}/u.test(c) ? ci : -1)).filter((i) => i >= 0));
+ byTok.set(
+ ti,
+ [...p]
+ .map((c, ci) => (/\p{L}/u.test(c) ? ci : -1))
+ .filter((i) => i >= 0),
+ );
});
const all: Array<[number, number]> = [];
- for (const [ti, arr] of byTok) for (const ci of arr) all.push([ti, ci]);
+ for (const [ti, arr] of byTok)
+ for (const ci of arr) all.push([ti, ci]);
const chosen = new Map