Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c8bbbfe
build(idevices): reuse centralized TypeScript build convention
claude Jul 30, 2026
3b85c3f
refactor(three-sixty-viewer): add typed shared schema and migration
claude Jul 30, 2026
ea17acc
refactor(three-sixty-viewer): migrate viewer runtime to TypeScript
claude Jul 30, 2026
ecaa468
refactor(three-sixty-viewer): migrate editor to TypeScript, improve h…
claude Jul 30, 2026
089d685
test(three-sixty-viewer): add bundle-contract and extended E2E coverage
claude Jul 30, 2026
55d1dac
docs(three-sixty-viewer): document the TypeScript architecture (SDD-0…
claude Jul 30, 2026
8c70545
test(three-sixty-viewer): raise editor coverage; exclude bundle entry…
claude Jul 30, 2026
ef53323
feat(three-sixty-viewer): align hotspot authoring UI with Interactive…
erseco Jul 30, 2026
fd42997
ci: re-run checks after cancelled workflow runs
erseco Aug 4, 2026
fde51dc
Merge branch 'main' into refactor/three-sixty-viewer-typescript
erseco Aug 31, 2026
17b5f83
fix(three-sixty-viewer): pagehide cleanup and tracking-issue architec…
erseco Aug 31, 2026
e8fdd8f
fix(three-sixty-viewer): narrow pagehide handler before calling it
erseco Aug 31, 2026
cf933c5
Merge branch 'main' into refactor/three-sixty-viewer-typescript
erseco Sep 1, 2026
0506d2c
Merge branch 'main' into refactor/three-sixty-viewer-typescript
erseco Sep 2, 2026
e3e4be5
Merge branch 'main' into refactor/three-sixty-viewer-typescript
erseco Sep 3, 2026
eba783a
Merge branch 'main' into refactor/three-sixty-viewer-typescript
erseco Sep 3, 2026
2118cb3
Merge branch 'main' into refactor/three-sixty-viewer-typescript
erseco Sep 3, 2026
6249323
Merge remote-tracking branch 'ate/main' into refactor/three-sixty-vie…
erseco Sep 5, 2026
77e5d72
Merge branch 'main' into refactor/three-sixty-viewer-typescript
erseco Sep 6, 2026
dda7b38
Merge branch 'main' into refactor/three-sixty-viewer-typescript
erseco Sep 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .agents/skills/idevice/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,25 @@ Creating or modifying interactive devices (iDevices) in `public/files/perm/idevi

**Reference iDevices** (well-tested, good to study): `checklist`, `rubric`, `geogebra-activity`

## TypeScript iDevices (`src/`)

An iDevice with a `src/` directory is a **TypeScript iDevice**: its
`edition/<name>.js` and `export/<name>.js` are GENERATED bundles (gitignored)
— never edit them; edit `src/` and rebuild. Convention and commands:

- `src/edition/index.ts` → `edition/<name>.js` (assigns `window.$exeDevice`);
`src/export/index.ts` → `export/<name>.js` (assigns the runtime global).
- Build/typecheck: `bun run bundle:idevices` / `bun run typecheck:idevices`
(central runner `scripts/build-idevices.ts`; `--only <name>`, `--watch`).
Run `make bundle` after src/ edits and BEFORE E2E, or the preview serves the
stale bundle from `public/bundles/idevices.zip`.
- Tests are colocated `*.spec.ts` (Vitest — `bun test` ignores `public/**`),
plus bundle-contract smoke tests over the compiled IIFEs.
- Deviations (custom bundle name, externals, minify) go in an optional
`build.config.json` — see `doc/development/idevices-typescript.md` and
ADR-2147-01. Reference implementations: `three-sixty-viewer` (full convention),
`slide` (manifest).

## Structure

```
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ jobs:
- name: Build all assets once
run: bun run build:static

# The path list must include every GENERATED (gitignored) file the
# workarea serves — the test runners get a fresh checkout, so anything
# missing here 404s at runtime (e.g. TypeScript-iDevice bundles, ADR-2147-01).
- name: Upload dynamic bundles (chromium/firefox)
uses: actions/upload-artifact@v7
with:
Expand All @@ -65,6 +68,8 @@ jobs:
public/bundles/**
public/style/workarea/main.css
public/files/perm/idevices/base/slide/edition/slide-editor.bundle.js
public/files/perm/idevices/base/three-sixty-viewer/edition/three-sixty-viewer.js
public/files/perm/idevices/base/three-sixty-viewer/export/three-sixty-viewer.js

- name: Upload static distribution (static project)
uses: actions/upload-artifact@v7
Expand Down
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,12 @@ public/app/dist/
/app/dist/
/app/node_modules/

# Slide iDevice — pre-built editor bundle (regenerated by package.json postinstall)
# TypeScript iDevice bundles — generated from each iDevice's src/ by scripts/build-idevices.ts
/public/files/perm/idevices/base/slide/edition/slide-editor.bundle.js
/public/files/perm/idevices/base/three-sixty-viewer/edition/three-sixty-viewer.js
/public/files/perm/idevices/base/three-sixty-viewer/edition/three-sixty-viewer.js.map
/public/files/perm/idevices/base/three-sixty-viewer/export/three-sixty-viewer.js
/public/files/perm/idevices/base/three-sixty-viewer/export/three-sixty-viewer.js.map
.omc/

# Live-LMS harness working directories (see test/e2e/moodle/README.md)
Expand Down
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ These apply to every skill below. If you cannot meet them, stop and ask the user
| `src/**/*.ts` | Backend | `bun test` | `make fix` |
| `public/app/**/*.js` | Frontend | `vitest` | `make fix` |
| `public/files/perm/idevices/**` | iDevice | `vitest` | `make fix` |
| `public/files/perm/idevices/*/src/**` | iDevice (TypeScript, see [ADR-2147-01](doc/architecture/adr/ADR-2147-01-typescript-idevices-build-convention.md)) | `vitest` (`*.spec.ts`) | `make fix` |
| `src/shared/export/**` | Exporter | `bun test` | `make fix` |
| `src/db/migrations/**` | Database | `bun test` | `make fix` |
| `src/routes/api/v1/**` | API v1 | `bun test` | `make fix` |
Expand Down Expand Up @@ -91,6 +92,7 @@ make up # Docker dev environment
| `public/app/**/*.test.js` | Frontend unit tests | `vitest` |
| `public/libs/**/*.test.js` | Frontend lib tests | `vitest` |
| `public/files/perm/idevices/**/*.test.js` | iDevice tests | `vitest` |
| `public/files/perm/idevices/*/src/**/*.spec.ts` | TypeScript iDevice tests | `vitest` |
| `test/e2e/playwright/specs/*.spec.ts` | E2E tests | `playwright` |

### 5.2 Running Individual Tests
Expand Down Expand Up @@ -352,6 +354,7 @@ Domain-specific guidance lives in `.agents/skills/*/SKILL.md`.
| REST API v1 | [doc/development/rest-api.md](doc/development/rest-api.md) |
| Embedding in LMS | [doc/development/embedding.md](doc/development/embedding.md) |
| Profiling | [doc/development/profiling.md](doc/development/profiling.md) |
| TypeScript iDevices | [doc/development/idevices-typescript.md](doc/development/idevices-typescript.md) |
| Styles/Themes | [doc/development/styles.md](doc/development/styles.md) |
| Conventions | [doc/conventions.md](doc/conventions.md) |
| Architecture | [doc/architecture.md](doc/architecture.md) |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
---
id: ADR-2147-01
title: "TypeScript iDevices: src/ sources compiled by one convention-based build"
status: Proposed
date: 2026-07-30
tracking_issue: 2147
deciders:
- "@erseco"
reviewers:
- "@mnunezcedec"
- "@cristinavaldera"
related:
prs: [2147, 39]
changes:
- "39-three-sixty-viewer-typescript"
adrs: []
supersedes: []
superseded_by: []
ai_assistance:
tool: "Claude Code"
model: "claude-fable-5"
---

# ADR-2147-01: TypeScript iDevices: src/ sources compiled by one convention-based build

## Context

iDevices are classic-script objects loaded by the workarea and the exporters.
Historically each one is hand-written vanilla JavaScript committed directly
under `edition/` and `export/`. Two iDevices now keep their maintained source
in TypeScript instead — Slide (`src/` + a bespoke `scripts/build-slide-editor.ts`)
and Interactive Video (`src/` + a bespoke `scripts/build-interactive-video.ts`).
Two per-iDevice build scripts with duplicated Bun plumbing were already
diverging in flags and behaviour, and every future TypeScript iDevice would
have added another copy plus more package.json entries.

## Problem

How does the repository recognise, build, type-check and test an iDevice whose
maintained source is TypeScript, without a new build pipeline per iDevice?

## Decision drivers

- One obvious convention for the next TypeScript iDevice (zero new scripts).
- The shipped output must remain plain classic-script IIFEs; the language and
compile step are not a framework.
- Generated artifacts must never be committed; a clean checkout must
regenerate them through the existing pipeline (`build:all` / `make bundle`).
- Existing iDevices with special needs (Slide) must fit without renaming their
shipped bundles.

## Decision

**An iDevice that keeps a `src/` directory is a TypeScript iDevice**, built by
the centralized `scripts/build-idevices.ts`:

- **Convention:** `src/edition/index.ts` → `edition/<name>.js` and
`src/export/index.ts` → `export/<name>.js` — self-contained IIFEs
(`target: browser`, linked source maps, unminified), whose entry points
explicitly assign their window globals (`$exeDevice`, `$<name>`).
- **Escape hatch:** an optional `build.config.json` next to `config.xml`
replaces the convention for that iDevice (custom entries/naming/globalName/
minify/sourcemap, plus `externals` mapping bare imports to page-provided
globals so vendored libraries are never inlined). Slide uses it.
- **Type checking:** each TypeScript iDevice ships its own `tsconfig.json`
(strict for new code); the runner executes `tsc -p` for every one it finds.
- **Tests:** colocated `*.spec.ts` next to each module, run by **Vitest**
(`bun test` ignores `public/**`), plus bundle-contract smoke tests that
evaluate the compiled IIFEs.
- **Artifacts:** generated bundles and source maps are gitignored;
`build:all` runs `typecheck:idevices` + `bundle:idevices` before
`bundle:resources` (export bundles ship inside `idevices.zip`).

Package scripts: `typecheck:idevices`, `bundle:idevices`,
`bundle:idevices:watch`; the runner accepts `--only <names>` and `--watch`.

## Options considered

### Option 1: One bespoke build script per TypeScript iDevice (status quo)

Pros: each script is trivially readable. Cons: duplicated plumbing, per-iDevice
package.json entries, drift between scripts (they already differed in
sourcemaps, watch support and failure reporting).

### Option 2: Convention-based central runner + per-iDevice manifest (chosen)

Pros: the next TypeScript iDevice needs no build changes at all; one place to
fix bundler behaviour; deviations are declared, not programmed. Cons: one more
convention to know; the manifest is a small new format (documented in the
runner header and `doc/development/idevices-typescript.md`).

## Consequences

### Positive

- Adding a TypeScript iDevice = create `src/edition|export/index.ts` (+ a
strict `tsconfig.json`); building, type-checking and watching come for free.
- Slide and Interactive Video share one build path; Slide's output stayed
byte-identical apart from the generic externals shim's message strings.

### Negative

- A hidden convention: `src/` now has meaning. Mitigated by this ADR,
`doc/development/idevices-typescript.md` and the idevice skill.

### Neutral

- Classic-script iDevices are untouched; nothing forces a migration.

## Validation

- `scripts/build-idevices.spec.ts` covers discovery, the convention, the
manifest and its validation against the real repository state.
- `bun run build:all` exercises typecheck + build for every TypeScript
iDevice on every bundle/test target.

## References

- `scripts/build-idevices.ts` (runner; manifest schema in its header).
- `doc/development/idevices-typescript.md` (developer guide).
- PR [#2147](https://github.com/exelearning/exelearning/pull/2147).
145 changes: 145 additions & 0 deletions doc/architecture/changes/39-three-sixty-viewer-typescript/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
---
tracking_issue: 39
title: "360° Viewer iDevice: TypeScript refactor on the centralized build convention"
status: implemented
date: 2026-07-30
authors:
- "@erseco"
reviewers: []
implementation_prs: [39]
related_adrs: [ADR-2147-01]
supersedes: []
superseded_by: []
ai_assistance:
tool: "Claude Code"
model: "Claude"
---

# 360° Viewer iDevice: TypeScript refactor on the centralized build convention — design

## Summary

The 360° Viewer (`public/files/perm/idevices/base/three-sixty-viewer/`) moves
its maintained source from two hand-written classic scripts
(`edition/three-sixty-viewer.js`, `export/three-sixty-viewer.js`) to a typed,
modular `src/` tree compiled by the centralized TypeScript-iDevice build
([ADR-2147-01](../../adr/ADR-2147-01-typescript-idevices-build-convention.md)).
The generic conventions — discovery, bundling, typecheck, testing, gitignored
bundles — are documented in
[doc/development/idevices-typescript.md](../../../development/idevices-typescript.md);
this record covers only what is specific to the 360° Viewer.

## Source architecture

```text
src/
├── globals.d.ts # THREE / eXeLearning / _ ambient declarations
├── shared/ # pure, DOM-free logic used by BOTH bundles
│ ├── types.ts # v1/v2 document model, hotspot-action union
│ ├── schema.ts # hydrateDocument / serializeDocument
│ ├── migration.ts # v1 → v2 lift
│ ├── normalization.ts# idempotent v2 normalization + defaults
│ ├── hotspot-actions.ts # per-action normalize/serialize/validate/repair
│ ├── geometry.ts # yaw/pitch ↔ direction, letterbox math, NDC
│ ├── ids.ts, urls.ts, html.ts
├── viewer/ # browser layer shared by preview and runtime
│ ├── panorama-renderer.ts, flat-image-renderer.ts, hotspot-renderer.ts
│ ├── scene-controller.ts, controls.ts, lifecycle.ts, assets.ts, types.ts
├── edition/ # window.$exeDevice (editor)
│ ├── index.ts, device.ts, editor.ts, state.ts, form.ts
│ ├── scene-list.ts, scene-editor.ts, hotspot-list.ts, hotspot-editor.ts
│ ├── hotspot-placement.ts, preview.ts, asset-picker.ts, three-loader.ts
├── export/ # window.$threesixtyviewer (learner runtime)
│ ├── index.ts, runtime.ts, renderer.ts, instance.ts, modal.ts, actions.ts
└── test/ # THREE mock harness, bundle-contract, fixtures
```

Before the refactor, edition and export each carried a full copy of the state
normalization and letterbox geometry ("mirror edition/three-sixty-viewer.js"
comments in the legacy bundles). `src/shared/` is now the single source of
truth for both.

## Persisted formats and compatibility

- **v1** (original single-image shape: top-level `src`, `alt`, `initialView`,
`autorotate`, `zoomEnabled`, `fullscreenEnabled`, `showNavControls`) is
never written any more but remains readable; `hydrateDocument()` lifts it
into a one-scene v2 tour with nothing lost. Detection mirrors the legacy
checks exactly.
- **v2** (`version: 2`, `ideviceId`, `startSceneId`, `scenes[]`, `behaviour`)
is unchanged by this refactor: same property names, same ranges, same enum
values, same hotspot actions (`goToScene`, `text`, `image`, `video`,
`link`). The persisted `version` property stays `version: 2`.
- **Future versions** (`version > 2`) are rejected explicitly
(`status: 'unsupported-version'`): the editor shows a notice and `save()`
returns the ORIGINAL payload untouched; the runtime renders an accessible
notice. Unknown hotspot ACTION types inside a v2 document are preserved as
`{ type: 'unsupported', originalType, originalPayload }` in memory and
serialized back verbatim — opening and saving old or future content never
destroys data.

## Runtime contracts

- `edition/three-sixty-viewer.js` (generated) assigns
`globalThis.$exeDevice` on every evaluation — the workarea re-runs the
script for each edit session. Contract: `init(element, previousData,
idevicePath)`, `save(): document | false`, `destroy()`.
- `export/three-sixty-viewer.js` (generated) assigns
`globalThis.$threesixtyviewer` with the JSON-iDevice engine API
(`renderView` / `renderBehaviour` / `init`) used by
`public/app/common/exe_export.js`.
- three.js and OrbitControls stay EXTERNAL vendored files
(`export/three.min.js`, `export/OrbitControls.js`, declared in
`config.xml`'s `<export-js>`). Neither bundle inlines them; the bundles
only dereference `THREE` when a viewer is actually built
(`renderBehaviour()` / preview construction), so bundle evaluation order
relative to the vendor scripts is not critical, and the editor lazy-loads
them (`edition/three-loader.ts`) for its preview. This is asserted by the
bundle-contract tests.

## Lifecycle

Every runtime viewer is one instance in a `WeakMap`-backed registry keyed by
its wrapper element. An instance owns its scene controller, panorama/flat
renderers, hotspot layer, nav/fullscreen controls, animation frame, resize
observer, drag blockers and modal; `destroy()` releases all of them (LIFO
disposer bag) and re-rendering a node disposes its predecessor first.
Multiple viewers per page never share state. The editor mirrors the same
pattern: one Editor per `init()`, destroyed on re-init.

The export bundle binds `pagehide` (not `beforeunload`) so SCORM 1.2 packages
stay eligible for the back/forward cache. A persisted `pagehide` leaves
WebGL contexts intact; a real teardown calls `destroyAll()`.

## Hotspot placement

Direct placement is an additional authoring path next to list-based creation:
an explicit "Place hotspot by clicking" toggle (`aria-pressed`, visible hint,
aria-live announcements, Escape cancels), then one click on the preview.
Equirectangular scenes unproject the click through the camera to yaw/pitch
(`shared/geometry.ts` + `viewer/panorama-renderer.ts`); flat scenes convert
the click to percentages of the `object-fit: contain` rectangle, and clicks
on letterbox bars are ignored rather than snapped to an edge. Numeric fields
remain available for precise adjustment. Deleting a scene referenced by
`goToScene` hotspots asks for confirmation, states how many hotspots are
affected, and clears their targets deterministically (flagged inline until
retargeted).

## Testing

- Colocated `*.spec.ts` (Vitest, happy-dom) next to every module; three.js is
injected as a structural mock (`src/test/helpers.ts`), frames are stepped
manually.
- `src/test/bundle-contract.spec.ts` evaluates the real generated IIFEs and
asserts the window globals and their public APIs.
- `test/e2e/playwright/specs/idevices/three-sixty-viewer.spec.ts` covers the
authoring flows (scenes, hotspots, placement, persistence) and the bundle
contracts in a real browser.
- Fixtures for v1, v2, future-version and invalid payloads live in
`src/test/fixtures/`.

## ADRs required or referenced

- [ADR-2147-01](../../adr/ADR-2147-01-typescript-idevices-build-convention.md) —
TypeScript iDevices build convention (reused, no new durable decision
introduced by this refactor).
Loading
Loading