diff --git a/src/app/globals.css b/src/app/globals.css
index 82aa0d5..41c195a 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -1,30 +1,98 @@
@import "tailwindcss";
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ border-radius: 0 !important; /* Square everything out */
+}
+
:root {
- --background: #000000;
+ --background: #0b0b0b;
--foreground: #e5e5e5;
- --accent: #60a5fa;
- --accent-hover: #3b82f6;
- --accent-dim: #1e3a5f;
- --surface: #0a0a0a;
- --surface-hover: #111111;
- --border: #1a1a1a;
- --border-hover: #2a2a2a;
- --muted: #525252;
+ --accent: #ffffff;
+ --accent-hover: #e5e5e5;
+ --accent-dim: #262626;
+ --surface: #111111;
+ --surface-hover: #161616;
+ --border: #1f1f1f;
+ --border-hover: #383838;
+ --muted: #666666;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-accent: var(--accent);
- --font-sans: var(--font-geist-sans);
- --font-mono: var(--font-geist-mono);
+ --font-mono: var(--font-mono);
}
body {
- background: #000000;
- color: var(--foreground);
- font-family: var(--font-sans), Arial, Helvetica, sans-serif;
+ background: #0b0b0b;
+ color: #e5e5e5;
+ font-family: var(--font-mono), 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+ min-height: 100vh;
+ -webkit-font-smoothing: antialiased;
+}
+
+/* Minimal Monospace Form Controls */
+input, textarea, select {
+ font-family: inherit;
+ border-radius: 0 !important;
+}
+
+input[type="text"],
+input[type="number"],
+input[type="search"],
+input[type="password"],
+input[type="email"],
+input[type="url"],
+textarea,
+select {
+ background-color: #0d0d0d;
+ border: 1px solid #222222;
+ color: #e5e5e5;
+ outline: none;
+ transition: border-color 0.15s ease;
+}
+
+input[type="text"]:focus,
+input[type="number"]:focus,
+input[type="search"]:focus,
+input[type="password"]:focus,
+input[type="email"]:focus,
+input[type="url"]:focus,
+textarea:focus,
+select:focus {
+ border-color: #555555;
+}
+
+/* Minimal Square Buttons */
+button {
+ border-radius: 0 !important;
+ font-family: inherit;
+ user-select: none;
+}
+
+/* Scrollbars */
+::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+}
+::-webkit-scrollbar-track {
+ background: #0b0b0b;
+}
+::-webkit-scrollbar-thumb {
+ background: #222222;
+}
+::-webkit-scrollbar-thumb:hover {
+ background: #383838;
+}
+
+/* Selection */
+::selection {
+ background: #282828;
+ color: #ffffff;
}
/* Dot pattern background */
@@ -36,10 +104,62 @@ body {
content: "";
position: fixed;
inset: 0;
- background-image: radial-gradient(circle, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
+ background-image: radial-gradient(circle, rgba(255, 255, 255, 0.035) 1px, transparent 1px);
background-size: 20px 20px;
pointer-events: none;
- mask-image: radial-gradient(ellipse 80% 70% at 50% 50%, black 40%, transparent 100%);
- -webkit-mask-image: radial-gradient(ellipse 80% 70% at 50% 50%, black 40%, transparent 100%);
+ mask-image: radial-gradient(ellipse 85% 75% at 50% 50%, black 40%, transparent 100%);
+ -webkit-mask-image: radial-gradient(ellipse 85% 75% at 50% 50%, black 40%, transparent 100%);
z-index: 0;
}
+
+/* Minimal Monochrome Overrides for Kat Aesthetic */
+.bg-blue-600 {
+ background-color: #e5e5e5 !important;
+ color: #0b0b0b !important;
+ border-color: #ffffff !important;
+}
+.bg-blue-600:hover,
+.hover\:bg-blue-500:hover,
+.hover\:bg-blue-700:hover {
+ background-color: #ffffff !important;
+ color: #000000 !important;
+}
+.active\:bg-blue-800:active {
+ background-color: #cccccc !important;
+ color: #000000 !important;
+}
+.bg-blue-700,
+.bg-blue-800,
+.bg-blue-900,
+.bg-blue-900\/20,
+.bg-blue-900\/60,
+.bg-blue-950\/20 {
+ background-color: #222222 !important;
+}
+.text-blue-100,
+.text-blue-200,
+.text-blue-400,
+.text-blue-500,
+.text-blue-600 {
+ color: #e5e5e5 !important;
+}
+.hover\:text-blue-400:hover {
+ color: #ffffff !important;
+}
+.border-blue-500,
+.border-blue-500\/60,
+.border-blue-600,
+.border-blue-700,
+.border-blue-900,
+.hover\:border-blue-900:hover {
+ border-color: #383838 !important;
+}
+.ring-blue-500\/30,
+.ring-blue-500\/40,
+.ring-blue-900,
+.ring-blue-900\/20 {
+ --tw-ring-color: #444444 !important;
+}
+.accent-blue-600 {
+ accent-color: #e5e5e5 !important;
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 73e1938..27ec7ac 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,16 +1,11 @@
import type { Metadata } from "next";
-import { Geist, Geist_Mono } from "next/font/google";
+import { JetBrains_Mono } from "next/font/google";
import "./globals.css";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
-const geistSans = Geist({
- variable: "--font-geist-sans",
- subsets: ["latin"],
-});
-
-const geistMono = Geist_Mono({
- variable: "--font-geist-mono",
+const jetbrainsMono = JetBrains_Mono({
+ variable: "--font-mono",
subsets: ["latin"],
});
@@ -62,7 +57,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
return (
-
+
{children}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 240460c..0b69801 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -8,7 +8,7 @@ export default function Home() {
const featuredTools = tools.filter((t) => t.featured);
return (
-
+
{/* Left Side Rail (Ultra-wide) */}
@@ -18,29 +18,32 @@ export default function Home() {
{/* Main Content */}
{/* Header intro */}
-
-
- Local execution • No uploads
+
+
+
+ client-side execution
+ •
+ zero server uploads
-
- Browser-based file and developer utilities
+
+ browser-based file & developer utilities
-
- Utilities for image resizing, metadata stripping, PDF manipulation, and standard developer conversions. All tasks execute locally in your browser.
+
+ Fast, privacy-focused utilities for image resizing, EXIF metadata scrubbing, PDF manipulation, hashing, formatting, and standard developer conversions. All processing runs entirely in client memory.
{/* Quick access row */}
-
+
{featuredTools.map((t) => (
-
+
{t.name}
-
+
{t.description}
@@ -51,12 +54,12 @@ export default function Home() {
{/* Categorized tools */}
{Object.entries(toolsByCategory).map(([category, categoryTools]) => (
-
-
-
+
+
{category}
-
({categoryTools.length})
+
[{categoryTools.length}]
+
{categoryTools.map((tool) => (
diff --git a/src/app/privacy/page.tsx b/src/app/privacy/page.tsx
index 81fdc9f..fc5b286 100644
--- a/src/app/privacy/page.tsx
+++ b/src/app/privacy/page.tsx
@@ -8,7 +8,7 @@ export default function PrivacyPage() {
description="Information on data processing, third-party advertising cookies, and local execution."
showSideAds={false}
>
-
+
1. Client-Side Data Handling
@@ -34,7 +34,7 @@ export default function PrivacyPage() {
href="https://www.google.com/settings/ads"
target="_blank"
rel="noopener noreferrer"
- className="text-blue-400 hover:underline"
+ className="text-gray-300 hover:text-white underline"
>
Google Ads Settings
{" "}
@@ -43,7 +43,7 @@ export default function PrivacyPage() {
href="https://www.aboutads.info"
target="_blank"
rel="noopener noreferrer"
- className="text-blue-400 hover:underline"
+ className="text-gray-300 hover:text-white underline"
>
aboutads.info
@@ -67,7 +67,7 @@ export default function PrivacyPage() {
href="https://github.com/oiupoyt/kattoolbox"
target="_blank"
rel="noopener noreferrer"
- className="text-blue-400 hover:underline"
+ className="text-gray-300 hover:text-white underline"
>
github.com/oiupoyt/kattoolbox
@@ -76,7 +76,7 @@ export default function PrivacyPage() {
-
+
← Return to utilities index
diff --git a/src/app/tools/case-converter/page.tsx b/src/app/tools/case-converter/page.tsx
index 725e6d6..6adde98 100644
--- a/src/app/tools/case-converter/page.tsx
+++ b/src/app/tools/case-converter/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useState, useId, useCallback, useEffect } from "react";
+import { useState, useId, useMemo } from "react";
import ToolLayout from "@/components/ToolLayout";
// Helper to extract words from various formats (camelCase, PascalCase, snake_case, kebab-case, normal text)
@@ -219,29 +219,17 @@ export default function CaseConverterPage() {
const outputId = useId();
const [input, setInput] = useState(SAMPLE_TEXT);
- const [output, setOutput] = useState("");
const [activeCaseKey, setActiveCaseKey] = useState
("titleCase");
const [copied, setCopied] = useState(false);
const [previewCopiedKey, setPreviewCopiedKey] = useState(null);
- // Auto-convert when input or activeCaseKey changes
- const applyConversion = useCallback(
- (key: string, textToConvert: string) => {
- const converter = CONVERTERS[key];
- if (converter) {
- setOutput(converter.fn(textToConvert));
- }
- },
- []
- );
-
- useEffect(() => {
- applyConversion(activeCaseKey, input);
- }, [input, activeCaseKey, applyConversion]);
+ const output = useMemo(() => {
+ const converter = CONVERTERS[activeCaseKey];
+ return converter ? converter.fn(input) : "";
+ }, [activeCaseKey, input]);
const handleCaseButtonClick = (key: string) => {
setActiveCaseKey(key);
- applyConversion(key, input);
};
const handleCopy = async () => {
@@ -266,14 +254,11 @@ export default function CaseConverterPage() {
};
const handleSwap = () => {
- const temp = output;
- setInput(temp);
- setOutput(input);
+ setInput(output);
};
const handleClear = () => {
setInput("");
- setOutput("");
};
const handleLoadSample = () => {
diff --git a/src/app/tools/color-palette-extractor/page.tsx b/src/app/tools/color-palette-extractor/page.tsx
index f5c97a5..fc48bad 100644
--- a/src/app/tools/color-palette-extractor/page.tsx
+++ b/src/app/tools/color-palette-extractor/page.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable @next/next/no-img-element */
"use client";
import { useState, useRef, useCallback } from "react";
diff --git a/src/app/tools/cron-parser/page.tsx b/src/app/tools/cron-parser/page.tsx
index 41b0e13..fcf5e5c 100644
--- a/src/app/tools/cron-parser/page.tsx
+++ b/src/app/tools/cron-parser/page.tsx
@@ -476,7 +476,6 @@ function computeNextRuns(fields: ParsedField[], count: number = 10, timezone: "l
while (results.length < count && steps < maxSteps) {
steps++;
- const getYear = timezone === "utc" ? current.getUTCFullYear() : current.getFullYear();
const getMonth = (timezone === "utc" ? current.getUTCMonth() : current.getMonth()) + 1;
const getDate = timezone === "utc" ? current.getUTCDate() : current.getDate();
const getDay = timezone === "utc" ? current.getUTCDay() : current.getDay();
diff --git a/src/app/tools/discord-emoji-resizer/page.tsx b/src/app/tools/discord-emoji-resizer/page.tsx
index 43c59ce..a919441 100644
--- a/src/app/tools/discord-emoji-resizer/page.tsx
+++ b/src/app/tools/discord-emoji-resizer/page.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable @next/next/no-img-element */
"use client";
import { useState, useRef, useCallback } from "react";
@@ -49,7 +50,6 @@ function formatBytes(bytes: number): string {
export default function DiscordEmojiResizerPage() {
const [file, setFile] = useState(null);
- const [originalUrl, setOriginalUrl] = useState(null);
const [originalDimensions, setOriginalDimensions] = useState<{ w: number; h: number } | null>(null);
const [resultBlob, setResultBlob] = useState(null);
const [resultUrl, setResultUrl] = useState(null);
@@ -169,12 +169,12 @@ export default function DiscordEmojiResizerPage() {
}
setFile(selected);
const url = URL.createObjectURL(selected);
- setOriginalUrl(url);
const img = new Image();
img.src = url;
img.onload = () => {
setOriginalDimensions({ w: img.naturalWidth, h: img.naturalHeight });
+ URL.revokeObjectURL(url);
};
processImage(selected, activePreset, cropMode);
diff --git a/src/app/tools/discord-image-shrinker/page.tsx b/src/app/tools/discord-image-shrinker/page.tsx
index e048fc2..e65f159 100644
--- a/src/app/tools/discord-image-shrinker/page.tsx
+++ b/src/app/tools/discord-image-shrinker/page.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable @next/next/no-img-element */
"use client";
import { useState, useRef, useCallback } from "react";
diff --git a/src/app/tools/exif-scrubber/page.tsx b/src/app/tools/exif-scrubber/page.tsx
index fc0cc11..b9b0ed0 100644
--- a/src/app/tools/exif-scrubber/page.tsx
+++ b/src/app/tools/exif-scrubber/page.tsx
@@ -11,7 +11,6 @@ interface GPSData {
export default function ExifScrubberPage() {
const [file, setFile] = useState(null);
- const [previewUrl, setPreviewUrl] = useState(null);
const [exifData, setExifData] = useState | null>(null);
const [gpsData, setGpsData] = useState(null);
const [isAnalyzing, setIsAnalyzing] = useState(false);
@@ -29,9 +28,6 @@ export default function ExifScrubberPage() {
setFile(selected);
setScrubbedUrl(null);
setStatusMessage("");
-
- const preview = URL.createObjectURL(selected);
- setPreviewUrl(preview);
setIsAnalyzing(true);
try {
diff --git a/src/app/tools/favicon-generator/page.tsx b/src/app/tools/favicon-generator/page.tsx
index 7522dba..f5b9111 100644
--- a/src/app/tools/favicon-generator/page.tsx
+++ b/src/app/tools/favicon-generator/page.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable @next/next/no-img-element */
"use client";
import { useState, useRef, useCallback } from "react";
@@ -20,11 +21,21 @@ const ICON_SPECS: IconSize[] = [
{ name: "Android Chrome 512", filename: "android-chrome-512x512.png", size: 512, purpose: "PWA splash screen" },
];
+function getHtmlTags(title: string) {
+ return `
+
+
+
+
+
+ `;
+}
+
function createIcoFromPngs(pngBuffers: { size: number; buffer: ArrayBuffer }[]): Blob {
const numImages = pngBuffers.length;
const headerSize = 6;
const dirEntrySize = 16;
- let dataOffset = headerSize + dirEntrySize * numImages;
+ const dataOffset = headerSize + dirEntrySize * numImages;
const totalBytes = dataOffset + pngBuffers.reduce((acc, p) => acc + p.buffer.byteLength, 0);
const out = new Uint8Array(totalBytes);
@@ -183,16 +194,6 @@ export default function FaviconGeneratorPage() {
document.body.removeChild(link);
};
- const getHtmlTags = (title: string) => {
- return `
-
-
-
-
-
- `;
- };
-
const handleCopyHtml = () => {
navigator.clipboard.writeText(getHtmlTags(appName));
setCopiedHtml(true);
diff --git a/src/app/tools/hash-generator/page.tsx b/src/app/tools/hash-generator/page.tsx
index 6037244..3701955 100644
--- a/src/app/tools/hash-generator/page.tsx
+++ b/src/app/tools/hash-generator/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useState, useEffect, useCallback } from "react";
+import { useState, useEffect } from "react";
import ToolLayout from "@/components/ToolLayout";
// Pure JavaScript MD5 Implementation (RFC 1321)
@@ -169,74 +169,80 @@ export default function HashGeneratorPage() {
const [copiedKey, setCopiedKey] = useState(null);
const [copiedAll, setCopiedAll] = useState(false);
- const computeHashes = useCallback(async (text: string) => {
- const encoder = new TextEncoder();
- const data = encoder.encode(text);
-
- // 1. MD5
- const md5Hash = md5(text);
-
- // 2. Web Crypto subtle digests
- const getSubtleHash = async (algo: string): Promise => {
- try {
- const buffer = await crypto.subtle.digest(algo, data);
- const hashArray = Array.from(new Uint8Array(buffer));
- return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
- } catch (err) {
- console.error(`Failed to calculate ${algo}:`, err);
- return "";
- }
- };
-
- const [sha1Hash, sha256Hash, sha384Hash, sha512Hash] = await Promise.all([
- getSubtleHash("SHA-1"),
- getSubtleHash("SHA-256"),
- getSubtleHash("SHA-384"),
- getSubtleHash("SHA-512"),
- ]);
-
- setHashes([
- {
- algorithm: "MD5",
- name: "MD5 (Message Digest 5)",
- bitLength: 128,
- hash: md5Hash,
- securityNote: "Legacy / Checksums only (vulnerable to collisions)",
- },
- {
- algorithm: "SHA-1",
- name: "SHA-1 (Secure Hash Algorithm 1)",
- bitLength: 160,
- hash: sha1Hash,
- securityNote: "Legacy (Git commits, legacy checksums)",
- },
- {
- algorithm: "SHA-256",
- name: "SHA-256 (SHA-2 Family)",
- bitLength: 256,
- hash: sha256Hash,
- securityNote: "Industry Standard (High security, SSL, Bitcoin)",
- },
- {
- algorithm: "SHA-384",
- name: "SHA-384 (SHA-2 Family)",
- bitLength: 384,
- hash: sha384Hash,
- securityNote: "High Security (Government & enterprise grade)",
- },
- {
- algorithm: "SHA-512",
- name: "SHA-512 (SHA-2 Family)",
- bitLength: 512,
- hash: sha512Hash,
- securityNote: "Maximum Security (64-bit optimized systems)",
- },
- ]);
- }, []);
-
useEffect(() => {
- computeHashes(inputText);
- }, [inputText, computeHashes]);
+ let isCancelled = false;
+
+ async function runCompute() {
+ const encoder = new TextEncoder();
+ const data = encoder.encode(inputText);
+
+ const md5Hash = md5(inputText);
+
+ const getSubtleHash = async (algo: string): Promise => {
+ try {
+ const buffer = await crypto.subtle.digest(algo, data);
+ const hashArray = Array.from(new Uint8Array(buffer));
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
+ } catch (err) {
+ console.error(`Failed to calculate ${algo}:`, err);
+ return "";
+ }
+ };
+
+ const [sha1Hash, sha256Hash, sha384Hash, sha512Hash] = await Promise.all([
+ getSubtleHash("SHA-1"),
+ getSubtleHash("SHA-256"),
+ getSubtleHash("SHA-384"),
+ getSubtleHash("SHA-512"),
+ ]);
+
+ if (!isCancelled) {
+ setHashes([
+ {
+ algorithm: "MD5",
+ name: "MD5 (Message Digest 5)",
+ bitLength: 128,
+ hash: md5Hash,
+ securityNote: "Legacy / Checksums only (vulnerable to collisions)",
+ },
+ {
+ algorithm: "SHA-1",
+ name: "SHA-1 (Secure Hash Algorithm 1)",
+ bitLength: 160,
+ hash: sha1Hash,
+ securityNote: "Legacy (Git commits, legacy checksums)",
+ },
+ {
+ algorithm: "SHA-256",
+ name: "SHA-256 (SHA-2 Family)",
+ bitLength: 256,
+ hash: sha256Hash,
+ securityNote: "Industry Standard (High security, SSL, Bitcoin)",
+ },
+ {
+ algorithm: "SHA-384",
+ name: "SHA-384 (SHA-2 Family)",
+ bitLength: 384,
+ hash: sha384Hash,
+ securityNote: "High Security (Government & enterprise grade)",
+ },
+ {
+ algorithm: "SHA-512",
+ name: "SHA-512 (SHA-2 Family)",
+ bitLength: 512,
+ hash: sha512Hash,
+ securityNote: "Maximum Security (64-bit optimized systems)",
+ },
+ ]);
+ }
+ }
+
+ runCompute();
+
+ return () => {
+ isCancelled = true;
+ };
+ }, [inputText]);
const copyHash = async (hashValue: string, key: string) => {
const formatted = uppercase ? hashValue.toUpperCase() : hashValue.toLowerCase();
diff --git a/src/app/tools/lorem-ipsum/page.tsx b/src/app/tools/lorem-ipsum/page.tsx
index 93f139a..03f6d44 100644
--- a/src/app/tools/lorem-ipsum/page.tsx
+++ b/src/app/tools/lorem-ipsum/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useState, useEffect, useCallback } from "react";
+import { useState } from "react";
import ToolLayout from "@/components/ToolLayout";
const LOREM_WORDS = [
@@ -86,40 +86,76 @@ const LENGTH_RANGES: Record `${p}
`).join("\n\n");
+ }
+ return paras.join("\n\n");
+}
+
export default function LoremIpsumPage() {
const [numParagraphs, setNumParagraphs] = useState(3);
const [lengthPreset, setLengthPreset] = useState("medium");
const [customWords, setCustomWords] = useState(75);
const [startWithLorem, setStartWithLorem] = useState(true);
const [format, setFormat] = useState("plain");
- const [generatedText, setGeneratedText] = useState("");
+ const [generatedText, setGeneratedText] = useState(() =>
+ buildLorem(3, "medium", 75, true, "plain")
+ );
const [copied, setCopied] = useState(false);
- const generate = useCallback(() => {
- const targetWordsPerPara =
- lengthPreset === "custom"
- ? customWords
- : Math.floor(
- Math.random() * (LENGTH_RANGES[lengthPreset].max - LENGTH_RANGES[lengthPreset].min + 1)
- ) + LENGTH_RANGES[lengthPreset].min;
+ const generate = (
+ paras = numParagraphs,
+ preset = lengthPreset,
+ words = customWords,
+ start = startWithLorem,
+ fmt = format
+ ) => {
+ setGeneratedText(buildLorem(paras, preset, words, start, fmt));
+ };
- const paras: string[] = [];
- for (let i = 0; i < numParagraphs; i++) {
- paras.push(generateParagraph(targetWordsPerPara, i === 0, startWithLorem));
- }
+ const handleParagraphsChange = (val: number) => {
+ setNumParagraphs(val);
+ generate(val, lengthPreset, customWords, startWithLorem, format);
+ };
- if (format === "html") {
- setGeneratedText(paras.map((p) => `${p}
`).join("\n\n"));
- } else if (format === "markdown") {
- setGeneratedText(paras.join("\n\n"));
- } else {
- setGeneratedText(paras.join("\n\n"));
- }
- }, [numParagraphs, lengthPreset, customWords, startWithLorem, format]);
+ const handleLengthChange = (preset: ParagraphLength) => {
+ setLengthPreset(preset);
+ generate(numParagraphs, preset, customWords, startWithLorem, format);
+ };
- useEffect(() => {
- generate();
- }, [generate]);
+ const handleCustomWordsChange = (words: number) => {
+ setCustomWords(words);
+ generate(numParagraphs, "custom", words, startWithLorem, format);
+ };
+
+ const handleStartWithLoremChange = (start: boolean) => {
+ setStartWithLorem(start);
+ generate(numParagraphs, lengthPreset, customWords, start, format);
+ };
+
+ const handleFormatChange = (fmt: OutputFormat) => {
+ setFormat(fmt);
+ generate(numParagraphs, lengthPreset, customWords, startWithLorem, fmt);
+ };
const handleCopy = async () => {
try {
@@ -174,17 +210,17 @@ export default function LoremIpsumPage() {
value={numParagraphs}
onChange={(e) => {
const val = Math.min(Math.max(1, parseInt(e.target.value) || 1), 20);
- setNumParagraphs(val);
+ handleParagraphsChange(val);
}}
- className="w-20 border border-[#1a1a1a] bg-[#0a0a0a] px-3 py-2 text-sm font-medium text-gray-200 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-900/20"
+ className="w-20 border border-[#1a1a1a] bg-[#0a0a0a] px-3 py-2 text-sm font-medium text-gray-200 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-900/20"
/>
{[1, 3, 5, 10].map((count) => (
setNumParagraphs(count)}
- className={`rounded px-2 py-1 text-xs font-medium transition-colors ${
+ onClick={() => handleParagraphsChange(count)}
+ className={`px-2 py-1 text-xs font-medium transition-colors ${
numParagraphs === count
? "bg-blue-600 text-white"
: "bg-[#1a1a1a] text-gray-400 hover:bg-[#222]"
@@ -204,8 +240,8 @@ export default function LoremIpsumPage() {
setLengthPreset(e.target.value as ParagraphLength)}
- className="w-full border border-[#1a1a1a] bg-[#0a0a0a] px-3 py-2 text-sm font-medium text-gray-200 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-900/20"
+ onChange={(e) => handleLengthChange(e.target.value as ParagraphLength)}
+ className="w-full border border-[#1a1a1a] bg-[#0a0a0a] px-3 py-2 text-sm font-medium text-gray-200 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-900/20"
>
Short (~35 words)
Medium (~75 words)
@@ -222,7 +258,7 @@ export default function LoremIpsumPage() {
max="300"
value={customWords}
onChange={(e) =>
- setCustomWords(Math.min(Math.max(10, parseInt(e.target.value) || 10), 300))
+ handleCustomWordsChange(Math.min(Math.max(10, parseInt(e.target.value) || 10), 300))
}
className="w-24 border border-[#1a1a1a] bg-[#0a0a0a] px-2 py-1 text-xs font-medium text-gray-200 focus:outline-none focus:ring-1 focus:ring-blue-900"
/>
@@ -235,11 +271,11 @@ export default function LoremIpsumPage() {
Format
-
+
setFormat("plain")}
- className={`flex-1 py-1.5 text-xs font-medium transition-colors ${
+ onClick={() => handleFormatChange("plain")}
+ className={`flex-1 py-1.5 text-xs font-medium transition-colors ${
format === "plain"
? "bg-[#0a0a0a] text-gray-200 shadow-xs"
: "text-gray-600 hover:text-gray-200"
@@ -249,8 +285,8 @@ export default function LoremIpsumPage() {
setFormat("html")}
- className={`flex-1 py-1.5 text-xs font-medium transition-colors ${
+ onClick={() => handleFormatChange("html")}
+ className={`flex-1 py-1.5 text-xs font-medium transition-colors ${
format === "html"
? "bg-[#0a0a0a] text-gray-200 shadow-xs"
: "text-gray-600 hover:text-gray-200"
@@ -270,7 +306,7 @@ export default function LoremIpsumPage() {
setStartWithLorem(e.target.checked)}
+ onChange={(e) => handleStartWithLoremChange(e.target.checked)}
className="h-4 w-4 border-[#1a1a1a] text-blue-600 focus:ring-blue-900"
/>
Start with “Lorem ipsum...”
@@ -278,7 +314,7 @@ export default function LoremIpsumPage() {
generate()}
className="mt-3 flex w-full items-center justify-center gap-1.5 bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 focus:outline-none focus:ring-1 focus:ring-blue-900 focus:ring-offset-2"
>
diff --git a/src/app/tools/password-generator/page.tsx b/src/app/tools/password-generator/page.tsx
index fa0c645..0c31eae 100644
--- a/src/app/tools/password-generator/page.tsx
+++ b/src/app/tools/password-generator/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useState, useEffect, useCallback } from "react";
+import { useState, useMemo } from "react";
import ToolLayout from "@/components/ToolLayout";
const CHARSETS = {
@@ -90,6 +90,68 @@ function calculateStrength(password: string, poolSize: number): StrengthInfo {
}
}
+function createPassword(
+ length: number,
+ useUppercase: boolean,
+ useLowercase: boolean,
+ useDigits: boolean,
+ useSymbols: boolean,
+ excludeAmbiguous: boolean
+): string {
+ let pool = "";
+ const guaranteedChars: string[] = [];
+
+ const getCleanCharset = (chars: string) => {
+ if (!excludeAmbiguous) return chars;
+ return chars
+ .split("")
+ .filter((c) => !AMBIGUOUS_CHARS.has(c))
+ .join("");
+ };
+
+ if (useUppercase) {
+ const set = getCleanCharset(CHARSETS.uppercase);
+ if (set.length > 0) {
+ pool += set;
+ guaranteedChars.push(set[getRandomInt(set.length)]);
+ }
+ }
+ if (useLowercase) {
+ const set = getCleanCharset(CHARSETS.lowercase);
+ if (set.length > 0) {
+ pool += set;
+ guaranteedChars.push(set[getRandomInt(set.length)]);
+ }
+ }
+ if (useDigits) {
+ const set = getCleanCharset(CHARSETS.digits);
+ if (set.length > 0) {
+ pool += set;
+ guaranteedChars.push(set[getRandomInt(set.length)]);
+ }
+ }
+ if (useSymbols) {
+ const set = getCleanCharset(CHARSETS.symbols);
+ if (set.length > 0) {
+ pool += set;
+ guaranteedChars.push(set[getRandomInt(set.length)]);
+ }
+ }
+
+ if (pool.length === 0) {
+ return "";
+ }
+
+ const resultChars = [...guaranteedChars];
+ const remainingCount = Math.max(0, length - guaranteedChars.length);
+
+ for (let i = 0; i < remainingCount; i++) {
+ resultChars.push(pool[getRandomInt(pool.length)]);
+ }
+
+ return shuffleArray(resultChars).slice(0, length).join("");
+}
+
export default function PasswordGeneratorPage() {
const [length, setLength] = useState(16);
const [useUppercase, setUseUppercase] = useState(true);
@@ -97,81 +159,23 @@ export default function PasswordGeneratorPage() {
const [useDigits, setUseDigits] = useState(true);
const [useSymbols, setUseSymbols] = useState(true);
const [excludeAmbiguous, setExcludeAmbiguous] = useState(false);
+ const [version, setVersion] = useState(0);
- const [password, setPassword] = useState("");
const [history, setHistory] = useState([]);
const [copied, setCopied] = useState(false);
const [historyCopiedIndex, setHistoryCopiedIndex] = useState(null);
- const generatePassword = useCallback(() => {
- let pool = "";
- const guaranteedChars: string[] = [];
+ const password = useMemo(() => {
+ void version;
+ return createPassword(length, useUppercase, useLowercase, useDigits, useSymbols, excludeAmbiguous);
+ }, [length, useUppercase, useLowercase, useDigits, useSymbols, excludeAmbiguous, version]);
- const getCleanCharset = (chars: string) => {
- if (!excludeAmbiguous) return chars;
- return chars
- .split("")
- .filter((c) => !AMBIGUOUS_CHARS.has(c))
- .join("");
- };
-
- if (useUppercase) {
- const set = getCleanCharset(CHARSETS.uppercase);
- if (set.length > 0) {
- pool += set;
- guaranteedChars.push(set[getRandomInt(set.length)]);
- }
+ const generatePassword = () => {
+ if (password) {
+ setHistory((prev) => (!prev.includes(password) ? [password, ...prev.slice(0, 4)] : prev));
}
- if (useLowercase) {
- const set = getCleanCharset(CHARSETS.lowercase);
- if (set.length > 0) {
- pool += set;
- guaranteedChars.push(set[getRandomInt(set.length)]);
- }
- }
- if (useDigits) {
- const set = getCleanCharset(CHARSETS.digits);
- if (set.length > 0) {
- pool += set;
- guaranteedChars.push(set[getRandomInt(set.length)]);
- }
- }
- if (useSymbols) {
- const set = getCleanCharset(CHARSETS.symbols);
- if (set.length > 0) {
- pool += set;
- guaranteedChars.push(set[getRandomInt(set.length)]);
- }
- }
-
- if (pool.length === 0) {
- setPassword("");
- return;
- }
-
- const resultChars = [...guaranteedChars];
- const remainingCount = Math.max(0, length - guaranteedChars.length);
-
- for (let i = 0; i < remainingCount; i++) {
- resultChars.push(pool[getRandomInt(pool.length)]);
- }
-
- const shuffled = shuffleArray(resultChars).slice(0, length).join("");
- setPassword(shuffled);
-
- // Add to history if unique
- setHistory((prev) => {
- if (shuffled && !prev.includes(shuffled)) {
- return [shuffled, ...prev.slice(0, 4)];
- }
- return prev;
- });
- }, [length, useUppercase, useLowercase, useDigits, useSymbols, excludeAmbiguous]);
-
- // Generate on initial load and option changes
- useEffect(() => {
- generatePassword();
- }, [generatePassword]);
+ setVersion((v) => v + 1);
+ };
const handleCopy = async () => {
if (!password) return;
diff --git a/src/app/tools/px-to-rem/page.tsx b/src/app/tools/px-to-rem/page.tsx
index 5dcd688..67383a6 100644
--- a/src/app/tools/px-to-rem/page.tsx
+++ b/src/app/tools/px-to-rem/page.tsx
@@ -142,7 +142,6 @@ export default function PxToRemPage() {
// Extra units for currently active px
const currentPxNum = parseFloat(pxValue) || 0;
- const currentRemNum = parseFloat(remValue) || 0;
const percentageValue = formatNumber((currentPxNum / baseSize) * 100, 2);
const ptValue = formatNumber(currentPxNum * 0.75, 2); // 1px = 0.75pt
const cssDeclaration = `${selectedProperty}: ${remValue || "0"}rem; /* ${pxValue || "0"}px */`;
diff --git a/src/app/tools/qr-code/page.tsx b/src/app/tools/qr-code/page.tsx
index f54b2db..344a6b4 100644
--- a/src/app/tools/qr-code/page.tsx
+++ b/src/app/tools/qr-code/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useState, useEffect, useCallback } from "react";
+import { useState, useEffect } from "react";
import ToolLayout from "@/components/ToolLayout";
import QRCode from "qrcode";
@@ -21,7 +21,7 @@ const ERROR_CORRECTION_DESCRIPTIONS: Record = {
};
export default function QrCodePage() {
- const [inputText, setInputText] = useState("https://devtoolbox.com");
+ const [inputText, setInputText] = useState("https://toolbox.oiupoyt.space");
const [size, setSize] = useState("medium");
const [errorCorrection, setErrorCorrection] = useState("M");
const [margin, setMargin] = useState(2);
@@ -32,45 +32,57 @@ export default function QrCodePage() {
const [error, setError] = useState(null);
const [copiedDataUrl, setCopiedDataUrl] = useState(false);
- const generateQRCode = useCallback(async () => {
- const textToEncode = inputText.trim();
- if (!textToEncode) {
- setDataUrl("");
- setSvgString("");
- setError(null);
- return;
- }
-
- try {
- const options = {
- width: SIZE_MAP[size].px,
- margin: margin,
- errorCorrectionLevel: errorCorrection,
- color: {
- dark: fgColor,
- light: bgColor,
- },
- };
-
- const [url, svg] = await Promise.all([
- QRCode.toDataURL(textToEncode, options),
- QRCode.toString(textToEncode, { ...options, type: "svg" }),
- ]);
-
- setDataUrl(url);
- setSvgString(svg);
- setError(null);
- } catch (err: unknown) {
- console.error("QR Code generation error:", err);
- const message = err instanceof Error ? err.message : "Failed to generate QR code";
- setError(message);
- setDataUrl("");
- }
- }, [inputText, size, errorCorrection, margin, fgColor, bgColor]);
-
useEffect(() => {
- generateQRCode();
- }, [generateQRCode]);
+ let active = true;
+
+ async function generate() {
+ const textToEncode = inputText.trim();
+ if (!textToEncode) {
+ if (active) {
+ setDataUrl("");
+ setSvgString("");
+ setError(null);
+ }
+ return;
+ }
+
+ try {
+ const options = {
+ width: SIZE_MAP[size].px,
+ margin,
+ errorCorrectionLevel: errorCorrection,
+ color: {
+ dark: fgColor,
+ light: bgColor,
+ },
+ };
+
+ const [url, svg] = await Promise.all([
+ QRCode.toDataURL(textToEncode, options),
+ QRCode.toString(textToEncode, { ...options, type: "svg" }),
+ ]);
+
+ if (active) {
+ setDataUrl(url);
+ setSvgString(svg);
+ setError(null);
+ }
+ } catch (err: unknown) {
+ if (active) {
+ console.error("QR Code generation error:", err);
+ const message = err instanceof Error ? err.message : "Failed to generate QR code";
+ setError(message);
+ setDataUrl("");
+ }
+ }
+ }
+
+ generate();
+
+ return () => {
+ active = false;
+ };
+ }, [inputText, size, errorCorrection, margin, fgColor, bgColor]);
const handleDownloadPng = () => {
if (!dataUrl) return;
diff --git a/src/app/tools/secret-share/page.tsx b/src/app/tools/secret-share/page.tsx
index 0e414e0..14b2390 100644
--- a/src/app/tools/secret-share/page.tsx
+++ b/src/app/tools/secret-share/page.tsx
@@ -1,8 +1,21 @@
"use client";
-import { useState, useEffect, useCallback } from "react";
+import { useState, useMemo, useSyncExternalStore, useCallback } from "react";
import ToolLayout from "@/components/ToolLayout";
+function subscribeHash(callback: () => void) {
+ window.addEventListener("hashchange", callback);
+ return () => window.removeEventListener("hashchange", callback);
+}
+
+function getHashSnapshot() {
+ return typeof window !== "undefined" ? window.location.hash : "";
+}
+
+function getServerSnapshot() {
+ return "";
+}
+
// Utility helpers for Uint8Array <-> Base64URL
function bufferToBase64Url(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer);
@@ -33,23 +46,20 @@ export default function SecretSharePage() {
const [secretText, setSecretText] = useState("");
const [generatedLink, setGeneratedLink] = useState("");
const [isCopied, setIsCopied] = useState(false);
- const [mode, setMode] = useState<"create" | "view">("create");
+ const hash = useSyncExternalStore(subscribeHash, getHashSnapshot, getServerSnapshot);
+ const [explicitMode, setExplicitMode] = useState<"create" | "view" | null>(null);
+
+ const hasSecretInHash = useMemo(() => {
+ const raw = hash.startsWith("#") ? hash.substring(1) : hash;
+ const params = new URLSearchParams(raw);
+ return Boolean(params.get("d") && params.get("k"));
+ }, [hash]);
+
+ const mode = explicitMode ?? (hasSecretInHash ? "view" : "create");
const [decryptedText, setDecryptedText] = useState(null);
const [decryptError, setDecryptError] = useState(null);
const [isDecrypting, setIsDecrypting] = useState(false);
- // Check URL hash on mount for view mode
- useEffect(() => {
- if (typeof window === "undefined") return;
- const hash = window.location.hash.substring(1);
- if (!hash) return;
-
- const params = new URLSearchParams(hash);
- if (params.get("d") && params.get("k")) {
- setMode("view");
- }
- }, []);
-
const handleCreateSecret = async () => {
if (!secretText.trim()) return;
@@ -145,8 +155,10 @@ export default function SecretSharePage() {
const handleDestroy = () => {
setDecryptedText(null);
setDecryptError(null);
- window.location.hash = "";
- setMode("create");
+ if (typeof window !== "undefined") {
+ window.location.hash = "";
+ }
+ setExplicitMode("create");
setSecretText("");
setGeneratedLink("");
};
diff --git a/src/app/tools/uuid-generator/page.tsx b/src/app/tools/uuid-generator/page.tsx
index 8fcd209..10b458d 100644
--- a/src/app/tools/uuid-generator/page.tsx
+++ b/src/app/tools/uuid-generator/page.tsx
@@ -1,21 +1,15 @@
"use client";
-import { useState, useEffect, useCallback } from "react";
+import { useState, useCallback } from "react";
import ToolLayout from "@/components/ToolLayout";
function generateSingleUUID(uppercase: boolean, hyphens: boolean): string {
- let uuid: string;
+ let id: string;
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
- uuid = crypto.randomUUID();
- } else if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
- const bytes = new Uint8Array(16);
- crypto.getRandomValues(bytes);
- bytes[6] = (bytes[6] & 0x0f) | 0x40; // Version 4
- bytes[8] = (bytes[8] & 0x3f) | 0x80; // Variant 10xx
- const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
- uuid = `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
+ id = crypto.randomUUID();
} else {
- uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
+ // RFC4122 v4 fallback
+ id = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
@@ -23,17 +17,19 @@ function generateSingleUUID(uppercase: boolean, hyphens: boolean): string {
}
if (!hyphens) {
- uuid = uuid.replace(/-/g, "");
+ id = id.replace(/-/g, "");
}
- return uppercase ? uuid.toUpperCase() : uuid.toLowerCase();
+ return uppercase ? id.toUpperCase() : id.toLowerCase();
}
export default function UuidGeneratorPage() {
const [count, setCount] = useState(5);
const [uppercase, setUppercase] = useState(false);
const [hyphens, setHyphens] = useState(true);
- const [uuids, setUuids] = useState([]);
+ const [uuids, setUuids] = useState(() =>
+ Array.from({ length: 5 }, () => generateSingleUUID(false, true))
+ );
const [copiedIndex, setCopiedIndex] = useState(null);
const [copiedAll, setCopiedAll] = useState(false);
@@ -51,11 +47,6 @@ export default function UuidGeneratorPage() {
[]
);
- // Generate on initial load
- useEffect(() => {
- generate(count, uppercase, hyphens);
- }, []); // eslint-disable-line react-hooks/exhaustive-deps
-
const handleGenerate = () => {
generate(count, uppercase, hyphens);
};
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx
index b039b5c..52fd6bb 100644
--- a/src/components/Footer.tsx
+++ b/src/components/Footer.tsx
@@ -2,19 +2,19 @@ import Link from "next/link";
export default function Footer() {
return (
-
- Free, client-side developer and media utilities.
+
+ kattoolbox // zero-knowledge client execution
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index da9e63a..ba9b3a1 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -18,21 +18,22 @@ export default function Header() {
: [];
return (
-