Skip to content

feat: add baseline security headers - #94

Open
sleepycat12341013 wants to merge 1 commit into
aghyad97:mainfrom
sleepycat12341013:main
Open

feat: add baseline security headers#94
sleepycat12341013 wants to merge 1 commit into
aghyad97:mainfrom
sleepycat12341013:main

Conversation

@sleepycat12341013

Copy link
Copy Markdown

There is no X-Frame-Options, X-Content-Type-Options, Referrer-Policy or
Permissions-Policy on any route.

Why it matters here

Two of these are worth more than usual for this project:

Framing. Every tool page can be loaded inside an iframe on any origin. For a
site whose promise is "your files never leave your browser", an attacker can
frame a tool, overlay it, and drive a user's clicks somewhere else while the
address bar they trust is out of sight. I could not find anywhere the app
relies on being embedded, so DENY should be safe.

Camera and microphone. getUserMedia is used by BarcodeScanner, QRScanner,
CameraCapture, ScreenRecorder and the two media-tester panels. Without
Permissions-Policy those grants are not scoped, so an embedded third party can
inherit them. Restricting them to self keeps the tools working while closing
that off.

What this changes

Adds one header block covering /:path*. The three existing rules (the COOP
header on /tools/bg-removal and the two Cache-Control rules) are untouched —
this is 24 added lines and nothing removed.

What this deliberately leaves out

No Content-Security-Policy. The AI tools pull models from huggingface.co and
staticimgly.com at runtime and execute them through WebAssembly, so a policy
would need 'wasm-unsafe-eval' plus an allowlist for those hosts, and Next.js
needs nonce handling for its inline bootstrap. Getting that wrong silently
breaks tools, so it seemed better left to you as a separate decision. Happy to
open an issue with a draft policy if that is useful.


Disclosure: I used AI assistance to survey the codebase and check the live
headers. The header choices and the decision to leave CSP out are mine, and I
verified the config still emits all four rule blocks after the change.

## What's missing

The site sends no security headers apart from HSTS, which Vercel adds
automatically:
@vercel

vercel Bot commented Aug 17, 2026

Copy link
Copy Markdown

@sleepycat12341013 is attempting to deploy a commit to the Aghyad's projects Team on Vercel.

A member of the Team first needs to authorize it.

Little-Data pushed a commit to Little-Data/browserytools that referenced this pull request Aug 23, 2026
… interiors across 137 tools (aghyad97#36)

* feat(design): token layer, light + dark

Add src/styles/design-tokens.css with all --bt-* custom properties from
spec §2 (base colors, category chip pairs, motion easings) for :root
(light) and .dark. Import it as the first line of globals.css. Adds a
completeness test asserting every required token is defined in both
scopes.

* feat(i18n): redesign namespaces across 9 locales

* feat(design): Inter as latin font

* feat(chrome): production Rail with i18n/RTL/theming

Port the /preview prototype rail into src/components/layout/rail.tsx +
rail.module.css as a standalone, production-ready component:

- Logical properties throughout (inset-inline-start, padding-inline-end,
  inset-inline-end, text-align: start) so dir=rtl mirrors for free.
- Every string via next-intl (Rail + ToolsConfig.categoriesShort); the
  prototype's .replace() label hack is replaced with translated short labels.
- All colours mapped to var(--bt-*) design tokens (light + dark); zero
  hardcoded hex/rgba.
- Rail-bottom slots for LanguageSwitcher + ThemeSwitcher.
- src/lib/sponsors.ts ships SPONSORS empty; the rotor renders nothing until a
  real sponsor exists (no fictional entries, no fake metrics).

Not mounted in the production layout (chrome-switchover owns that). Adds
rail.test.tsx: 11 category labels render, single active-dot tracks selection,
sponsor section absent while empty.

* feat(chrome): command palette; rewrite search e2e contract

Port the /preview prototype's ⌘K CommandPalette + useCommandPalette to
src/components/layout/ as production chrome (sibling of the Rail, mounted
at chrome-switchover):

- Logical CSS properties only; all colours via --bt-* tokens (adds
  --bt-hover / --bt-overlay / --bt-shadow-pop with dark counterparts);
  the input/list hairline is a standalone element, never a one-edge border
- All strings via existing i18n keys: tool names ToolsConfig.tools.<slug>.name,
  category tags ToolsConfig.categoriesShort.*, placeholder
  Landing.searchPlaceholder, empty state Sidebar.noToolsFound; results
  locale-sorted
- Keyboard contract: Cmd/Ctrl+K toggle, / when idle, Escape closes, arrows
  move selection, Enter navigates; zero open/close animation (keyboard-initiated)
- 9 RTL unit tests (rendering, i18n resolution, filtering, keyboard nav,
  router navigation, backdrop close, hook shortcuts)
- e2e/search.spec.ts rewritten to the new contract: palette specs gated
  behind PALETTE_MOUNTED=false until chrome-switchover mounts the palette;
  the preserved ?search= server redirect (SearchAction JSON-LD SEO contract)
  stays actively tested and passes against the production build

* test(design): guard palette tokens in completeness check

* feat(chrome): switch to rail shell — mount Rail/Palette/Footer/MobileBar, retire Header/Sidebar/CoffeeBanner usages

* fix(chrome): review fixes — homepage h1, exactly-one-h1 smoke gate, scoped ignore

Task-review follow-ups to the rail switchover (a554e81):

- HomePage: promote the always-rendered tagline <p> to the page <h1> (the
  retired Header carried the site h1; / must not ship h1-less). Tailwind
  preflight makes headings inherit, so the visual is unchanged.
- ToolTitle: add HAS_OWN_H1 (40 slugs, derived by scanning each tool's
  page.tsx + imported component tree for <h1) and render nothing for them —
  those routes already have their own h1. Two runtime corrections the smoke
  gate caught: invoice's own h1 lives in an inactive Radix tab (not in DOM at
  load) so ToolTitle keeps supplying it; markdown-editor's ReactMarkdown
  preview renders SAMPLE_MARKDOWN's "# Welcome" as an h1 so ToolTitle is
  suppressed.
- Smoke: drop the unscoped 401/403/429 status-code ignore (the URL-scoped
  api.github.com pattern covers the star badge via text + location URL);
  assert exactly one h1 on every route; cover / alongside the 137 tools.

Gates: unit 494, build, smoke 138/138, search e2e 4.

* feat(legal): privacy + terms pages

Adds /privacy and /terms as simple content pages under the (home) route
group (reusing the AppShell + Footer chrome for free), with a shared
Legal.* i18n namespace across all 9 locales (Task 2's script pattern).
Content is short and true: no accounts, on-device processing only,
Vercel Analytics aggregate usage, AGPL-3.0 source on GitHub, no warranty,
free for everyone.

Also closes the Task 6 footer deferral: adds license/privacy/terms links
to the production footer's bottom bar now that the routes + keys exist.

Adds both routes to sitemap.ts.

* feat(landing)!: rail landing with full-catalog SEO surface

Replace the home page body with the rail landing ported from the /preview
prototype, keeping every SEO surface the old HomePage carried.

- New src/components/landing/{landing.tsx,landing.module.css}: statement h1,
  coffee CTA (one per screen), ⌘K search button, on-device live demo,
  favorites/recent rows (client-only, non-empty), featured Apps strip, Popular
  curated grid with category filters, and a full-catalog 'All tools' section
  that links every one of the 137 tools grouped by category (spec §6.5).
- src/lib/featured-apps.ts: Apps strip lists LIVE routes only (audio-transcriber,
  pdf, image-compression, screen-recorder); localized copy resolves from the
  existing ToolsConfig.tools.<slug> keys.
- (home)/page.tsx: body swapped to <Landing/>; metadata, StructuredData
  type=website (ItemList JSON-LD), the ?search= redirect and hreflang preserved.
- command-palette: useCommandPalette also opens on a custom event so the landing
  search button (a sibling tree from the AppShell) can trigger it.
- Remove the now-orphaned src/components/HomePage.tsx.

Tokens only (light+dark), logical properties (RTL mirrors), existing i18n keys
only, no single-edge borders. Gates: landing.spec 5/5, unit 494/494, smoke
138/138, Lighthouse SEO 100 (= baseline), performance 72->75.

* feat(template): five-zone ToolShell

Port the /preview tool-page prototype into a production five-zone template
(spec §3) that all 137 tools adopt in the R2 migration batches:

  1 Crumb (mono CATEGORY / TOOL, category in its chip fg token)
  2 Title + sub (sub always ends with Template.onDevicePromise; owns the h1)
  3 Stage (max-width 880, the tool's primary surface as children)
  4 ControlsBar (bordered card; single dark-pill primary, end-aligned)
  5 Related (3 same-category tiles, the shared landing tile visual)
    + ToolSeoContent, rendered once by the tools layout (never duplicated)

All colours via var(--bt-*) tokens (light+dark), logical properties
throughout (RTL for free), existing i18n keys only. No single-edge accent
bars. tool-shell.test.tsx covers crumb colour, exactly-one-h1, appended
promise, 3 same-category related, and controls/primary wiring.

No tool adopts it yet (batches do that), so the exactly-one-h1 smoke gate
stays 138/138.

* refactor(shared): extract ToolTile used by landing and ToolShell

Landing (Popular/Favorites/Recent/All-tools) and ToolShell's Related
section each rendered a near-identical ~90-line tile (icon chip with
category tokens, name, category label, hover arrow). Extracted into
src/components/shared/ToolTile.tsx + ToolTile.module.css; entrance
animation stays owned by the landing call site via className/style so
ToolShell's related grid keeps its non-animated behavior.

Also fixes a backwards comment in tool-title.tsx: HAS_OWN_H1 grows as
tools migrate to the five-zone template (adopting tools add their
slug so ToolTitle stands down), not shrinks.

* refactor(tools): ImageCompression onto ToolShell

File-tool pilot for the five-zone template. Wraps ImageCompression in
<ToolShell> (crumb/title/sub/controls/related), moves the size-reduction
stat + Download into the controls bar (dark-pill primary via lib/download's
downloadDataUrl, same _compressed.<fmt> filename + toasts). Adds
image-compression to HAS_OWN_H1 so ToolTitle stands down (one h1). Also
centres the shell canvas (margin-inline:auto) so zone 5's ToolSeoContent
lines up under it — reconciling the R2 pilot alignment seam.

* refactor(tools): JsonFormatter onto ToolShell

Text-tool pilot for the five-zone template. Wraps JsonFormatter in
<ToolShell>; Format is the dark-pill primary action, the secondary ops
(Minify/Validate/Load Sample/Clear) + indent/sort-keys controls move into
the controls bar. Copy now uses the shared <CopyButton> with translated
success/error messages (rendered only when there is text to copy). Adds
json-formatter to HAS_OWN_H1 so ToolTitle stands down (one h1).

* refactor(tools): Timer onto ToolShell

Interactive-tool pilot for the five-zone template. Wraps Timer in
<ToolShell>; Start/Pause is the dark-pill primary action, Reset/Fullscreen/
Test-sound move into the controls bar. Mode tabs, countdown inputs and the
NumberFlow display stay in the stage; the fullscreen overlay keeps its own
controls via a persistent containerRef so requestFullscreen still works.
Card title/description are replaced by the shell's title/sub. Adds timer to
HAS_OWN_H1 so ToolTitle stands down (one h1).

* fix(template): pre-batch pattern fixes — category-only crumb, ControlStat, CopyButton disabled

Review items adjudicated before the migration batches:
- ToolShell crumb is category-only (mono eyebrow in chip fg); the tool name
  exactly duplicated the h1 — crumbName span + CSS removed, tests updated.
- <ControlStat label>value</ControlStat> exported from template/controls-bar
  so tools stop importing tool-shell.module.css; ImageCompression migrated.
- ImageCompression compare toggle back to icon-only; sizeReduction caption
  now sits over the % value inside the ControlStat.
- CopyButton gains an optional disabled prop; JsonFormatter renders it
  persistently, inert while there is nothing to copy
  (Tools.JsonFormatter.nothingToCopy now unused — flagged for cleanup).

* refactor(tools): batch B1 onto ToolShell (image)

Migrates the 21 remaining Image Tools onto the five-zone ToolShell
template (image-compression was the pilot): image-upscaler,
image-captioner, depth-map, object-cutout, ascii-art, photo-collage,
image-color-picker, exif-remover, screenshot-beautifier, meme-generator,
bg-removal, phone-mockups, image-converter, color-correction, svg,
svg-png, photo-censor, image-resizer, color-blindness, exif-viewer,
favicon-generator.

Per the ratified pattern: shell owns crumb/h1/sub (slugs added to
HAS_OWN_H1), hand-rolled dropzones -> shared FileDropzone, download
anchors -> lib/download, dedicated copy buttons -> CopyButton, live
stats -> ControlStat. Behaviour, toasts, filenames and options
unchanged; existing tool tests pass unmodified except one h1-moved
assertion in SvgPngConverter.test.tsx (title now rendered by the shell).

* feat(shared): CopyButton data-testid passthrough for batch migrations

* refactor(tools): batch B2 onto ToolShell (ai)

* refactor(tools): batch B2 remainder — WASM AI tools onto ToolShell

* fix(shared): CopyButton defaults localize via Common namespace

CopyButton's label/successMessage/errorMessage props defaulted to
hardcoded English strings, so any caller omitting errorMessage (9 B2
components did) toasted English clipboard-failure text regardless of
locale. Make CopyButton self-localizing via useTranslations("Common"),
falling back to t("copy")/t("copied")/t("copyFailed") when a prop is
omitted; explicit props still override. Adds the new Common.copyFailed
key to all 9 locale files.

* refactor(tools): batch B3 onto ToolShell (text-language)

* refactor(tools): batch B4 onto ToolShell (math-finance, file)

* fix(template): honest network note for non-on-device tools (currency-converter)

ToolShell unconditionally appended the on-device promise to every tool's
sub line, but CurrencyConverter fetches live rates from external APIs
(frankfurter.app, exchangerate-api.com) — the claim was false there.

Add optional Tool.onDevice (default true) to tools-config, flip it to
false only for currency-converter, and have ToolShell render the new
Template.networkNote string instead when a resolved entry opts out.
Translated networkNote into all 9 locales via inject-redesign-i18n.mjs.

* refactor(tools): batch B5 onto ToolShell (data, media)

* refactor(tools): batch B6 onto ToolShell (productivity, developer)

* refactor(tools): batch B7 onto ToolShell (design, security)

Final migration batch. All 12 Design + 5 Security tools wrapped in the
five-zone ToolShell (shell owns the h1; own headers/h1s removed; dedicated
output copies -> CopyButton per the ratified rule; manual downloads ->
lib/download; OgImageGenerator logo input -> FileDropzone). HAS_OWN_H1
updated.

Also sweeps three Productivity tools that predate B6 and were missed by its
derived scope (signature-maker, random-picker, mind-map) so the only tool
left off ToolShell is the known markdown-editor STOP.

e2e smoke: scoped-ignore /_vercel/insights (Vercel Analytics 404s under
local next start; pre-existing every-route artifact, previously masked on
mind-map by its double-h1 baseline failure).

* feat(catalog): category reorg — Tests & Games, School & Learning, Business

Display-only reorg per audit spec §4.3: Code/HTML/JSON Formatter →
Developer Tools, Image Color Picker + Color Blindness Simulator →
Design Tools, Morse Code → Data Tools, and three new categories
(Tests & Games, School & Learning, Business) absorbing Typing Test,
Periodic Table, and Invoice Generator + Expense Tracker. "Speech to
Text" renamed to "Live Dictation (mic)" in the config's SEO source
name. Zero slug/URL changes — 137 tools, 14 categories before and
after.

Wires the 3 new category ids through every place that enumerates
them: chip tokens (light+dark) in design-tokens.css, the
completeness test's REQUIRED list, the CHIP maps in ToolShell and
landing, and categories/categoriesShort across all 9 locales via an
extended inject-redesign-i18n.mjs. README synced for validate-tools.

* feat(brand): manifest/meta refresh (logo mount deferred — awaiting final SVG)

* chore(redesign): R2 cleanup ledger

- Dedup per-category chip colour map into src/lib/category-chips.ts,
  consumed by both landing and tool-shell (was two byte-identical blocks).
- Tokenize landing + ToolTile hover shadows via --bt-shadow-pop
  (removes the last dark-mode-blind hardcoded rgba() in these components).
- Prune 29 i18n keys orphaned by the ToolShell migration + earlier batches,
  verified namespace-aware unreferenced, across all 9 locales
  (AudioEditor dead effect/tab keys, Json/Curl nothingToCopy, Favicon
  copySnippet, Css/Sql/productivity title+subtitle). resetSpeed and
  KeepAwake.title deliberately kept.
- Rename speech-to-text to "Live Dictation (mic)" in ToolsConfig across
  9 locales (natural translations), matching tools-config.ts + README.
- README tool badge 136+ -> 137+; fix stale (home)/page.tsx search comment;
  /coffee link no longer opens a new tab.

* chore(redesign): remove superseded chrome + prototype

The redesign ships the left-rail app-shell (Rail + MobileBar + ToolShell),
so the old top chrome and the /preview prototype are dead code:
- src/components/header.tsx, sidebar.tsx (component; the Sidebar.noToolsFound
  LOCALE KEY stays — it is reused by command-palette), coffee-banner.tsx,
  GitHubStarBanner.tsx — verified zero imports.
- src/app/preview/ prototype (+ its noindex layout) — served its purpose.
mobile-bar imports Sheet from @/components/ui/sheet (not header), so no
extraction was needed.

* chore(redesign): final-review cleanup — delete orphaned chrome, prune dead locale keys, fix stale registry comments, ignore test artifacts

* feat(brand): mount final b glyph — rail, icons, favicons, pinned tab

* feat(brand): single-source logo pipeline (src/brand/logo.svg → bun run brand) + kit mark candidate

* feat(brand): tighter kit mark, tile-less app icons (TILE_ENABLED flag)

* feat(chrome): wordmark beside rail glyph (localized siteName)

* feat(chrome): bare brand glyph — drop the dark tile box in rail and mobile bar

* fix(chrome): wire rail categories to the landing filter store

Rail rendered category rows as dead <Link href="/"> because app-shell
never passed onCategory/activeCategory, and Landing kept its own
private useState instead of a shared source of truth. Add a
session-only Zustand store (category-filter-store) that both the rail
chips and Landing's Popular grid now read/write, and wire it through
app-shell into Rail and MobileBar's sheet-rail path (closing the sheet
on category pick, navigating home first if not already on "/").

* feat(chrome): sliding fill pill replaces active-category dot in rail

* fix(chrome): symmetric pill padding — pill edges align to rail column

* feat(chrome): synthesized interaction sounds with opt-in mute toggle

* feat(chrome): top-right utility cluster (coffee + switchers); drop rail search pill

* feat(chrome): glass search bar, rail-bottom utilities, universal landing dropzone with file router

- Sticky glass top bar (desktop >900px, every AppShell route): wide ⌘K
  search field only (max 520px), color-mix glass + backdrop blur, no
  divider; sticky in the content column so it never overlaps content.
- First-paint CSS entrance: .enter now animates via @Keyframes fadeUp
  with 'backwards' fill (no data-mounted hydration gate) — everything
  fades up together; Apps strip joins the 0/40/80/120/160ms cadence.
  'backwards' (not 'both') so no retained transform traps fixed children.
- Coffee pill + theme/language/sound switchers moved from the top-right
  cluster to the rail bottom, below the sponsor slot (utility-cluster
  deleted); still exactly one coffee CTA per screen.
- Universal landing dropzone: whole page is a drop target with a frosted
  overlay; new src/lib/file-router.ts routes dropped files to ranked tool
  suggestions (images keep the inline compression demo); tick cue on
  suggest; no auto-navigation, no cross-route file handoff (future work).
- i18n: Landing.dropAnyFile/dropSuggest injected into all 9 locales.
- Tests: 19 new file-router unit tests (incl. slug↔tools-config audit);
  rail-category e2e spec skipped on Mobile Chrome (desktop-only chrome).

* fix: eliminate landing hydration mismatch from Math.sin float formatting

The .vizBar waveform in AppViz set inline height from
`${30 + 60 * Math.abs(Math.sin(i * 1.7))}%`. Math.sin is transcendental
and spec-defined only to ~1 ULP, so the dev server's V8 and the browser's
V8 format the raw double into different strings, mismatching the inline
style attribute on hydrate. Quantize to .toFixed(2) so both engines emit
an identical string.

Also remove the now-obsolete /hydration/i entry from the smoke suite's
IGNORED list so a future hydration regression can never be masked; full
smoke passes 138/138 without it.

* feat(motion): instant landing, graceful late rows, reduced-motion for JS animation

Retire the landing entrance stagger (content renders fully visible at first
paint — no fade, no translate, no per-tile --d delays). fadeUp survives only
for the genuinely event-driven drag overlay and drop-suggestion panel.

Favorites/recent rows mount after hydration (persisted stores); give just
those rows a single 150ms opacity-only fade so their late arrival reads
intentional. Reduced-motion: instant (existing .canvas * animation:none).

Wrap the app tree in <MotionConfig reducedMotion="user"> to gate every
framer-motion spring (rail active pill etc.) on the user's preference.

Gate useCountUp on prefers-reduced-motion (jump to final value). Retire the
landing fake-liveness pulses: liveDot is now a static dot; the vizBar
equalizer runs only on .appCard:hover (static, fully-visible at rest).

* feat(motion): pointer-open palette transition

Pointer-triggered palette opens (top-bar / mobile-bar / suggestion-panel
button — the openCommandPalette event path) now animate: overlay opacity
120ms, panel scale(0.97)->1 + fade 160ms, var(--bt-ease-out), transform-origin
top center. Keyboard opens (Cmd+K, /) bypass the event and stay instant.

Source is carried on the custom event's detail.animated; useCommandPalette
returns it so AppShell can pass it to CommandPalette. Esc/backdrop close stays
instant (unmount). Reduced-motion: all instant.

* feat(motion): soft theme flip

Wrap the ThemeSwitcher's class swap in a View Transition so light/dark tokens
cross-dissolve as one coordinated ~180ms crossfade instead of a hard cut.
next-themes runs with disableTransitionOnChange (it injects *{transition:none}
during the swap), which blocks any CSS-transition soft-flip — the View
Transitions API sidesteps that and keeps the global transition-all suppression
intact. flushSync commits next-themes' effect-driven class flip inside the
transition so the new snapshot captures the new theme. Feature-checked and
reduced-motion-gated (falls back to instant setTheme); only fires on an
explicit toggle, so initial load is unaffected.

* feat(motion): filter continuity + copy check-in

Category filter continuity: the Popular grid remounts on each category change
(keyed nonce, never on first render) and replays a brief 240ms opacity dip
(1 -> 0.4 -> 1) so the instant re-order reads as an intentional flush instead
of a hard snap. Reduced-motion: instant.

CopyButton: the Copy -> Check swap gets a tiny scale-in on the check
(0.8 -> 1, 150ms ease-out) via a wrapper that mounts fresh each copy.
Reduced-motion: instant.

* perf(motion): per-property transitions, opacity shadows, cheaper glass, dead framer removal

* perf(motion): per-property transitions on remaining selection swatches

* docs: Wave R3 tool-interior design contract (token bridge, width variant, molecules)

* feat(shared): interior molecule library (OutputPanel, SettingsCard, SliderRow, StatStrip, ModePicker, TwoPane) + format utils

* feat(design): bridge shadcn tokens onto bt palette — interiors inherit the system

* feat(template): ToolShell wide variant — 24 tools get room to breathe

* fix(design): AA muted contrast in light mode; ModePicker dark indicator; contract amendments

* refactor(tools): R3 pilots — unit-converter, json-formatter, expense-tracker onto interior molecules

* docs: ratify pilot precedents in R3 contract

* feat(shared): molecule API gaps from pilots — StatStrip sub, OutputPanel toast parity, SettingsCard action

* fix(shared): ModePicker measured indicator + overflow scroll — narrow/RTL safe

Replace the grid(--count)/translateX(--active) equal-width assumption with a
flex track (flex: 1 0 max-content) plus a measured indicator (offsetLeft/
offsetWidth -> --x/--w, re-measured via ResizeObserver + font load + active
change). Below ~480px with 6 unequal-length segments (unit-converter,
Arabic "درجة الحرارة") the old grid jammed to min-content and overflowed the
container; the indicator math assumed equal segment widths and broke.

The root now scrolls horizontally (overflow-x: auto, hidden scrollbar,
overscroll-behavior-x: contain) with the active segment auto-scrolled into
view. offsetLeft is a physical measurement regardless of dir, so the RTL
mirror rule is no longer needed and is removed.

* refactor(tools): R3 batch RB1 — image interiors onto molecules

* fix(shared): SliderRow readout dir=ltr — sign ordering safe under RTL (RB1 review)

* refactor(tools): R3 batch RB2 — AI interiors onto molecules

* refactor(tools): R3 batch RB3 — text-language interiors onto molecules

* refactor(tools): R3 batch RB4 — math/file/business/school/games interiors onto molecules

* docs: RB4 adjudications in R3 contract

* fix(shared): OutputPanel download toast passthrough; ModePicker re-measures on segment reflow

* refactor(tools): R3 batch RB5 — data/media interiors onto molecules

* fix(tools): reinstate Morse output descriptions via OutputPanel children (RB5 review)

* refactor(tools): R3 batch RB6 — productivity/developer interiors onto molecules

Migrates 20 of 22 productivity + developer-tools interiors onto the R3
interior design contract (SettingsCard/OptionRow, OutputPanel, TwoPane,
ModePicker, StatStrip, SliderRow); WorldClock and MindMap are documented
no-ops (live data-list / canvas interiors with no settings-form surface).
Zero test-file edits — all 8 covered components pass unmodified.

* refactor(tools): R3 batch RB7 — design/security interiors onto molecules

* chore(r3): cleanup — ModePicker disabled, orphaned i18n keys, dead scripts

- ModePicker: add disabled prop (dims control via opacity+pointer-events,
  aria-disabled, disables segments, guards onChange). Wire KeepAwake's
  session-active state to both pickers so the frozen-while-running visual
  cue is restored (was a functional no-op that lost its dimmed appearance).
- Prune 31 orphaned copy-pattern i18n keys x9 locales (identical per-locale
  count), dead after OutputPanel/CopyButton molecule adoption. New
  scripts/prune-orphaned-i18n.mjs self-derives orphans from en.json + src
  and enforces locale parity.
- Delete 6 dead one-off R1/R2 i18n migration scripts (inject-*, sync-honest-
  descriptions) whose output is baked into messages/*.json; keep
  generate-brand-assets and generate-tool-routes (living tools).

* fix: five pre-existing hydration errors surfaced by the R3 exit-gate sweep

The wave-exit RTL+dark console sweep (all 137 tool routes) caught React #418
hydration failures on 5 routes. All five pre-date R3 (blame: aghyad97#11, aghyad97#23, and
earlier); the new gate is simply the first check that listened. Fixes:

- ui/badge.tsx: Badge renders <span> instead of <div> (matches upstream
  shadcn). A div inside <p> (Stopwatch keyboard hint) is invalid HTML —
  browsers reparent it and hydration fails. inline-flex keeps layout identical.
- TypingTest: word stream was Math.random()-generated in a useState
  initializer — SSR text never matched the client. Now generated post-mount.
- QRScanner + BarcodeScanner: navigator.userAgent browser-name branch ran
  during render (SSR 'Other' vs client 'Chrome'). Now detected in an effect.
- ObjectCutout: hasWebGPU() called during render (SSR false -> amber warning;
  WebGPU client -> none). Now checked in an effect, defaulting to supported.

Unit 616/616; tsc clean. Note: the existing smoke suite misses these because
it asserts console-error-free before hydration settles — sweep waits 1.2s.

* fix: DynamicTitle title-observer infinite loop freezing Keep Awake / Pomodoro

The R3 exit-gate RTL sweep wedged twice at /tools/keep-awake: the page's main
thread was stuck in an infinite microtask loop (CDP Debugger.pause stack:
DynamicTitle.useEffect.applyLocale).

Cause: applyLocale assigned document.title unconditionally. Assigning the
title always replaces the <title> text node — even when the string is
identical — which re-fires the MutationObserver that called it. Any tool that
writes document.title after mount (Keep Awake's session title, Pomodoro's
countdown) seeds the first mutation, after which observer -> write ->
mutation -> observer loops forever and the tab freezes.

Pre-existing on main (identical code); reproduced in EN and AR, light and
dark, dev and prod. Fix: only assign when the locale substitution actually
changes the title. Verified: keep-awake + pomodoro pages responsive post-fix;
616/616 unit, tsc clean.

* fix: CronParser hydration mismatch — build-time next-run times baked into static HTML

Route aghyad97#94 in the exit-gate sweep (first time it was ever swept — earlier runs
stopped at route 89). The page is statically prerendered, so the useMemo'd
next-run list (cron interval starting from 'now', rendered via
toLocaleString() in the build machine's locale/timezone) and the
Intl-resolved default timezone were baked into the HTML at build time and
could never match the client's hydration render — React #418 text mismatch
on every load.

Fix: resolve the timezone and compute next-run times only after mount
(mounted flag); SSR shows the existing empty state briefly. Same pattern as
the TypingTest fix in f92c502. 616/616 unit, tsc clean.

* fix(landing): raster-image drops show routed suggestions alongside compress demo

* feat(chrome): start-aligned tool canvas, switcher order (lang last), blog link removed, maker apps in sponsor slot

* refactor(tools): R3 straggler pass — remaining old-style interiors normalized

* feat(chrome): maker slot trimmed to KashfBank+SuperBilled with real icons; tool description tooltips restored on tiles

* feat(chrome): utilities to header end cluster (compact); rounded 130+ marketing counts

* fix(chrome): coffee CTA last in header cluster, logical spacing step

* feat(landing): new statement — files-never-leave + open source, count-interpolated x9; drop rail status row

* feat(brand): Satoshi (self-hosted, Fontshare FFL) for the wordmark

* feat(ui): align base primitives to the design system — cards, tabs, controls

Card: match the shared SettingsCard shell so a stock Card reads as its
sibling — flat (drop the shadow), 11px radius, and THE interior padding
rule (20px inline / 16px block) on header/content/footer. CardTitle stays
a heading (not the mono eyebrow): it is load-bearing content on every page
via ToolSeoContent's text-xl section titles, so shell parity — not
typography — is what makes stock Cards read as SettingsCard siblings.

Tabs: restyle TabsList/TabsTrigger to the shared ModePicker segmented look
— a --bt-fill track with a hairline border and 3px inset, an active pill
that lifts on --bt-surface (light) / --bt-fill-hover (dark) with a hairline
+ subtle shadow. A transparent border on every trigger reserves the 1px so
the active state adds no layout shift. Radix role=tab/tablist semantics and
the --bt-accent focus ring are untouched.

APIs unchanged; dozens of tools inherit the system with no per-tool edits.

* fix(i18n): wordmark + maker app names stay English; maker slot copy localized x9

* fix(lint): prefer-const in ModePicker test (CI blocker)

* fix(chrome): breathing room between header switcher buttons

* fix(tools): code snippets follow the active theme

Syntax-highlighted output in CodeHighlighter, JsonCsvConverter, and
GlassmorphismGenerator always rendered dark (github-dark.css) regardless
of the app's light/dark state. Replaced the fixed stylesheet with a
theme-aware highlight.js palette scoped to next-themes' .dark class, and
swapped CodeHighlighter's hardcoded bg-[#0d1117] panel for bg-muted.
CodeScreenshot's export artboard keeps its own selectable dark themes —
that's an intentional, independent design (documented in the fix report).

* feat(chrome): tile hover swaps chip icon for arrow — slide-in from the side

* feat(landing): app-card vignettes depict the actual apps

* chore: new og-image (fitted 1200x630), README stripe link + rounded count

* chore: updated og-image — landing mockup on texture, fitted 1200x630 without clipping
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant