Skip to content

Architecture

Amir edited this page Sep 1, 2026 · 2 revisions

Language: English · فارسی

Architecture

Full detail lives in docs/ARCHITECTURE.md. This page is the short version.

Four decisions shape everything

  1. Minimum permissions. storage, and nothing else. No tabs, no activeTab, no host permissions.
  2. No per-element JavaScript. A messenger renders thousands of nodes and re-renders constantly; anything walking the DOM on every mutation would show up as jank.
  3. Survive Bale's redeploys. Its class names are per-build hashes, so nothing may depend on them.
  4. One codebase, two stores. Chrome and Firefox differ in exactly one manifest field, and that difference lives in the build script.

How the blur works

The content script writes one <style> element and one attribute:

<html data-bale-privacy="active animate hover messageText messageMedia …">

Every rule in the stylesheet is gated on those tokens:

html[data-bale-privacy~="active"][data-bale-privacy~="messageText"]
  [data-sentry-source-file="Text.tsx"] { filter: blur(var(--bale-privacy-text-radius)) !important }

So toggling a category, hovering, peeking or changing the blur radius rewrites one attribute or one custom property — a single style recalculation, never a DOM walk. New messages are blurred by the engine as they paint, because the rules already exist.

Data flow

   popup / options page          background (keyboard command)
            │                              │
            └──────────► storage ◄─────────┘
                            │
                  storage.onChanged
                            │
              ┌─────────────┴─────────────┐
        content script                content script
        (tab A)                       (tab B)

Storage is the only cross-context channel. Nothing sends a message to a tab, which is why no tabs permission is needed and why background tabs stay in sync.

What is where

Path Role
src/common/ settings schema, validation, migration, storage access
src/content/engine/selectors.ts the target registry — what to blur, and where it lives
src/content/engine/cssBuilder.ts pure functions: settings → stylesheet, tokens, custom properties
src/content/engine/styleController.ts the only code that mutates the host page
src/content/engine/activityWatcher.ts window focus, idle timer, hold-to-peek key
src/content/engine/tooltipGuard.ts suppresses native tooltips that CSS cannot reach
src/background/ install defaults, keyboard command
src/ui/ popup and options page
scripts/ build, manifests, icon generation, driven browser, screenshots

The one thing CSS cannot reach

Bale puts the last-message preview in a title attribute, and the browser renders that as a native tooltip outside the page. TooltipGuard moves the attribute aside with a single delegated pointerover listener, armed only while blurring is on and hover reveal is off, and restores everything by querying the DOM when it stands down.

Clone this wiki locally