From 70c47f4855a22ec4babb209add9842ccef47f18f Mon Sep 17 00:00:00 2001 From: Kresna <13603341+slaveofcode@users.noreply.github.com> Date: Fri, 14 Aug 2026 10:11:18 +0700 Subject: [PATCH 1/3] feat(dev): add Word Counter, Case Converter, and Text Cleanup A basic text toolkit sharing one pure lib: live word/character/sentence/ reading-time counts; case conversion (UPPER/lower/Title/Sentence/camel/ Pascal/snake/kebab/CONSTANT); and text cleanup (trim, collapse spaces, remove blank lines/line breaks, strip HTML, remove accents, dedupe, sort). EN + ID SEO with how-to for each. --- .../plans/2026-08-14-text-toolkit.md | 21 ++++ src/islands/dev/CaseConverter.tsx | 56 +++++++++++ src/islands/dev/TextCleanup.tsx | 74 ++++++++++++++ src/islands/dev/WordCounter.tsx | 67 +++++++++++++ src/registry/tool-seo.ts | 98 +++++++++++++++++++ src/registry/tools.ts | 35 ++++++- src/tools/dev/text.lib.test.ts | 56 +++++++++++ src/tools/dev/text.lib.ts | 93 ++++++++++++++++++ 8 files changed, 499 insertions(+), 1 deletion(-) create mode 100644 docs/superpowers/plans/2026-08-14-text-toolkit.md create mode 100644 src/islands/dev/CaseConverter.tsx create mode 100644 src/islands/dev/TextCleanup.tsx create mode 100644 src/islands/dev/WordCounter.tsx create mode 100644 src/tools/dev/text.lib.test.ts create mode 100644 src/tools/dev/text.lib.ts diff --git a/docs/superpowers/plans/2026-08-14-text-toolkit.md b/docs/superpowers/plans/2026-08-14-text-toolkit.md new file mode 100644 index 0000000..bd3b7e9 --- /dev/null +++ b/docs/superpowers/plans/2026-08-14-text-toolkit.md @@ -0,0 +1,21 @@ +# Basic Text Toolkit — Plan + +**Date:** 2026-08-14 · Category: Dev · Three focused tools sharing one lib. + +Rationale: separate keyword-targeted pages ("word counter" alone is millions of searches/mo) beat one combined page that can only rank for one term. + +## Shared lib `src/tools/dev/text.lib.ts` (pure, unit-tested) +- `countText(text): TextStats` — characters, charactersNoSpaces, words, sentences, paragraphs, lines, readingMinutes (≈200 wpm). +- Case fns + `CASES` list: upper, lower, Title, Sentence, camel, Pascal, snake, kebab, CONSTANT (tokenizer respects camelCase + separators). +- Cleanup fns + `CLEANUP_OPS` + `cleanup(text, keys)`: trimLines, collapseSpaces, removeBlankLines, removeLineBreaks, stripHtml, removeAccents (NFD + strip ̀–ͯ), dedupeLines, sortLines. + +## Tools (islands, thin) +- `word-counter` (icon Baseline) → `WordCounter.tsx`: live stats grid + textarea. +- `case-converter` (icon CaseSensitive) → `CaseConverter.tsx`: input → case buttons → output + copy. +- `text-cleanup` (icon Brush) → `TextCleanup.tsx`: op checkboxes → live cleaned output + copy/download. + +## Registry + SEO +3 ToolDefs in `tools.ts`; EN + ID SEO for each in `tool-seo.ts` (title/description/intro/howTo/faqs). Bahasa "tool" loanword. + +## DoD +lib unit-tested (20 cases) · EN+ID SEO · vitest+lint+build green · 6 new routes built · develop→main→live verified. diff --git a/src/islands/dev/CaseConverter.tsx b/src/islands/dev/CaseConverter.tsx new file mode 100644 index 0000000..eb8c0ac --- /dev/null +++ b/src/islands/dev/CaseConverter.tsx @@ -0,0 +1,56 @@ +import { useState } from 'react'; +import { TextArea } from '@/components/ui/TextArea'; +import { Button } from '@/components/ui/Button'; +import { CopyButton } from '@/components/ui/CopyButton'; +import { CASES } from '@/tools/dev/text.lib'; +import type { Lang } from '@/i18n/config'; + +const TR: Record = { + en: { + intro: 'Convert text between UPPERCASE, lowercase, Title Case, Sentence case, camelCase, snake_case, kebab-case and more. Runs in your browser.', + input: 'Text', + output: 'Result', + placeholder: 'Type or paste your text…', + }, + id: { + intro: 'Ubah teks antara UPPERCASE, lowercase, Title Case, Sentence case, camelCase, snake_case, kebab-case, dan lainnya. Berjalan di browser Anda.', + input: 'Teks', + output: 'Hasil', + placeholder: 'Ketik atau tempel teks Anda…', + }, +}; + +export default function CaseConverter({ lang = 'en' }: { lang?: Lang }) { + const t = TR[lang] ?? TR.en; + const [text, setText] = useState(''); + const [output, setOutput] = useState(''); + + return ( +
+

{t.intro}

+ +
+ {t.input} +