# Pdfstudio Core Solidjs Vanilla Bridge

> Use when modifying UI components or PDF/canvas logic in open-pdf-studio. Prevents the common mistake of breaking the bridge.ts facade that connects SolidJS reactive UI with vanilla JS PDF operations, or mutating state outside the createMutable proxy which breaks SolidJS reactivity. Covers the hybrid architecture, bridge.ts re-export pattern, createMutable shared state, per-document state delegation, and data flow direction rules. Keywords: bridge.ts, SolidJS, vanilla JS, createMutable, state, stores, hybrid architecture, reactivity, facade pattern, multi-document, UI not updating, state out of sync, SolidJS meets vanilla.

- Skill: `impertio-studio/pdfstudio-core-solidjs-vanilla-bridge` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add impertio-studio/pdfstudio-core-solidjs-vanilla-bridge`
- Raw SKILL.md: https://api.skillmd.com/api/skills/impertio-studio/pdfstudio-core-solidjs-vanilla-bridge/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- License: MIT
- Author: Impertio-Studio (https://skillmd.com/u/impertio-studio)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/impertio-studio/pdfstudio-core-solidjs-vanilla-bridge

---


# SolidJS ↔ Vanilla JS Bridge Architecture

## Architecture Overview

open-pdf-studio uses a **hybrid architecture** where two distinct JavaScript paradigms coexist:

| Layer | Technology | Responsibility | File Locations |
|-------|-----------|----------------|----------------|
| UI Shell | SolidJS (reactive) | Ribbon, panels, dialogs, status bar, overlays | `js/solid/components/*.jsx`, `js/solid/stores/*.js` |
| PDF Engine | Vanilla JS (imperative) | Canvas rendering, annotation tools, PDF load/save | `js/tools/*.js`, `js/annotations/*.js`, `js/pdf/*.js` |
| Bridge | TypeScript facade | Stable API for vanilla JS to call SolidJS stores | `js/bridge.ts` |
| Shared State | SolidJS `createMutable` | Central mutable state accessible from both layers | `js/core/state.ts` |

### Layer Separation Diagram

```
SolidJS Layer                    Vanilla JS Layer
─────────────                    ────────────────
App.jsx                          tools/tool-dispatcher.js
  TitleBar                       annotations/rendering.js
  Ribbon                         pdf/renderer.js
  LeftPanel                      pdf/loader.js
  PropertiesPanel                pdf/saver.js
  StatusBar                      ui/chrome/menus.js
  DialogHost                     ui/chrome/tabs.js
  ContextMenu                    ui/panels/*.js
  LoadingOverlay
```

**Rule: SolidJS owns ALL visual rendering. Vanilla JS owns ALL PDF/canvas logic.**

---

## The bridge.ts Facade

### Purpose

`js/bridge.ts` is the ONLY approved import path for vanilla JS code that needs to interact with SolidJS stores. It re-exports ~100+ functions from 17+ SolidJS store modules through a single, domain-grouped interface.

### Structure

```typescript
// js/bridge.ts — organized by UI domain

// ============= DIALOGS =============
export { openDialog, closeDialog, getDialogs, showMessage }
  from './solid/stores/dialogStore.js';

// ============= RIBBON =============
export { switchToTab as switchRibbonTab, activeTab as ribbonActiveTab,
  getColorPickerValue, setColorPickerValue }
  from './solid/stores/ribbonStore.js';

// ============= PROPERTIES PANEL =============
export { storeShowProperties, storeHideProperties, updateAnnotProp }
  from './solid/stores/propertiesStore.js';

// ============= LOADING OVERLAY =============
export { visible as loadingVisible, setVisible as setLoadingVisible }
  from './solid/stores/loadingStore.js';

// ... 13+ more domain sections covering:
// FORMAT, LEFT PANEL, CONTEXT MENU, FIND BAR, APP MENU,
// STICKY NOTE POPUPS, TEXT EDIT OVERLAY, PDF TEXT EDITOR,
// SCREENSHOT, BARS, FONTS, THUMBNAIL PANEL, PANEL DATA STORES
```

### Bridge Domains (Complete List)

| Domain | Store Source | Export Count |
|--------|------------|--------------|
| Dialogs | `dialogStore.js` | 4 |
| Ribbon | `ribbonStore.js` | 10 |
| Properties Panel | `propertiesStore.js` | 9 |
| Format | `formatStore.js` | 2 |
| Left Panel | `leftPanelStore.js` | 6 |
| Context Menu | `contextMenuStore.js` | 7 |
| Find Bar | `findBarStore.js` | 10 |
| Loading Overlay | `loadingStore.js` | 4 |
| App Menu | `appMenuStore.js` | 4 |
| Sticky Note Popups | `stickyNotePopupStore.js` | 4 |
| Text Edit Overlay | `textEditOverlayStore.js` | 4 |
| PDF Text Editor | `pdfTextEditStore.js` | 3 |
| Screenshot | `screenshotStore.js` | 2 |
| Bars | `formFieldsBarStore.js`, `pdfaBarStore.js`, `defaultAppBarStore.js` | 6 |
| Fonts | `fontStore.js` | 2 |
| Thumbnails | `panels/thumbnailStore.js` | 7 |
| Panel Data | 9 panel stores in `panels/` | ~30 |

---

## Data Flow Rules

### Direction 1: Vanilla JS → SolidJS (via bridge.ts)

When vanilla JS code needs to update UI state, it ALWAYS imports from `bridge.ts`:

```javascript
// CORRECT — vanilla JS file (e.g., tools/tool-dispatcher.js)
import { showMessage, setLoadingVisible } from '../bridge.js';

setLoadingVisible(true);
// ... do PDF operation ...
setLoadingVisible(false);
showMessage('PDF saved successfully');
```

### Direction 2: SolidJS → Vanilla JS (direct imports)

SolidJS components MAY import vanilla JS modules directly. The bridge is NOT needed in this direction:

```jsx
// CORRECT — SolidJS component importing vanilla JS
import { savePDF } from '../pdf/saver.js';
import { zoomIn } from '../pdf/renderer.js';

function SaveButton() {
  return <button onClick={() => savePDF()}>Save</button>;
}
```

### Direction 3: Both Layers → Shared State (direct import)

Both layers import `state` directly from `core/state.ts`:

```javascript
// CORRECT — either layer can import state directly
import { state } from '../core/state.js';

const currentPage = state.currentPage;
state.currentTool = 'highlight';
```

### Summary Table

| From | To | Method |
|------|----|--------|
| Vanilla JS | SolidJS stores | ALWAYS via `bridge.ts` |
| SolidJS components | Vanilla JS modules | Direct import |
| Either layer | `state` object | Direct import from `core/state.ts` |
| Either layer | Sub-stores | Direct import from `core/stores/*.ts` |

---

## Central State: core/state.ts

### createMutable Pattern

The central state uses SolidJS's `createMutable()`, which wraps a plain object in a deep reactive proxy. Mutations are tracked automatically:

```typescript
import { createMutable } from 'solid-js/store';

export const state = createMutable<AppState>({
  documents: [],
  activeDocumentIndex: -1,
  currentTool: 'hand',
  preferences: { ...DEFAULT_PREFERENCES },
  // ... app-level properties
});
```

**Rule: ALWAYS mutate `state` properties directly on the proxy object. NEVER replace the `state` reference itself.**

### Per-Document Delegation Pattern

The codebase is transitioning from single-document to multi-document. The `state` object provides backward-compatible getter/setter pairs that delegate to `state.documents[state.activeDocumentIndex]`:

```typescript
export const state = createMutable<AppState>({
  documents: [],
  activeDocumentIndex: -1,

  // Delegation: state.pdfDoc → active document's pdfDoc
  get pdfDoc() {
    const doc = this.documents[this.activeDocumentIndex];
    return doc ? doc.pdfDoc : null;
  },
  set pdfDoc(value) {
    const doc = this.documents[this.activeDocumentIndex];
    if (doc) doc.pdfDoc = value;
  },

  // ~30 more getter/setter pairs for: currentPage, scale, viewMode,
  // currentPdfPath, annotations, textEdits, watermarks, bookmarks,
  // redoStack, pageRotations, selectedAnnotation, measureScale, etc.
});
```

**Delegated properties** (route to active document):
`pdfDoc`, `currentPage`, `scale`, `viewMode`, `currentPdfPath`, `annotations`, `textEdits`, `watermarks`, `bookmarks`, `redoStack`, `pageRotations`, `selectedAnnotation`, `selectedAnnotations`, `measureScale`

**App-level properties** (live on state directly):
`documents`, `activeDocumentIndex`, `currentTool`, `toolOverrides`, `imageCache`, `modalDialogOpen`, `appMenuOpen`, `preferences`, `defaultAuthor`, `shiftKeyPressed`, `statusMessage`, `statusMessageVisible`, `textSelection`, `search`

---

## Sub-Stores

Three focused sub-stores handle high-frequency interaction state. Each uses its own `createMutable` and is proxied through the main `state` object via getter/setter delegation:

### interaction-store.ts

Handles drawing, dragging, panning, and rubber-band selection state:

```typescript
import { createMutable } from 'solid-js/store';

export const interactionState = createMutable<InteractionState>({
  isDrawing: false, startX: 0, startY: 0,
  currentPath: [], polylinePoints: [],
  isDragging: false, isResizing: false,
  isPanning: false, isMiddleButtonPanning: false,
  // ... 20+ properties
});
```

Proxied through `state`:
```typescript
get isDrawing() { return interactionState.isDrawing; },
set isDrawing(v) { interactionState.isDrawing = v; },
```

### editing-store.ts

Handles text editing and PDF text editing state:
- `isEditingText`, `editingAnnotation`, `textEditElement`
- `isEditingPdfText`, `pdfTextEditState`

### clipboard-store.ts

Handles clipboard operations:
- `annotation` (single), `annotations` (multi-selection)

### Import Pattern for Sub-Stores

New code SHOULD import sub-stores directly. The `state.*` proxy is for backward compatibility:

```typescript
// PREFERRED for new code
import { interactionState } from '../core/stores/interaction-store.js';
interactionState.isDrawing = true;

// ALSO WORKS (backward compat) — but adds indirection
import { state } from '../core/state.js';
state.isDrawing = true;  // Proxies to interactionState.isDrawing
```

---

## When to Add a New Bridge Export

Add a new export to `bridge.ts` when ALL of these conditions are true:

1. A **vanilla JS** file needs to call a **SolidJS store** function
2. The store function modifies **SolidJS signals or mutable state** used by components
3. No existing bridge export covers the use case

### Steps to Add

1. Create or update the SolidJS store in `js/solid/stores/`
2. Add the export to `bridge.ts` under the correct domain section
3. Use `as` aliases if the exported name would collide (e.g., `visible as findBarVisible`)
4. NEVER export raw signals — ALWAYS export getter/setter pairs or action functions

### When NOT to Use the Bridge

- SolidJS component calling another SolidJS store → direct import
- Any code reading/writing `state.*` → direct import from `core/state.ts`
- Vanilla JS calling other vanilla JS → direct import
- Pure utility functions with no store dependency → direct import

---

## SolidJS Store Pattern

All SolidJS stores in `js/solid/stores/` follow a consistent pattern using `createSignal`:

```javascript
// js/solid/stores/loadingStore.js
import { createSignal } from 'solid-js';

const [visible, setVisible] = createSignal(false);
const [message, setMessage] = createSignal('');

export { visible, setVisible, message, setMessage };
```

Key distinction:
- **SolidJS stores** (`js/solid/stores/`) use `createSignal` — fine-grained reactivity for UI
- **Core state** (`js/core/state.ts`) uses `createMutable` — coarse-grained proxy for shared access
- **Sub-stores** (`js/core/stores/`) use `createMutable` — domain-specific interaction state

---

## Ownership Rules

| Concern | Owner | Location |
|---------|-------|----------|
| UI rendering (DOM) | SolidJS | `js/solid/components/*.jsx` |
| UI state (signals) | SolidJS stores | `js/solid/stores/*.js` |
| PDF loading/saving | Vanilla JS | `js/pdf/*.js` |
| Canvas rendering | Vanilla JS | `js/annotations/rendering.js` |
| Tool dispatch | Vanilla JS | `js/tools/tool-dispatcher.js` |
| Annotation model | Shared (state) | `js/core/state.ts` → `documents[i].annotations` |
| App preferences | Shared (state) | `js/core/state.ts` → `preferences` |
| Interaction tracking | Sub-store | `js/core/stores/interaction-store.ts` |
| Clipboard | Sub-store | `js/core/stores/clipboard-store.ts` |
| Text editing flags | Sub-store | `js/core/stores/editing-store.ts` |

---

## Key Files Reference

| File | Lines | Role |
|------|-------|------|
| `js/bridge.ts` | ~240 | Facade: vanilla JS → SolidJS store access |
| `js/core/state.ts` | ~350 | Central mutable state with delegation |
| `js/core/stores/interaction-store.ts` | ~70 | Drawing/dragging/panning state |
| `js/core/stores/editing-store.ts` | ~40 | Text editing state |
| `js/core/stores/clipboard-store.ts` | ~20 | Clipboard state |
| `js/core/stores/document-helpers.ts` | varies | Document creation helpers |
| `js/core/stores/selection-helpers.ts` | varies | Selection management helpers |
| `js/solid/App.jsx` | varies | Root SolidJS component |
| `js/solid/stores/*.js` | varies | 17+ SolidJS signal-based stores |

