Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
e29a045
Init Decorator API in visual
JayPanoz Apr 27, 2026
eb6910d
Fix CSS updates for non-highlight API
JayPanoz Apr 28, 2026
4819b1d
Update import of sML
JayPanoz Apr 28, 2026
5cba77a
Add Decoration Style types
JayPanoz Apr 28, 2026
4ae3fc7
Templating
JayPanoz Apr 28, 2026
9843e7d
Implement mask
JayPanoz Apr 28, 2026
3dd7340
Add support for vertical-writing
JayPanoz Apr 29, 2026
a53b04a
MoreProtect against locations
JayPanoz Apr 29, 2026
3caa25d
Pass Locator to TextSelection
JayPanoz Apr 29, 2026
ecd8e91
Update BasicTextSelection Locator
JayPanoz Apr 29, 2026
180efbd
Make enforceContrast optional
JayPanoz Apr 30, 2026
d9146db
Improve Mutation observing in Snapper
JayPanoz May 4, 2026
1d04dd3
Clean up Service noop
JayPanoz May 4, 2026
a7888f4
Update Readium CSS JSON usage
JayPanoz May 4, 2026
039050b
Fix path of Bounds/Width options
JayPanoz May 5, 2026
08e2755
Handle outline in page/viewport Width
JayPanoz May 5, 2026
46d68b2
Update Underline for layout bounds
JayPanoz May 5, 2026
efea862
Remove MaskBlock type
JayPanoz May 5, 2026
f15fcb1
Correct path for textColor type
JayPanoz May 5, 2026
f110777
Handle text color width
JayPanoz May 5, 2026
e001b3d
Handle vertical-writing mode
JayPanoz May 5, 2026
90e0f8a
Adress gaps in implementation
JayPanoz May 6, 2026
9ec98b6
Correct textColor Bounds and Page
JayPanoz May 6, 2026
d84d4d9
Respect tint for mask type
JayPanoz May 6, 2026
49f3931
Merge branch 'develop' into decorator-api
JayPanoz May 7, 2026
c1c2968
Merge branch 'develop' into decorator-api
JayPanoz May 11, 2026
89a0f0e
Merge branch 'develop' into decorator-api
JayPanoz May 21, 2026
7ddd373
Merge branch 'develop' into decorator-api
JayPanoz May 27, 2026
44a9a97
Extract Decorator
JayPanoz Jun 2, 2026
a134b0f
Update config
JayPanoz Jun 2, 2026
b82530e
Add doc
JayPanoz Jun 2, 2026
7b096be
Merge branch 'develop' into standalone-decorator
JayPanoz Jun 4, 2026
c67dcea
Merge branch 'develop' into standalone-decorator
JayPanoz Jun 23, 2026
f4835df
Extract color helpers
JayPanoz Jun 23, 2026
2312112
Check if range sits inside inline SVG
JayPanoz Jun 23, 2026
305589b
Skip ruby for underline
JayPanoz Jun 23, 2026
09f6dc8
Add hover state
JayPanoz Jun 23, 2026
de0509d
Add expand prop
JayPanoz Jun 23, 2026
f1331a1
Add strikethrough style type
JayPanoz Jun 23, 2026
2916c8a
Merge rects for hit zones
JayPanoz Jun 23, 2026
417c6d8
Fix supportsDecorationStyle
JayPanoz Jun 23, 2026
a612070
Fix underline in vertical writing
JayPanoz Jun 23, 2026
b4b3061
Uncast type from experimentalHighlights
JayPanoz Jun 23, 2026
220cc9e
Align DecorationController
JayPanoz Jun 23, 2026
802d1b4
Ungate events
JayPanoz Jun 24, 2026
73d813a
Rename hovered events
JayPanoz Jun 24, 2026
a596d03
Allow per-decoration tint in CSS Highlight API
JayPanoz Jun 24, 2026
b8e6669
Update docs
JayPanoz Jun 25, 2026
ad18402
Remove isActive
JayPanoz Jun 25, 2026
5a343fa
Update return type of requireContainer
JayPanoz Jun 25, 2026
07b6652
Correct docs
JayPanoz Jun 25, 2026
8bb7145
Improve events
JayPanoz Jun 25, 2026
3359f47
Add HighlightUnderline built-in
JayPanoz Jun 25, 2026
31a9893
Fix type of supportsDecorationStyle arg
JayPanoz Jun 30, 2026
54b5d36
Fix expand
JayPanoz Jun 30, 2026
2db7612
Adjust built-ins for bounds
JayPanoz Jun 30, 2026
d5d374d
Fine tune "slashthrough"
JayPanoz Jun 30, 2026
84a4c33
Improve events
JayPanoz Jun 30, 2026
a74783c
Update docs
JayPanoz Jun 30, 2026
d8e4aea
Make onDecorationActivated optional
JayPanoz Jun 30, 2026
2843b29
Update controller for parity
JayPanoz Jun 30, 2026
ea20a69
Update ReadMe of Decorator
JayPanoz Jun 30, 2026
3274046
Compare text
JayPanoz Jul 1, 2026
ef980f0
Consolidate Decorator types and helpers
JayPanoz Jul 1, 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
6 changes: 6 additions & 0 deletions decorator/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*.log
.DS_Store
node_modules
dist
types
coverage
131 changes: 131 additions & 0 deletions decorator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# @readium/decorator

Renders visual annotations over text ranges on a page — highlights, underlines, strikethroughs, and more.

## Installation

```ts
import { DirectCommsChannel, Decorator, DecorationController, DecorationStyleType } from "@readium/decorator";
```

## Usage

```ts
// 1. Create the comms channel
const channel = new DirectCommsChannel();

// 2. Mount the Decorator module on the page
const decorator = new Decorator();
decorator.mount(window, channel.frame);

// 3. Create the controller
const ctrl = new DecorationController(channel.host);

// 4. Check style support before applying (TextColor requires the CSS Highlight API)
if (ctrl.supportsDecorationStyle(DecorationStyleType.TextColor)) {
// safe to use
}

// 5. Apply decorations — call again with a new array to update, empty array to clear
ctrl.applyDecorations([
{
id: "tts-0",
locator: /* Locator for the text range to decorate */,
style: { type: DecorationStyleType.Highlight, tint: "#FFFF00" },
}
], "tts");

// 6. Cleanup
decorator.unmount(window, channel.frame);
ctrl.destroy();
channel.frame.destroy();
```

## API

### `DecorationController`

```ts
class DecorationController {
constructor(host: DirectCommsHost, config?: DecorationControllerConfig)

// Returns true if the given style ID can be rendered.
// Returns false for TextColor when the CSS Highlight API is unavailable.
// Returns true for any ID registered in config.decorationTemplates.
supportsDecorationStyle(styleTypeId: string): boolean

// Replaces all decorations for a group. Diffs against previous state.
applyDecorations(decorations: Decoration[], group: string): void

// Registers an observer for a group.
// Activation is enabled for the group only when the observer declares onDecorationActivated.
// Hover tracking is enabled when the observer declares onDecorationPointerEnter or onDecorationPointerLeave.
registerDecorationObserver(group: string, observer: DecorationObserver): void
unregisterDecorationObserver(observer: DecorationObserver): void

destroy(): void
}

interface DecorationControllerConfig {
// Named style templates. Register a template here and reference it by ID in decoration styles.
decorationTemplates?: Record<string, HTMLDecorationTemplate>;
}
```

### `DecorationObserver`

```ts
interface DecorationObserver {
// Called when a decoration is tapped/clicked. Return true to consume the event.
// Activation is enabled for the group only when this method is declared.
onDecorationActivated?(event: OnDecorationActivatedEvent): boolean;

// Called when the pointer enters a decoration.
// Registering either hover method automatically enables hover tracking for the group.
onDecorationPointerEnter?(event: OnDecorationPointerEnterEvent): void;

// Called when the pointer leaves a decoration.
// rect is absent if the decoration was removed from the DOM before leave fired.
// point is the current pointer position at the moment of leave.
onDecorationPointerLeave?(event: OnDecorationPointerLeaveEvent): void;
}
```

### `DirectCommsChannel`

Connects the `DecorationController` to the `Decorator` module in the same process.

```ts
const channel = new DirectCommsChannel();
channel.frame // pass to Decorator.mount
channel.host // pass to DecorationController constructor
```

### `Decorator`

Mounts and unmounts the decoration renderer on a page.

```ts
class Decorator {
mount(wnd: Window, comms: IComms): boolean
unmount(wnd: Window, comms: IComms): boolean
}
```

## Types

| Name | Notes |
|------|-------|
| `Decoration` | `{ id, locator, style, extras? }` |
| `DecorationStyle` | `BuiltinDecorationStyle \| HTMLDecorationTemplate \| NamedDecorationStyle` |
| `BuiltinDecorationStyle` | `{ type?, tint?, layout?, width?, enforceContrast?, expand? }` |
| `HTMLDecorationTemplate` | `{ type: "template", layout, width, element, stylesheet? }` — `element` is a function `(decoration) => string`, resolved to HTML per decoration before rendering |
| `NamedDecorationStyle` | `{ type: string }` — reference to a style registered in `DecorationControllerConfig.decorationTemplates` |
| `DecorationStyleType` | `"highlight" \| "highlightUnderline" \| "underline" \| "strikethrough" \| "outline" \| "textColor" \| "mask" \| "template"` |
| `DecorationLayout` | `"boxes" \| "bounds"` |
| `DecorationWidth` | `"wrap" \| "viewport" \| "bounds" \| "page"` |
| `DecorationObserver` | `{ onDecorationActivated?, onDecorationPointerEnter?, onDecorationPointerLeave? }` |
| `OnDecorationActivatedEvent` | `{ decoration, group, rect, point }` |
| `OnDecorationPointerEnterEvent` | `{ decoration, group, rect, point }` |
| `OnDecorationPointerLeaveEvent` | `{ decoration, group, rect?, point? }` — rect absent if decoration removed from DOM before leave fired |
| `IComms` | Interface for the comms channel module side |
48 changes: 48 additions & 0 deletions decorator/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "@readium/decorator",
"version": "1.0.0",
"type": "module",
"description": "Standalone decoration controller for Readium publications",
"author": "readium",
"repository": {
"type": "git",
"url": "git+https://github.com/readium/ts-toolkit.git",
"directory": "decorator"
},
"license": "BSD-3-Clause",
"bugs": {
"url": "https://github.com/readium/ts-toolkit/issues"
},
"homepage": "https://github.com/readium/ts-toolkit",
"main": "./dist/index.umd.cjs",
"module": "./dist/index.js",
"types": "./types/src/index.d.ts",
"exports": {
".": {
"types": "./types/src/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.umd.cjs"
}
},
"files": [
"dist",
"src",
"types"
],
"engines": {
"node": ">=18"
},
"scripts": {
"clean": "rimraf types dist",
"build": "pnpm clean && tsc && vite build"
},
"devDependencies": {
"@readium/helpers": "workspace:*",
"@readium/navigator-html-injectables": "workspace:*",
"@readium/shared": "workspace:*",
"rimraf": "^6.1.2",
"tslib": "^2.8.1",
"typescript": "^5.9.3",
"vite": "^7.3.1"
}
}
16 changes: 16 additions & 0 deletions decorator/src/Decoration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { DecorationActivatedEvent, DecorationPointerEnterData, DecorationPointerLeaveData } from "@readium/navigator-html-injectables";
import type { Decoration } from "./styles.ts";

export interface OnDecorationActivatedEvent<D = Decoration> extends Omit<DecorationActivatedEvent, "decorationId"> {
decoration: D;
}

export type OnDecorationPointerEnterEvent<D = Decoration> = Omit<DecorationPointerEnterData, "decorationId"> & { decoration: D };

export type OnDecorationPointerLeaveEvent<D = Decoration> = Omit<DecorationPointerLeaveData, "decorationId"> & { decoration: D };

export interface DecorationObserver<D = Decoration> {
onDecorationActivated?(event: OnDecorationActivatedEvent<D>): boolean;
onDecorationPointerEnter?(event: OnDecorationPointerEnterEvent<D>): void;
onDecorationPointerLeave?(event: OnDecorationPointerLeaveEvent<D>): void;
}
93 changes: 93 additions & 0 deletions decorator/src/comms/direct.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import type { IComms, CommsCallback } from "@readium/navigator-html-injectables";

type AckFn = (ok: boolean) => void;
type EventListener = (data: unknown) => void;

export class DirectCommsChannel {
readonly frame: DirectCommsFrame;
readonly host: DirectCommsHost;

constructor() {
this.frame = new DirectCommsFrame(this);
this.host = new DirectCommsHost(this);
}
}

export class DirectCommsFrame implements IComms {
private registrar = new Map<string, { module: string; cb: CommsCallback }[]>();

constructor(private readonly channel: DirectCommsChannel) {}

register(key: string | string[], module: string, callback: CommsCallback): void {
const keys = Array.isArray(key) ? key : [key];
keys.forEach(k => {
const listeners = this.registrar.get(k) ?? [];
const existing = listeners.find(l => l.module === module);
if (existing) throw new Error(`Duplicate callback for "${k}" in module "${module}"`);
listeners.push({ module, cb: callback });
this.registrar.set(k, listeners);
});
}

unregister(key: string | string[], module: string): void {
const keys = Array.isArray(key) ? key : [key];
keys.forEach(k => {
const ls = this.registrar.get(k);
if (!ls) return;
this.registrar.set(k, ls.filter(l => l.module !== module));
});
}

unregisterAll(module: string): void {
this.registrar.forEach((ls, k) => {
this.registrar.set(k, ls.filter(l => l.module !== module));
});
}

_dispatch(key: string, data: unknown, ack: AckFn): void {
const ls = this.registrar.get(key);
if (!ls?.length) { ack(false); return; }
ls.forEach(l => l.cb(data, ack));
}

send(key: string, data: unknown): void {
this.channel.host._receive(key, data);
}

log(...data: unknown[]): void {
this.channel.host._receive("log", data);
}

readonly ready = true;

destroy(): void {
this.registrar.clear();
}
}

export class DirectCommsHost {
private listeners = new Map<string, EventListener[]>();

constructor(private readonly channel: DirectCommsChannel) {}

send(key: string, data: unknown, callback?: AckFn): void {
this.channel.frame._dispatch(key, data, callback ?? (() => {}));
}

on(key: string, cb: EventListener): void {
const ls = this.listeners.get(key) ?? [];
ls.push(cb);
this.listeners.set(key, ls);
}

off(key: string, cb: EventListener): void {
const ls = this.listeners.get(key);
if (ls) this.listeners.set(key, ls.filter(l => l !== cb));
}

_receive(key: string, data: unknown): void {
this.listeners.get(key)?.forEach(cb => cb(data));
}

readonly ready = true;
}
Loading