feat: add baseline security headers - #94
Open
sleepycat12341013 wants to merge 1 commit into
Open
Conversation
## What's missing The site sends no security headers apart from HSTS, which Vercel adds automatically:
|
@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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
There is no
X-Frame-Options,X-Content-Type-Options,Referrer-PolicyorPermissions-Policyon 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
DENYshould be safe.Camera and microphone.
getUserMediais used by BarcodeScanner, QRScanner,CameraCapture, ScreenRecorder and the two media-tester panels. Without
Permissions-Policythose grants are not scoped, so an embedded third party caninherit them. Restricting them to
selfkeeps the tools working while closingthat off.
What this changes
Adds one header block covering
/:path*. The three existing rules (the COOPheader on
/tools/bg-removaland the twoCache-Controlrules) are untouched —this is 24 added lines and nothing removed.
What this deliberately leaves out
No
Content-Security-Policy. The AI tools pull models fromhuggingface.coandstaticimgly.comat runtime and execute them through WebAssembly, so a policywould need
'wasm-unsafe-eval'plus an allowlist for those hosts, and Next.jsneeds 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.