| name | pdfstudio-core-solidjs-vanilla-bridge |
| description | 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.
|
| license | MIT |
| compatibility | Designed for Claude Code. Specific to open-pdf-studio (SolidJS 1.9, Tauri 2.10). |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
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
export { openDialog, closeDialog, getDialogs, showMessage }
from './solid/stores/dialogStore.js';
export { switchToTab as switchRibbonTab, activeTab as ribbonActiveTab,
getColorPickerValue, setColorPickerValue }
from './solid/stores/ribbonStore.js';
export { storeShowProperties, storeHideProperties, updateAnnotProp }
from './solid/stores/propertiesStore.js';
export { visible as loadingVisible, setVisible as setLoadingVisible }
from './solid/stores/loadingStore.js';
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:
import { showMessage, setLoadingVisible } from '../bridge.js';
setLoadingVisible(true);
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:
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:
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:
import { createMutable } from 'solid-js/store';
export const state = createMutable<AppState>({
documents: [],
activeDocumentIndex: -1,
currentTool: 'hand',
preferences: { ...DEFAULT_PREFERENCES },
});
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]:
export const state = createMutable<AppState>({
documents: [],
activeDocumentIndex: -1,
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;
},
});
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:
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,
});
Proxied through state:
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:
import { interactionState } from '../core/stores/interaction-store.js';
interactionState.isDrawing = true;
import { state } from '../core/state.js';
state.isDrawing = true;
When to Add a New Bridge Export
Add a new export to bridge.ts when ALL of these conditions are true:
- A vanilla JS file needs to call a SolidJS store function
- The store function modifies SolidJS signals or mutable state used by components
- No existing bridge export covers the use case
Steps to Add
- Create or update the SolidJS store in
js/solid/stores/
- Add the export to
bridge.ts under the correct domain section
- Use
as aliases if the exported name would collide (e.g., visible as findBarVisible)
- 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:
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 |