Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
c8d2968
fix(case-converter): keep . and / in the prose styles (#274)
arxh80 Oct 23, 2026
e5aab20
feat(guides): add JWT decoding and claims guide (#36) (#273)
bhushan4work Oct 23, 2026
820d059
feat(guides): add free professional invoice guide (#286)
sank8-2 Oct 23, 2026
c1df312
docs: add URL encoding guide (#287)
srikar-naidu Oct 23, 2026
1b71bed
escape control characters (#288)
anurag2787 Oct 23, 2026
f3d3004
escape single quotes (#289)
anurag2787 Oct 23, 2026
7d2fac4
fix(use-local-storage): sync values across tabs (#291)
sank8-2 Oct 23, 2026
39cf016
fix(json-formatter): improve conversion panel accessibility (#293)
srikar-naidu Oct 23, 2026
87975d9
feat(guides): link guides and tools bidirectionally (#292)
bhushan4work Oct 23, 2026
ea1fd92
Add binary, decimal, and hexadecimal conversion guide (#290)
srikar-naidu Oct 23, 2026
1027499
docs(guides): add what is base64 guide (#37) (#294)
bhushan4work Oct 23, 2026
20c8cd4
fix(color-converter): reject malformed color tokens (#295)
Pratham2511 Oct 23, 2026
4162ef1
Preserve HSL values when changing alpha (#297)
srikar-naidu Oct 23, 2026
01b2ef5
feat: add UUID v4 and v7 generator and inspector (#296)
srujankaleru2007 Oct 23, 2026
4e63e6a
chore(deps): bump dompurify from 3.4.16 to 3.4.17 (#298)
dependabot[bot] Oct 23, 2026
c2ab9a4
FIx relative time (#301)
anurag2787 Oct 23, 2026
0440fdc
feat(tools): add Cron Expression Explainer (#305)
Saifjafri212213 Oct 23, 2026
5dce2bd
fix: preserve __proto__ in JSON key sorting (#312)
kachamsiddarth Oct 23, 2026
1d6a499
fix(jwt): carry rounded claim deltas into the next unit (#320)
bfs2021 Oct 23, 2026
c367a4c
fix: expose conversion errors as alerts (#323)
bhushan4work Oct 23, 2026
5c67145
fix: reject non-object JWT payloads in encoder (#325)
MugdhoAI Oct 23, 2026
dc6da88
fix(jwt-decoder): keep big numbers in the payload exact (#326)
Saifjafri212213 Oct 23, 2026
3f71e49
fix(a11y): add aria attributes and Escape handling to ShortcutsLegend…
sank8-2 Oct 23, 2026
4d42784
fix: improve disclosure control accessibility (#321) (#327)
bhushan4work Oct 23, 2026
df9257e
fix(json): name tree toggles after their node and report expansion (#…
bfs2021 Oct 23, 2026
3c8458a
feat(tools): add CSV/TSV Formatter & Validator (#299) (#330)
sank8-2 Oct 24, 2026
3e9bbb9
fix: normalize negative zero in invoice rounding (#331)
shresthgoel77-png Oct 24, 2026
f20e340
fix: improve converter copy button labels (#334)
srikar-naidu Oct 24, 2026
254bf65
fix: improve JSON formatter toolbar accessibility (#313)
srikar-naidu Oct 24, 2026
2b4c4dc
fix(base64): label copy buttons accessibly (#338)
srikar-naidu Oct 24, 2026
0bf1fa5
fix: label URL encoder and JWT decoder copy buttons (#335) (#339)
bhushan4work Oct 25, 2026
0e71416
fix(timestamp-converter): reject impossible calendar days in all date…
bhushan4work Oct 25, 2026
23ba86e
fix(UI): anchor shortcuts popover to left on base64 page to prevent o…
DeepakNegi13 Oct 25, 2026
25fd0d4
fix(json-formatter): keep key order in Format and Minify (#342)
arxh80 Oct 25, 2026
7681bea
fix(cron-explainer): describe the days the way the next runs work the…
Saifjafri212213 Oct 26, 2026
93d1eb6
feat(regex-tester): add regex tester tool (#349)
prakashNalla Oct 26, 2026
df10046
fix: handle JSON5 parsing in history and import flows (#348)
anurag2787 Oct 26, 2026
6c5bcfa
fix regrex 1000 matches issue: (#351)
anurag2787 Oct 26, 2026
ed4f935
docs: add the URL encoder to the README, home share card and package.…
Saifjafri212213 Oct 26, 2026
1776fad
docs: add the eight missing tools to the README, share images and pac…
harshit-exe-dev Oct 26, 2026
2618f1c
Fix timeour window is not defined issue (#360)
anurag2787 Oct 26, 2026
8c17466
fix(json-formatter): ignore quoted strings in error suggestions (#359)
kachamsiddarth Oct 26, 2026
6ec29ed
fix(deps): patch the undici, body-parser and zod advisories (#355)
anurag629 Oct 26, 2026
2e0c844
feat: add Markdown live preview tool (#358)
srikar-naidu Oct 26, 2026
473c46d
fix: reject impossible US-style slash dates (#362)
shresthgoel77-png Oct 26, 2026
13ea2a7
docs(guides): add how to test a regular expression in JavaScript (#363)
Saifjafri212213 Oct 26, 2026
7dd0ee1
fix(base64): report original invalid character positions (#332)
Aaqibhafeezkhan Oct 27, 2026
6f5476e
feat(guides): add WCAG color contrast ratio guide (#341)
08abhinav Oct 27, 2026
b87e897
docs(guides): add how to make a table in Markdown (#365)
Saifjafri212213 Oct 27, 2026
0571009
fix(markdown-preview): extract language from code-fence info string (…
bhushan4work Oct 27, 2026
fa62eaa
fix(markdown-preview): keep the Table button's table a table (#369)
Saifjafri212213 Oct 27, 2026
94e92bb
docs(guides): add how to read and write a cron expression (#371)
Saifjafri212213 Oct 27, 2026
f2c302c
docs(guides): add how to convert camelCase, snake_case and kebab-case…
Saifjafri212213 Oct 27, 2026
8adc108
fix: keep UUID v7 batches in ascending order (#328)
omisha123 Oct 28, 2026
ec7e994
chore(deps): bump lossless-json from 4.3.2 to 4.3.3 (#376)
dependabot[bot] Oct 28, 2026
4adc202
chore(deps): bump the next group with 3 updates (#374)
dependabot[bot] Oct 28, 2026
870b27a
chore(deps-dev): bump the dev-dependencies group across 1 directory w…
dependabot[bot] Oct 28, 2026
77a9add
docs(guides): add UUID v4 vs v7 and how to generate a UUID (#378)
Saifjafri212213 Oct 28, 2026
c4cabfb
feat(markdown-preview): open local markdown files (#383)
srikar-naidu Oct 28, 2026
c2c801a
docs(guides): add how to validate a CSV file and fix common errors (#…
Saifjafri212213 Oct 28, 2026
6e76dc5
feat(markdown-preview): download standalone html (#386)
srikar-naidu Oct 28, 2026
5408642
feat(color-converter): suggest AA-compliant foreground colors (#387)
srikar-naidu Oct 28, 2026
5ff6dfb
fix(json-formatter): keep carriage returns and refuse control charact…
Saifjafri212213 Oct 28, 2026
a3d23ab
feat(timestamp-converter): calculate elapsed time between instants (#…
srikar-naidu Oct 28, 2026
7cc34f2
feat(cron-explainer): add monthly schedule calendar (#394)
srikar-naidu Oct 28, 2026
25ddaf2
feat(json-formatter): add copyable tree paths (#395)
srikar-naidu Oct 28, 2026
adfb2e0
feat(json-formatter): compare JSON structurally in diff panel (#398)
srikar-naidu Oct 28, 2026
284b548
feat(uuid-inspector): decode RFC UUID v1 fields (#402)
srikar-naidu Oct 28, 2026
eee589e
docs: add guide for converting Unix timestamps to dates (#397)
anurag2787 Oct 28, 2026
4c837b4
feat(url-encoder): flag repeated query parameters (#401)
srikar-naidu Oct 28, 2026
288e2d5
fix(jwt-decoder): decode tokens pasted with a Bearer prefix or quotes…
prakashNalla Oct 28, 2026
6bf1015
fix(json-formatter): report every failing field in schema validation …
prakashNalla Oct 28, 2026
11bed25
fix(json-formatter): find JSONPath quoted names with escapes, bracket…
Saifjafri212213 Oct 28, 2026
5cfbfa7
fix(markdown-preview): count only words, not markdown markers (#413)
prakashNalla Oct 28, 2026
d1d5c7a
fix(uuid-generator): inspect GUIDs in braces and urn:uuid: values (#415)
prakashNalla Oct 28, 2026
83bbbdd
fix: give the tinted error and callout boxes a tint Tailwind 3 can bu…
Saifjafri212213 Oct 28, 2026
1df5ee2
feat(base64): report precise decode validation errors (#417)
srikar-naidu Oct 29, 2026
8965911
feat(case-converter): show the detected words (#418)
srikar-naidu Oct 30, 2026
139dd9f
fix(base64): show sizes just under 1 MB as MB instead of 1024.0 KB (#…
prakashNalla Oct 30, 2026
1ab0824
chore: version 0.9.0 (#424)
anurag629 Oct 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ body:
- Unix timestamp converter
- Text case converter
- Number base converter
- UUID Generator & Inspector
- Cron Expression Explainer
- CSV/TSV Formatter & Validator
- Regex Tester
- Markdown Live Preview
- Shell (theme, command palette, header)
- Other / not sure
validations:
Expand Down
Binary file added .github/assets/og-case-converter.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/assets/og-color-converter.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/assets/og-cron-explainer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/assets/og-csv-formatter.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/assets/og-home.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/assets/og-markdown-preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/assets/og-number-base-converter.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/assets/og-regex-tester.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/assets/og-timestamp-converter.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/assets/og-url-encoder.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/assets/og-uuid-generator.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ CI also builds the Cloudflare Worker and requests every page from it in local wo
2. Create `app/(tools)/<slug>/page.tsx`, a `<Name>Client.tsx` client component, and `opengraph-image.tsx`. Copying `page.tsx` and `opengraph-image.tsx` from `app/(tools)/base64/` and changing the slug is the easiest start.
3. Put pure logic in `lib/<name>.ts` with tests in `lib/__tests__/<name>.test.ts`. Tests can import with `@/` like the app does.
4. Add the tool to the "Which tool?" dropdown in `.github/ISSUE_TEMPLATE/bug_report.yml`.
5. Add the tool to the hand-written lists that the registry doesn't feed:
- `README.md`: an image in the "Tools" gallery and a line in the list under it.
- `.github/assets/`: the tool's share image as `og-<slug>.png` (1200 x 630). Run `npm run dev`, find the `og:image` path in the tool page's source (it has a suffix, such as `/<slug>/opengraph-image-190rzf`) and save the PNG from it.
- The home share card: name the tool in the description in `renderHomeOgImage` in `lib/og.tsx`, keep it to two lines (check `http://localhost:3000/opengraph-image`), and save the new card over `.github/assets/og-home.png`.
- `package.json`: the tool in `description` and a keyword in `keywords`.

The home page, metadata, JSON-LD, sitemap, manifest and command palette pick the new tool up from the registry automatically. Several people may be adding tools at the same time, so rebase on `develop` before asking for review.

Expand Down
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,62 @@ A developer's toolbelt: fast, privacy-first browser tools. Everything runs clien
<a href="https://tools.codercops.com/invoice-generator"><img src="./.github/assets/og-invoice-generator.png" alt="Invoice Generator"></a>
</td>
</tr>
<tr>
<td width="50%">
<a href="https://tools.codercops.com/url-encoder"><img src="./.github/assets/og-url-encoder.png" alt="URL Encoder and Decoder"></a>
</td>
<td width="50%">
<a href="https://tools.codercops.com/timestamp-converter"><img src="./.github/assets/og-timestamp-converter.png" alt="Unix Timestamp Converter"></a>
</td>
</tr>
<tr>
<td width="50%">
<a href="https://tools.codercops.com/color-converter"><img src="./.github/assets/og-color-converter.png" alt="Color Converter and Contrast Checker"></a>
</td>
<td width="50%">
<a href="https://tools.codercops.com/case-converter"><img src="./.github/assets/og-case-converter.png" alt="Text Case Converter"></a>
</td>
</tr>
<tr>
<td width="50%">
<a href="https://tools.codercops.com/number-base-converter"><img src="./.github/assets/og-number-base-converter.png" alt="Number Base Converter"></a>
</td>
<td width="50%">
<a href="https://tools.codercops.com/uuid-generator"><img src="./.github/assets/og-uuid-generator.png" alt="UUID Generator and Inspector"></a>
</td>
</tr>
<tr>
<td width="50%">
<a href="https://tools.codercops.com/cron-explainer"><img src="./.github/assets/og-cron-explainer.png" alt="Cron Expression Explainer"></a>
</td>
<td width="50%">
<a href="https://tools.codercops.com/csv-formatter"><img src="./.github/assets/og-csv-formatter.png" alt="CSV/TSV Formatter and Validator"></a>
</td>
</tr>
<tr>
<td width="50%">
<a href="https://tools.codercops.com/regex-tester"><img src="./.github/assets/og-regex-tester.png" alt="Regex Tester"></a>
</td>
<td width="50%">
<a href="https://tools.codercops.com/markdown-preview"><img src="./.github/assets/og-markdown-preview.png" alt="Markdown Live Preview"></a>
</td>
</tr>
</table>

- **[JSON formatter and validator](https://tools.codercops.com/json-formatter).** Format, minify, validate, collapsible tree view, and JSON to CSV / TypeScript / YAML / XML / query string. Large numbers are preserved losslessly.
- **[JWT decoder and inspector](https://tools.codercops.com/jwt-decoder).** Decode header, payload, and claims; verify HS/RS/PS/ES signatures via WebCrypto; security audit; HS256/384/512 encoder.
- **[Base64 encoder and decoder](https://tools.codercops.com/base64).** Text, images, and files; URL-safe variant; data URIs; magic-byte detection; Base32/58/85 and hashes.
- **[Invoice generator](https://tools.codercops.com/invoice-generator).** Live preview, vector PDF export with Unicode currency support, a reusable sender profile, all saved locally.
- **[URL encoder and decoder](https://tools.codercops.com/url-encoder).** Percent-encode or decode a URL, a query value, or form data, as a component or a full URL, with plus as space, and every query param broken out with duplicates kept in order.
- **[Unix timestamp converter](https://tools.codercops.com/timestamp-converter).** Convert Unix timestamps and dates with automatic unit detection, UTC, local time, ISO 8601, and relative time.
- **[Color converter & contrast checker](https://tools.codercops.com/color-converter).** Convert between HEX, RGB, and HSL with alpha channel support, live swatch, and WCAG AA/AAA contrast ratios for normal and large text.
- **[Text case converter](https://tools.codercops.com/case-converter).** Convert text between camelCase, snake_case, kebab-case, and 7 more naming conventions. Handles acronyms, digits, and multi-line input.
- **[Number base converter](https://tools.codercops.com/number-base-converter).** Convert integers between binary, octal, decimal, hex and any base from 2 to 36, with two's complement for 8, 16, 32 and 64 bits.
- **[UUID Generator & Inspector](https://tools.codercops.com/uuid-generator).** Generate UUID v4 and v7 identifiers locally, create batches, customize formatting, and inspect version, variant, and v7 timestamps.
- **[Cron expression explainer](https://tools.codercops.com/cron-explainer).** Paste a crontab line and get a plain-English description, a field-by-field breakdown and the next run times in UTC or your local time.
- **[CSV/TSV formatter & validator](https://tools.codercops.com/csv-formatter).** Parse, validate, and pretty-print CSV/TSV. Auto-detect delimiters, convert to JSON, TypeScript, YAML, SQL, and fix common formatting issues.
- **[Regex tester](https://tools.codercops.com/regex-tester).** Test a JavaScript regular expression against sample text with highlighted matches, capture groups, named groups and a replace preview.
- **[Markdown live preview](https://tools.codercops.com/markdown-preview).** Write GitHub-flavored Markdown with a sanitized live HTML preview, syntax-highlighted code, stats, and export tools.

## Why it exists

Expand Down
1 change: 1 addition & 0 deletions app/(tools)/base64/Base64Client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export function Base64Client() {
))}
</div>
<ShortcutsLegend
align="left"
shortcuts={[
{ keys: [modKey, "Enter"], label: "Encode/Decode" },
{ keys: [modKey, shiftKey, "S"], label: "Swap direction" },
Expand Down
35 changes: 35 additions & 0 deletions app/(tools)/base64/components/DecodeTab.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,39 @@ describe("DecodeTab data URIs", () => {
decode("data:text/plain,hello");
expect(notice()).not.toBeInTheDocument();
});

it("exposes invalid base64 through an alert", async () => {
decode("SGVs!bG8=");

expect(await screen.findByRole("alert")).toHaveTextContent(/Invalid base64/i);
});

it("reports an invalid character position in the original data URI", async () => {
const input = " data:text/plain;base64,SGV!bG8=";
decode(input);

expect(await screen.findByRole("alert")).toHaveTextContent(
`position ${input.indexOf("!") + 1}`,
);
});
});

describe("DecodeTab hex toggle", () => {
afterEach(cleanup);

it("reflects the hex dump state with aria-pressed", () => {
render(
<ToastProvider>
<DecodeTab />
</ToastProvider>,
);
fireEvent.change(screen.getByLabelText("Base64 to decode"), { target: { value: "SGVsbG8=" } });
const hex = screen.getByRole("button", { name: "Hex" });
expect(hex).toHaveAttribute("aria-pressed", "false");
fireEvent.click(hex);
expect(hex).toHaveAttribute("aria-pressed", "true");
expect(screen.getByText("Hex dump")).toBeInTheDocument();
fireEvent.click(hex);
expect(hex).toHaveAttribute("aria-pressed", "false");
});
});
10 changes: 7 additions & 3 deletions app/(tools)/base64/components/DecodeTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ export function DecodeTab() {

const result = useMemo(() => {
if (!effectiveInput.trim()) return null;
const r = base64ToBytes(effectiveInput);
const inputOffset = autoStripped
? input.length - input.trimStart().length + autoStripped.prefix.length
: 0;
const r = base64ToBytes(effectiveInput, { originalInput: input, inputOffset });
if (!r.ok) return { ok: false as const, error: r.error };
const bytes = r.value;
let mime = getMimeFromMagicBytes(bytes);
Expand Down Expand Up @@ -56,7 +59,7 @@ export function DecodeTab() {
return null;
})(),
};
}, [effectiveInput, autoStripped]);
}, [effectiveInput, autoStripped, input]);

function download() {
if (!result?.ok) return;
Expand Down Expand Up @@ -104,6 +107,7 @@ export function DecodeTab() {
<button
className={cn("btn", showHex && "bg-white/[0.06] border-[var(--hairline-strong)]")}
onClick={() => setShowHex((v) => !v)}
aria-pressed={showHex}
>
<Eye className="w-3.5 h-3.5" />
<span>Hex</span>
Expand All @@ -129,7 +133,7 @@ export function DecodeTab() {
</div>
)}
{result && !result.ok && (
<div className="flex items-start gap-2 text-[var(--rose)] text-[13px]">
<div role="alert" className="flex items-start gap-2 text-[var(--rose)] text-[13px]">
<AlertCircle className="w-4 h-4 mt-0.5" />
<span>{result.error}</span>
</div>
Expand Down
23 changes: 23 additions & 0 deletions app/(tools)/base64/components/EncodingsTab.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
import { ToastProvider } from "@/components/shared/Toast";
import { EncodingsTab } from "./EncodingsTab";

describe("EncodingsTab copy buttons", () => {
afterEach(cleanup);

it("names encoded output and hash copy buttons", async () => {
render(
<ToastProvider>
<EncodingsTab />
</ToastProvider>,
);

fireEvent.change(screen.getByRole("textbox", { name: "Text to encode" }), {
target: { value: "Hello" },
});

expect(await screen.findByRole("button", { name: "Copy Base32 (RFC 4648)" })).toBeInTheDocument();
expect(await screen.findByRole("button", { name: "Copy SHA256" })).toBeInTheDocument();
});
});
14 changes: 12 additions & 2 deletions app/(tools)/base64/components/EncodingsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,12 @@ export function EncodingsTab() {
<span className="font-mono text-[10.5px] tracking-[0.22em] uppercase text-[var(--fg-dim)]">
{LABELS[enc]}
</span>
<CopyButton value={outputs[enc]} disabled={!text} className="py-1" />
<CopyButton
value={outputs[enc]}
ariaLabel={`Copy ${LABELS[enc]}`}
disabled={!text}
className="py-1"
/>
</div>
<pre
className={cn(
Expand Down Expand Up @@ -112,7 +117,12 @@ export function EncodingsTab() {
<code className="flex-1 font-mono text-[11.5px] text-[var(--fg)] truncate">
{hashes[h]}
</code>
<CopyButton value={hashes[h]} iconOnly className="!py-1 !px-2" />
<CopyButton
value={hashes[h]}
ariaLabel={`Copy ${h.toUpperCase()}`}
iconOnly
className="!py-1 !px-2"
/>
</div>
))}
</div>
Expand Down
13 changes: 11 additions & 2 deletions app/(tools)/base64/components/FileTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,11 @@ export function FileTab() {
Single line
</button>
</div>
<CopyButton value={displayed} className="py-1" />
<CopyButton
value={displayed}
ariaLabel={`Copy ${info.name} as Base64`}
className="py-1"
/>
<button className="btn" onClick={downloadB64}>
<Download className="w-3.5 h-3.5" />
<span>.b64</span>
Expand Down Expand Up @@ -233,7 +237,12 @@ export function FileTab() {
{b.mime} · {formatBytes(b.size)}
</div>
</div>
<CopyButton value={b.base64} label="Copy" className="!py-1 !text-[11px]" />
<CopyButton
value={b.base64}
ariaLabel={`Copy ${b.name} as Base64`}
label="Copy"
className="!py-1 !text-[11px]"
/>
</div>
<pre className="mt-1 font-mono text-[11px] text-[var(--amber)] max-h-12 overflow-hidden opacity-80 truncate">
{b.base64.slice(0, 160)}...
Expand Down
12 changes: 7 additions & 5 deletions app/(tools)/base64/components/ImageTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,10 @@ export function ImageTab() {

{info ? (
<div className="space-y-3 animate-fade-in">
<OutputBlock label="Full data URI" value={info.dataUri} wrap />
<OutputBlock label="Base64 only (no prefix)" value={info.base64Only} wrap />
<OutputBlock label="HTML <img> tag" value={htmlSnippet} full={`<img src="${info.dataUri}" alt="${info.name}" width="${info.width}" height="${info.height}" />`} />
<OutputBlock label="CSS background-image" value={cssSnippet} full={`background-image: url("${info.dataUri}");`} />
<OutputBlock label="Full data URI" value={info.dataUri} copyLabel="Copy full data URI" wrap />
<OutputBlock label="Base64 only (no prefix)" value={info.base64Only} copyLabel="Copy Base64 only" wrap />
<OutputBlock label="HTML <img> tag" value={htmlSnippet} copyLabel="Copy HTML img tag" full={`<img src="${info.dataUri}" alt="${info.name}" width="${info.width}" height="${info.height}" />`} />
<OutputBlock label="CSS background-image" value={cssSnippet} copyLabel="Copy CSS background-image" full={`background-image: url("${info.dataUri}");`} />
</div>
) : (
<div className="card p-6 flex items-center justify-center min-h-[280px]">
Expand All @@ -176,11 +176,13 @@ export function ImageTab() {
function OutputBlock({
label,
value,
copyLabel,
full,
wrap,
}: {
label: string;
value: string;
copyLabel: string;
full?: string;
wrap?: boolean;
}) {
Expand All @@ -190,7 +192,7 @@ function OutputBlock({
<span className="font-mono text-[10.5px] tracking-[0.22em] uppercase text-[var(--fg-dim)]">
{label}
</span>
<CopyButton value={full ?? value} className="py-1" />
<CopyButton value={full ?? value} ariaLabel={copyLabel} className="py-1" />
</div>
<pre
className={cn(
Expand Down
76 changes: 76 additions & 0 deletions app/(tools)/base64/components/TextTab.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,43 @@ describe("TextTab switches", () => {
expect(screen.getByRole("button", { name: "Base64 → Text" })).toHaveAttribute("aria-pressed", "true");
});

it("exposes invalid base64 through an alert", async () => {
renderTab();
fireEvent.click(screen.getByRole("button", { name: "Base64 → Text" }));
fireEvent.change(screen.getByRole("textbox", { name: "Base64 to decode" }), {
target: { value: "SGVs!bG8=" },
});

expect(await screen.findByRole("alert")).toHaveTextContent(/Invalid base64/i);
});

it("decodes both alphabets regardless of the selected encoding variant", async () => {
renderTab();
fireEvent.click(screen.getByRole("button", { name: "Base64 → Text" }));
const input = screen.getByRole("textbox", { name: "Base64 to decode" });
expect(screen.getByText(/Decoding accepts standard and URL-safe characters, including mixed alphabets/)).toBeInTheDocument();

fireEvent.click(screen.getByRole("button", { name: "URL-safe" }));
fireEvent.change(input, { target: { value: "SGVsbG8=" } });
expect(await screen.findByText("Hello")).toBeInTheDocument();

fireEvent.click(screen.getByRole("button", { name: "Standard" }));
fireEvent.change(input, { target: { value: "8J-YgA" } });
expect(await screen.findByText("😀")).toBeInTheDocument();
});

it("clears a previous decoded result after invalid input", async () => {
renderTab();
fireEvent.click(screen.getByRole("button", { name: "Base64 → Text" }));
const input = screen.getByRole("textbox", { name: "Base64 to decode" });
fireEvent.change(input, { target: { value: "SGVsbG8=" } });
expect(await screen.findByText("Hello")).toBeInTheDocument();

fireEvent.change(input, { target: { value: "SGVs=" } });
expect(await screen.findByRole("alert")).toHaveTextContent(/Invalid base64 padding/);
expect(screen.queryByText("Hello")).not.toBeInTheDocument();
});

it("swaps a valid conversion result", async () => {
renderTab();
fireEvent.change(screen.getByRole("textbox", { name: "Text to encode" }), {
Expand All @@ -100,4 +137,43 @@ describe("TextTab switches", () => {
expect(screen.getByRole("textbox", { name: "Base64 to decode" })).toHaveValue("SGVsbG8gd29ybGQ=");
await waitFor(() => expect(screen.getByText("Hello world")).toBeInTheDocument());
});

it("names the output copy buttons by what they copy", async () => {
renderTab();
fireEvent.change(screen.getByRole("textbox", { name: "Text to encode" }), {
target: { value: "Hello world" },
});

expect(await screen.findByText("SGVsbG8gd29ybGQ=")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Copy Base64 output" })).toBeInTheDocument();
fireEvent.click(screen.getByText(/Copy as code literal/).closest("button")!);
expect(screen.getByRole("button", { name: "Copy javascript code literal" })).toBeInTheDocument();
});
});

describe("TextTab code-literal disclosure", () => {
afterEach(cleanup);

it("keeps its name stable and connects both states to the controlled panel", async () => {
render(
<ToastProvider>
<TextTab />
</ToastProvider>,
);
fireEvent.change(screen.getByRole("textbox", { name: "Text to encode" }), { target: { value: "Hello" } });
const toggle = await screen.findByRole("button", { name: "Copy as code literal" });
expect(toggle).toHaveAttribute("aria-expanded", "false");
expect(toggle).toHaveAttribute("aria-controls", "base64-code-literal-panel");
expect(screen.getByText("click to expand")).toHaveAttribute("aria-hidden", "true");
fireEvent.click(toggle);
expect(toggle).toHaveAttribute("aria-expanded", "true");
expect(screen.queryByText("click to expand")).not.toBeInTheDocument();
const panel = document.getElementById("base64-code-literal-panel");
expect(panel).toBeInTheDocument();
expect(toggle).toHaveAttribute("aria-controls", panel?.id);
expect(screen.getByRole("button", { name: "Copy as code literal" })).toBe(toggle);
fireEvent.click(toggle);
await waitFor(() => expect(toggle).toHaveAttribute("aria-expanded", "false"));
expect(document.getElementById("base64-code-literal-panel")).not.toBeInTheDocument();
});
});
Loading
Loading