This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
FrameTrail is an open hypervideo environment for creating, annotating, and remixing interactive videos. It's a client-side JavaScript application with an optional PHP backend that uses JSON files instead of a database for all data storage. The entire system is portable — copy the _data directory between servers and everything works.
FrameTrail can run in four modes: with a PHP server (full multi-user), with the File System Access API for local editing in Chrome/Edge (no server needed), in-memory using the Download adapter (view + edit + export, no persistence — works everywhere but requires data to be passed via init options), or in static/CDN mode (read from a static host + in-memory edits + Save As export, no PHP backend needed).
All source code lives in src/. The build/ directory (git-ignored) contains production output.
FrameTrail/
├── src/ # ALL source code (runnable as-is for development)
│ ├── index.html # Player/editor entry point
│ ├── resources.html # Standalone resource manager
│ ├── setup.html # First-run setup wizard
│ ├── .htaccess # Apache rewrite rules
│ ├── favico.png
│ ├── _lib/ # Vendored third-party libraries (11 packages)
│ ├── _shared/
│ │ ├── frametrail-core/
│ │ │ ├── frametrail-core.js # Core: defineModule, defineType, init, state
│ │ │ ├── storage/ # StorageAdapter + Server/Local/Download adapters
│ │ │ ├── _templateModule.js # Module boilerplate template
│ │ │ └── _templateType.js # Type boilerplate template
│ │ ├── modules/ # 12 shared modules
│ │ ├── types/ # 23 resource type definitions
│ │ ├── styles/ # Global CSS (variables, generic, webfont)
│ │ └── fonts/ # Webfonts (woff2 only)
│ ├── player/
│ │ ├── modules/ # 18 player-specific modules
│ │ └── types/ # Player types (Annotation, Overlay, etc.)
│ ├── resourcemanager/
│ │ └── modules/ResourceManagerLauncher/
│ ├── _server/ # PHP backend
├── scripts/
│ └── build.sh # Production build script
├── .github/workflows/
│ ├── build.yml # CI: build verification on push/PR
│ └── release.yml # CD: package + GitHub Release on tags
├── docs/ # Developer documentation
├── build/ # Build output (git-ignored)
└── ... # README, LICENSE, CONTRIBUTING, etc.
Three HTML entry points in src/:
src/index.html— Main player/editor (bootstraps viaPlayerLaunchermodule)src/resources.html— Standalone resource manager (bootstraps viaResourceManagerLauncher)src/setup.html— Initial setup wizard (one-time)
Custom Module System (src/_shared/frametrail-core/frametrail-core.js):
FrameTrail.defineModule()— registers modules with init() and onChange() lifecycle methodsFrameTrail.defineType()— registers data types (Annotation, Overlay, Resource types, etc.)FrameTrail.changeState()/FrameTrail.getState()— global state management with change listeners- Multiple FrameTrail instances can coexist on one page
- No build step for development — all modules loaded directly via
<script>tags
Frontend:
src/_shared/frametrail-core/— Core framework and module loadersrc/_shared/frametrail-core/storage/— Storage adapters (Server, Local, Download)src/_shared/modules/— Shared modules (Database, UserManagement, ResourceManager, RouteNavigation, StorageManager, Localization, etc.)src/_shared/types/— Resource type definitions (29 types, all inherit from base Resource)src/player/modules/— Player modules (HypervideoModel, HypervideoController, AnnotationsController, OverlaysController, Interface, Titlebar, Sidebar, etc.)src/player/types/— Player types (Annotation, Overlay, Hypervideo, Subtitle, CodeSnippet, ContentView)src/_shared/styles/— Global CSS (variables.css, generic.css, frametrail-webfont.css)src/_shared/fonts/— Webfonts in woff2 format (FrameTrail icon font + Titillium Web)src/_lib/— Vendored third-party libraries
Backend:
src/_server/ajaxServer.php— Central AJAX endpoint (switch statement dispatcher)src/_server/user.php— User managementsrc/_server/files.php— File upload/downloadsrc/_server/hypervideos.php— Hypervideo CRUDsrc/_server/annotationfiles.php— Annotation persistencesrc/_server/config.php— Server configurationsrc/_server/functions.incl.php— Shared utility functions
Data Storage (_data/ directory — not in git, created at runtime):
_data/
├── config.json # Global app configuration
├── users.json # User accounts
├── tagdefinitions.json # Tag definitions
├── custom.css # Global custom styles
├── hypervideos/
│ ├── _index.json # Hypervideo registry
│ └── {hypervideoId}/
│ ├── hypervideo.json # Metadata, clips, overlays, config
│ ├── annotations/
│ │ ├── _index.json
│ │ └── {userId}.json # User annotations (W3C Web Annotation format)
│ └── subtitles/ # VTT subtitle files
└── resources/
├── _index.json # Resource registry
└── {files} # Uploaded media files
| Directory | Library | Notes |
|---|---|---|
tabsjs/ |
FTTabs (custom) | Pure vanilla-JS tab widget, API-compatible drop-in for the former jquery.tabs |
collisiondetection/ |
Collision Detection | Overlay collision |
interactjs/ |
Interact.js | Drag/drop and resize for overlay editing |
sortablejs/ |
SortableJS | Sortable lists |
fflate/ |
fflate | ZIP file creation for Save As / All Data export |
dialog/ |
dialog (custom) | Lightweight wrapper around native <dialog> |
leaflet/ |
Leaflet | Map rendering (OpenStreetMap) |
codemirror6/ |
CodeMirror 6 | Code editor (JS/CSS/HTML modes + linting) |
hlsjs/ |
HLS.js | Adaptive video streaming |
quill/ |
Quill | Rich text editing (replaces WYSIHTML5) |
parsers/ |
VTT parser | Subtitle parsing |
'server'— PHP backend available, data loaded/saved via AJAX tosrc/_server/ajaxServer.php'local'— File System Access API active, data read/written viaStorageAdapterLocalto a user-selected folder'needsFolder'— File System Access API supported but no folder selected yet; launcher prompts user to pick a_datadirectory'download'— No persistent storage available (Firefox/Safari, or any browser without File System Access API and no PHP);StorageAdapterDownloadis used, which stores data in memory and lets users export/download it. Viewing and editing work;canSaveisfalse(no persistent target); changes are exported via Save As. Data persists only until page reload.'static'— CDN/static hosting mode (no PHP backend).StorageAdapterStaticreads JSON from a CDN base URL (dataPathinit option) and inherits in-memory write + Save As export fromStorageAdapterDownload. Used whendataPathis set butserveris omitted.
Three primary modes controlled by state:
- Player Mode (
viewMode: 'video') — Video playback with annotations/overlays (read-only) - Editor Mode (
editMode: true) — Authenticated editing of annotations and overlays - Overview Mode (
viewMode: 'overview') — Gallery view of all hypervideos
State variables:
editMode(true/false) — Whether user is editingviewMode('video'/'overview') — Current view typestorageMode('server'/'local'/'needsFolder'/'download'/'static') — Active storage backenddataPath(string|null) — Base URL for_data/directory;null= auto-detectserver(string|null) — Base URL for_server/PHP directory;null= auto-detect or no serverslidePosition('middle'/'bottom'/'top') — Layout positioningsidebarOpen(true/false) — Sidebar visibilityfullscreen(true/false) — Fullscreen modeviewSize([width, height]) — Responsive layout dimensions
- Module Pattern: Modules return public interfaces, private state hidden in closures
- Observer Pattern: State changes trigger module
onChange(stateName, stateValue)callbacks - Lazy Loading: Modules initialized on-demand via
FrameTrail.initModule() - File-Based Persistence: All data stored as JSON files (no database)
- Event-Driven: Timeline events, user actions broadcast to listeners
- W3C Web Annotations: Uses standardized annotation format with FrameTrail extensions
- Guest Mode is orthogonal to storage mode:
UserManagement.isGuestMode()is an identity-layer flag — it means "editing without a server account", not "in download mode". A user can be in guest mode in any storage mode (local, download, or server).StorageManager.canSave()is the authoritative gate for save UI: it returnsfalsefor server mode when in guest mode, and delegates toadapter.canSaveotherwise (StorageAdapterDownload.canSaveis alwaysfalse;StorageAdapterLocal.canSaveistrue). Always usecanSave()rather than checkingisGuestMode()orstorageModedirectly in save-related UI.
Important: FrameTrail instance vs global:
- The global
FrameTrailobject is the factory/registry.FrameTrail.module(),FrameTrail.changeState(), etc. are only available on initialized instances (theFrameTrailparameter passed intodefineModulecallbacks). - Modules defined via
FrameTrail.defineModule()receive the instance as their closure argument — they can freely callFrameTrail.module('X'). - Plain classes (e.g.
StorageAdaptersubclasses insrc/_shared/frametrail-core/storage/) are not FrameTrail modules and do not have access to any instance. If they need to call module APIs, the caller must pass the FrameTrail instance explicitly. - Every module must be initialized with
FrameTrail.initModule('ModuleName')before it can be accessed viaFrameTrail.module('ModuleName'). Callingmodule()on an uninitialized module returns undefined.
Client → Server: All requests go through src/_server/ajaxServer.php with action parameter (e.g., userLogin, fileUpload, hypervideoAdd, annotationfileSave). Every request includes the dataPath parameter (an absolute URL path) so the PHP backend resolves the correct _data directory.
Client → Local: StorageAdapterLocal uses the File System Access API to read/write JSON files directly in the user's selected _data folder
Client → Download: StorageAdapterDownload enables exporting data as downloadable files when neither server nor File System Access API is available
Client State Management:
Databasemodule loads JSON files via the active storage adapter on app init- State changes via
FrameTrail.changeState()trigger module updates - Modules respond to state changes in their
onChange()method - User edits saved back via the storage adapter to update JSON files
All 23 resource types inherit from the base Resource type in src/_shared/types/Resource/:
| Type | Description |
|---|---|
| ResourceVideo | HTML5 video (with HLS.js support) |
| ResourceImage | Static images |
| ResourceAudio | HTML5 audio |
| ResourceYoutube | YouTube embeds |
| ResourceVimeo | Vimeo embeds |
| ResourceWistia | Wistia embeds |
| ResourceLoom | Loom embeds |
| ResourceTwitch | Twitch embeds |
| ResourceSoundcloud | SoundCloud embeds |
| ResourceSpotify | Spotify embeds |
| ResourceWebpage | Generic iframe embeds |
| ResourceWikipedia | Wikipedia article embeds |
| ResourcePDF | PDF document viewer |
| ResourceText | Rich text (WYSIWYG via Quill + HTML editor) |
| ResourceHtml | Raw HTML (CodeMirror HTML editor only, no sanitisation) |
| ResourceLocation | OpenStreetMap (via Leaflet) |
| ResourceQuiz | Interactive quiz |
| ResourceHotspot | Clickable hotspot |
| ResourceEntity | Linked data entity |
| ResourceMastodon | Mastodon embeds |
| ResourceCodepen | CodePen embeds |
| ResourceFigma | Figma embeds |
| ResourceUrlPreview | URL preview cards |
- PHP 7.4+ (for server mode — run
php -S localhost:8080insrc/, no Apache needed for local dev) - Or: Chrome/Edge for local folder mode (File System Access API)
- No build tools required for development — edit files directly in
src/
Development (with server):
- Run
php -S localhost:8080in thesrc/directory - Open
http://localhost:8080— first run opens setup wizard - Creates
src/_data/directory and admin account
Development (local folder mode):
- Open
src/index.htmldirectly in Chrome or Edge - Select or create a
_datafolder when prompted
# Install build tools (one-time)
npm install -g terser csso-cli
# Build
bash scripts/build.sh
# Build with version label
bash scripts/build.sh v2.0.0The build script:
- Concatenates all CSS files in load order →
build/frametrail.css - Inlines woff2 fonts as base64 data URIs into the CSS
- Concatenates all JS files in load order →
build/frametrail.js - Minifies with terser/csso →
build/frametrail.min.js+build/frametrail.min.css - Generates clean HTML entry points that load only the two bundles
- Copies
_server/,.htaccess,favico.png,LICENSE.md
- Build verification (
.github/workflows/build.yml): Runs on every push tomain/developand every PR. Builds and verifies output. - Release packaging (
.github/workflows/release.yml): Runs onv*tags. Builds, zips, creates GitHub Release with the zip attached.
To create a release:
git checkout main
git tag v2.0.0
git push origin v2.0.0
# CI creates the GitHub Release automaticallymain— Stable release branch, tagged for releasesdevelop— Integration branch, feature branches merge here- Feature branches created from
develop, merged back via PR
Adding/Modifying Modules:
- Modules located in
src/_shared/modules/orsrc/player/modules/ - Follow template in
src/_shared/frametrail-core/_templateModule.js - Register with
FrameTrail.defineModule('ModuleName', function() { ... }) - Add
<script>tag tosrc/index.html(andsrc/resources.htmlif shared) - Add CSS if needed (one stylesheet per module)
- Add files to
scripts/build.shinJS_FILES/CSS_FILESarrays
Adding/Modifying Types:
- Types located in
src/_shared/types/orsrc/player/types/ - Follow template in
src/_shared/frametrail-core/_templateType.js - Register with
FrameTrail.defineType('TypeName', function() { ... }) - Add
<script>and<link>tags to HTML files - Add files to
scripts/build.sh
Modifying Server Logic:
- Add new action to switch statement in
src/_server/ajaxServer.php - Implement logic in specialized PHP file
- Return JSON response with
success/errorkeys - Client-side: Call via
FrameTrail.module('Database').ajax(action, data, callback)
JavaScript:
- Module structure: Private vars/functions in closure, return public interface
- Lifecycle:
init()called once,onChange(changedState, stateValue)for state updates - No ES6 modules — uses global
FrameTrailnamespace - Plain DOM APIs throughout — jQuery has been fully removed
CSS:
- One stylesheet per module/type in same directory as JS
- Loaded in
<head>of HTML file - Global styles in
src/_shared/styles/ - For
<select>elements, wrap in a<div class="custom-select">to get consistent styled dropdowns with a caret icon (defined insrc/_shared/styles/generic.css)
Data Files:
- All stored in
_data/directory (not in git) - JSON format with pretty printing (
JSON_PRETTY_PRINT) - Registry files:
_index.jsonfiles list all items in directory - Direct file I/O in PHP — no database abstraction layer
Key Configuration (src/_server/config.php):
$conf["dir"]["data"]— Data directory location (default:../_data)- Session lifetime controlled by PHP
session.gc_maxlifetime
Data Directory Resolution (dataPath):
The PHP backend supports a client-provided dataPath parameter to select which _data directory to use. This allows multiple data directories to coexist under the same server root.
Resolution order:
- Request parameter (
$_REQUEST["dataPath"]) — sent by the client with every request - Default:
../_data(sibling of_server/, used when nodataPathis sent)
Security: The resolved path must pass three checks:
realpath()must succeed (no dangling symlinks or non-existent paths)- Must be a directory within the sandbox boundary (
dirname(__DIR__)= parent of_server/) - Must contain
config.json(validates it's a real FrameTrail data directory)
There is no session-based override — every request is validated independently. This allows multiple data directories to be used in parallel (e.g. in different browser tabs).
When adding new server actions: The dataPath parameter is handled centrally in config.php — no per-action code needed. All PHP files that use $conf["dir"]["data"] automatically get the correct path.
When adding new client-side server calls: Include dataPath in the POST body. For modules with a _serverPost() helper, this is automatic. For direct fetch() calls, get the value via FrameTrail.module('StorageManager').getAdapter().dataPathAbsolute.
Runtime Configuration (_data/config.json):
- Authentication settings
- Upload restrictions (file types, sizes)
- Theme/UI settings
- Default user roles and permissions
- Custom labels for UI elements
Overlays containing text-like content are scaled so they always render at a comfortable reading width regardless of how small the overlay is on screen. This is a JS-driven transform, not a CSS media query or container query.
How it works (src/player/types/Overlay/type.js → scaleOverlayElement()):
- Called by
rescaleOverlays()inOverlaysControllerwhenever the video/overlay container resizes (triggered fromViewVideo.adjustHypervideo()). - Applies only to these types:
wikipedia,webpage,text,quiz,mastodon,urlpreview. - Logic:
scaleBase= 400px (800px fortext)- If the overlay wrapper is wider than
scaleBase, scaling is reset (no transform applied — content fills normally) - Otherwise:
scale = wrapperWidth / scaleBase, then the.resourceDetailis set towidth: scaleBase,height: wrapperHeight * (1/scale), andtransform: translate(-50%, -50%) scale(scale)— so the content always renders at 400px wide but is visually scaled down to fit
- The
texttype uses the full overlay container width as the reference instead of the wrapper.
CSS complement (ResourceWebpage/style.css):
The iframe inside a webpage overlay gets an additional static zoom-out via width/height: 133% + transform: scale(0.75) so that the full-width iframe content fits within the 400px rendered container. This is layered on top of the JS scaling, not a replacement for it.
To add scaling to a new type: add its data.type string to the condition at the top of scaleOverlayElement(). Do NOT use CSS container queries or static CSS transforms as a substitute — the JS mechanism is the authoritative approach.
Language Files: src/_shared/modules/Localization/locale/
- Default:
en.js - German:
de.js - Add new languages by creating
{locale}.jsfiles - Important: Locale files are
.jsfiles (not.json) - Switch language via
FrameTrail.module('Localization').setLanguage(locale)
Configuring the language: Language is set via config.defaultLanguage in the init options — language is NOT a direct init option. The data-frametrail-language HTML attribute maps to config.defaultLanguage internally (handled in _autoInit). Example: FrameTrail.init({ config: { defaultLanguage: 'de' } }, 'PlayerLauncher').