From c3ba37c825bd18fc5809f4fe8237adb17caf0caa Mon Sep 17 00:00:00 2001 From: Kresna <13603341+slaveofcode@users.noreply.github.com> Date: Fri, 28 Aug 2026 06:13:14 +0700 Subject: [PATCH] feat: add PDF Page Numbers, Social Image Resizer, Image Palette & Text Encryption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four more client-side tools, each with a unit-tested pure lib + EN/ID SEO + Bahasa labels: - Add Page Numbers to PDF (new addPageNumbers + formatPageLabel, reuses pageNumberXY) - Social Media Image Resizer (platform presets + cover/contain fitRect; canvas) - Image Color Palette (frequency-quantized dominant colors → hex; canvas) - Text Encryption (Web Crypto AES-256-GCM + PBKDF2, encrypt/decrypt with password) --- src/islands/dev/TextEncrypt.tsx | 97 +++++++++++++++ src/islands/image/ImagePalette.tsx | 110 +++++++++++++++++ src/islands/image/SocialResize.tsx | 128 ++++++++++++++++++++ src/islands/pdf/PdfPageNumbers.tsx | 114 ++++++++++++++++++ src/registry/tool-i18n.ts | 4 + src/registry/tool-seo.ts | 136 ++++++++++++++++++++++ src/registry/tools.ts | 44 +++++++ src/tools/dev/textcrypt.lib.test.ts | 31 +++++ src/tools/dev/textcrypt.lib.ts | 71 +++++++++++ src/tools/image/palette.lib.test.ts | 44 +++++++ src/tools/image/palette.lib.ts | 34 ++++++ src/tools/image/social-resize.lib.test.ts | 41 +++++++ src/tools/image/social-resize.lib.ts | 43 +++++++ src/tools/pdf/layout.lib.test.ts | 10 +- src/tools/pdf/layout.lib.ts | 5 + src/tools/pdf/pdf.lib.ts | 30 ++++- 16 files changed, 940 insertions(+), 2 deletions(-) create mode 100644 src/islands/dev/TextEncrypt.tsx create mode 100644 src/islands/image/ImagePalette.tsx create mode 100644 src/islands/image/SocialResize.tsx create mode 100644 src/islands/pdf/PdfPageNumbers.tsx create mode 100644 src/tools/dev/textcrypt.lib.test.ts create mode 100644 src/tools/dev/textcrypt.lib.ts create mode 100644 src/tools/image/palette.lib.test.ts create mode 100644 src/tools/image/palette.lib.ts create mode 100644 src/tools/image/social-resize.lib.test.ts create mode 100644 src/tools/image/social-resize.lib.ts diff --git a/src/islands/dev/TextEncrypt.tsx b/src/islands/dev/TextEncrypt.tsx new file mode 100644 index 0000000..625822d --- /dev/null +++ b/src/islands/dev/TextEncrypt.tsx @@ -0,0 +1,97 @@ +import { useState } from 'react'; +import { Lock, Unlock } from 'lucide-react'; +import { Button } from '@/components/ui/Button'; +import { TextArea } from '@/components/ui/TextArea'; +import { CopyButton } from '@/components/ui/CopyButton'; +import { encryptText, decryptText } from '@/tools/dev/textcrypt.lib'; +import type { Lang } from '@/i18n/config'; + +type Mode = 'encrypt' | 'decrypt'; + +const TR: Record> = { + en: { + intro: 'Encrypt a message with a password (AES-256) so only someone with the password can read it — then share the scrambled text safely. All in your browser; the password never leaves your device.', + encrypt: 'Encrypt', decrypt: 'Decrypt', password: 'Password', + inEnc: 'Message to encrypt', inDec: 'Encrypted text to decrypt', + outEnc: 'Encrypted text', outDec: 'Decrypted message', + run: 'Encrypt', runDec: 'Decrypt', copy: 'Copy', + needPw: 'Enter a password.', failEnc: 'Could not encrypt.', failDec: 'Wrong password or invalid text.', + placeholderEnc: 'Type a secret message…', placeholderDec: 'Paste the encrypted text…', + }, + id: { + intro: 'Enkripsi pesan dengan kata sandi (AES-256) agar hanya yang punya kata sandi bisa membacanya — lalu bagikan teks teracak dengan aman. Semua di browser Anda; kata sandi tidak pernah keluar dari perangkat.', + encrypt: 'Enkripsi', decrypt: 'Dekripsi', password: 'Kata sandi', + inEnc: 'Pesan untuk dienkripsi', inDec: 'Teks terenkripsi untuk didekripsi', + outEnc: 'Teks terenkripsi', outDec: 'Pesan terdekripsi', + run: 'Enkripsi', runDec: 'Dekripsi', copy: 'Salin', + needPw: 'Masukkan kata sandi.', failEnc: 'Tidak dapat mengenkripsi.', failDec: 'Kata sandi salah atau teks tidak valid.', + placeholderEnc: 'Ketik pesan rahasia…', placeholderDec: 'Tempel teks terenkripsi…', + }, +}; + +export default function TextEncrypt({ lang = 'en' }: { lang?: Lang }) { + const t = TR[lang] ?? TR.en; + const [mode, setMode] = useState('encrypt'); + const [text, setText] = useState(''); + const [password, setPassword] = useState(''); + const [out, setOut] = useState(''); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(''); + + const run = async () => { + if (!password) { setError(t.needPw); return; } + setBusy(true); setError(''); setOut(''); + try { + setOut(mode === 'encrypt' ? await encryptText(text, password) : await decryptText(text, password)); + } catch { + setError(mode === 'encrypt' ? t.failEnc : t.failDec); + } finally { + setBusy(false); + } + }; + + const switchMode = (m: Mode) => { setMode(m); setOut(''); setError(''); }; + + return ( +
+

{t.intro}

+ +
+ + +
+ +
+ {mode === 'encrypt' ? t.inEnc : t.inDec} +