@texuli/deck 0.0.0-stage → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +183 -2
- package/dist/authoring/builder.d.ts +351 -0
- package/dist/authoring/builder.js +64 -0
- package/dist/authoring/index.d.ts +6 -0
- package/dist/authoring/index.js +6 -0
- package/dist/authoring/lint.d.ts +36 -0
- package/dist/authoring/lint.js +325 -0
- package/dist/authoring/outlines.d.ts +34 -0
- package/dist/authoring/outlines.js +255 -0
- package/dist/authoring/patch.d.ts +23 -0
- package/dist/authoring/patch.js +166 -0
- package/dist/authoring/select-layouts.d.ts +19 -0
- package/dist/authoring/select-layouts.js +90 -0
- package/dist/engine/auto-slide.d.ts +32 -0
- package/dist/engine/auto-slide.js +67 -0
- package/dist/engine/canvas.d.ts +56 -0
- package/dist/engine/canvas.js +48 -0
- package/dist/engine/controller.d.ts +126 -0
- package/dist/engine/controller.js +248 -0
- package/dist/engine/controls.d.ts +128 -0
- package/dist/engine/controls.js +460 -0
- package/dist/engine/hash.d.ts +42 -0
- package/dist/engine/hash.js +86 -0
- package/dist/engine/index.d.ts +88 -0
- package/dist/engine/index.js +88 -0
- package/dist/engine/messages.d.ts +138 -0
- package/dist/engine/messages.js +140 -0
- package/dist/engine/motion.d.ts +64 -0
- package/dist/engine/motion.js +112 -0
- package/dist/engine/navigation.d.ts +87 -0
- package/dist/engine/navigation.js +156 -0
- package/dist/engine/presenter.d.ts +58 -0
- package/dist/engine/presenter.js +157 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/markdown/attributes.d.ts +24 -0
- package/dist/markdown/attributes.js +94 -0
- package/dist/markdown/attrs-closed.d.ts +52 -0
- package/dist/markdown/attrs-closed.js +520 -0
- package/dist/markdown/blocks.d.ts +112 -0
- package/dist/markdown/blocks.js +766 -0
- package/dist/markdown/frontmatter.d.ts +30 -0
- package/dist/markdown/frontmatter.js +165 -0
- package/dist/markdown/index.d.ts +29 -0
- package/dist/markdown/index.js +29 -0
- package/dist/markdown/issues.d.ts +68 -0
- package/dist/markdown/issues.js +83 -0
- package/dist/markdown/json.d.ts +21 -0
- package/dist/markdown/json.js +236 -0
- package/dist/markdown/limits.d.ts +32 -0
- package/dist/markdown/limits.js +32 -0
- package/dist/markdown/parse.d.ts +31 -0
- package/dist/markdown/parse.js +283 -0
- package/dist/markdown/scan.d.ts +26 -0
- package/dist/markdown/scan.js +138 -0
- package/dist/markdown/slides.d.ts +32 -0
- package/dist/markdown/slides.js +854 -0
- package/dist/markdown/text.d.ts +76 -0
- package/dist/markdown/text.js +447 -0
- package/dist/markdown/twin.d.ts +41 -0
- package/dist/markdown/twin.js +374 -0
- package/dist/markdown/write.d.ts +17 -0
- package/dist/markdown/write.js +688 -0
- package/dist/math/index.d.ts +14 -0
- package/dist/math/index.js +14 -0
- package/dist/math/parse.d.ts +9 -0
- package/dist/math/parse.js +648 -0
- package/dist/math/symbols.d.ts +44 -0
- package/dist/math/symbols.js +333 -0
- package/dist/math/types.d.ts +59 -0
- package/dist/math/types.js +69 -0
- package/dist/model/backgrounds/contrast.d.ts +77 -0
- package/dist/model/backgrounds/contrast.js +186 -0
- package/dist/model/backgrounds/index.d.ts +7 -0
- package/dist/model/backgrounds/index.js +7 -0
- package/dist/model/backgrounds/resolve.d.ts +86 -0
- package/dist/model/backgrounds/resolve.js +215 -0
- package/dist/model/backgrounds/schema.d.ts +178 -0
- package/dist/model/backgrounds/schema.js +203 -0
- package/dist/model/codes.d.ts +73 -0
- package/dist/model/codes.js +67 -0
- package/dist/model/deck.d.ts +392 -0
- package/dist/model/deck.js +338 -0
- package/dist/model/index.d.ts +19 -0
- package/dist/model/index.js +19 -0
- package/dist/model/layouts/catalog.d.ts +433 -0
- package/dist/model/layouts/catalog.js +495 -0
- package/dist/model/layouts/index.d.ts +5 -0
- package/dist/model/layouts/index.js +5 -0
- package/dist/model/layouts/props.d.ts +302 -0
- package/dist/model/layouts/props.js +272 -0
- package/dist/model/loader.d.ts +37 -0
- package/dist/model/loader.js +317 -0
- package/dist/model/migrate.d.ts +34 -0
- package/dist/model/migrate.js +453 -0
- package/dist/model/motion/index.d.ts +6 -0
- package/dist/model/motion/index.js +6 -0
- package/dist/model/motion/transition.d.ts +96 -0
- package/dist/model/motion/transition.js +127 -0
- package/dist/model/parse.d.ts +57 -0
- package/dist/model/parse.js +307 -0
- package/dist/model/plain.d.ts +40 -0
- package/dist/model/plain.js +275 -0
- package/dist/model/shared.d.ts +63 -0
- package/dist/model/shared.js +86 -0
- package/dist/model/steps/index.d.ts +7 -0
- package/dist/model/steps/index.js +7 -0
- package/dist/model/steps/phase.d.ts +34 -0
- package/dist/model/steps/phase.js +50 -0
- package/dist/model/steps/slide-steps.d.ts +129 -0
- package/dist/model/steps/slide-steps.js +256 -0
- package/dist/model/style/index.d.ts +8 -0
- package/dist/model/style/index.js +8 -0
- package/dist/model/style/resolve.d.ts +87 -0
- package/dist/model/style/resolve.js +174 -0
- package/dist/model/style/schema.d.ts +85 -0
- package/dist/model/style/schema.js +53 -0
- package/dist/model/style/theme.d.ts +69 -0
- package/dist/model/style/theme.js +84 -0
- package/dist/schema/builders.d.ts +146 -0
- package/dist/schema/builders.js +285 -0
- package/dist/schema/diagnostics.d.ts +78 -0
- package/dist/schema/diagnostics.js +81 -0
- package/dist/schema/index.d.ts +15 -0
- package/dist/schema/index.js +14 -0
- package/dist/schema/internal.d.ts +40 -0
- package/dist/schema/internal.js +209 -0
- package/dist/schema/json-schema.d.ts +12 -0
- package/dist/schema/json-schema.js +213 -0
- package/dist/schema/types.d.ts +166 -0
- package/dist/schema/types.js +1 -0
- package/dist/schema/validate.d.ts +51 -0
- package/dist/schema/validate.js +765 -0
- package/dist/templates/index.d.ts +2 -0
- package/dist/templates/index.js +2 -0
- package/dist/templates/templates.d.ts +11 -0
- package/dist/templates/templates.js +47 -0
- package/package.json +60 -4
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type JsonPatchOperation = {
|
|
2
|
+
op: 'add' | 'replace' | 'test';
|
|
3
|
+
path: string;
|
|
4
|
+
value: unknown;
|
|
5
|
+
} | {
|
|
6
|
+
op: 'remove';
|
|
7
|
+
path: string;
|
|
8
|
+
} | {
|
|
9
|
+
op: 'move' | 'copy';
|
|
10
|
+
path: string;
|
|
11
|
+
from: string;
|
|
12
|
+
};
|
|
13
|
+
export type JsonPatchResult = {
|
|
14
|
+
ok: true;
|
|
15
|
+
value: unknown;
|
|
16
|
+
} | {
|
|
17
|
+
ok: false;
|
|
18
|
+
index: number;
|
|
19
|
+
reason: string;
|
|
20
|
+
};
|
|
21
|
+
export declare const MAX_DECK_PATCH_OPERATIONS = 1000;
|
|
22
|
+
/** Apply a bounded RFC 6902 patch to plain JSON data; failures leave input untouched. */
|
|
23
|
+
export declare function applyDeckPatch(document: unknown, operations: readonly JsonPatchOperation[]): JsonPatchResult;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
export const MAX_DECK_PATCH_OPERATIONS = 1_000;
|
|
2
|
+
const unsafe = new Set(['__proto__', 'prototype', 'constructor']);
|
|
3
|
+
/** Apply a bounded RFC 6902 patch to plain JSON data; failures leave input untouched. */
|
|
4
|
+
export function applyDeckPatch(document, operations) {
|
|
5
|
+
if (!Array.isArray(operations) || operations.length > MAX_DECK_PATCH_OPERATIONS)
|
|
6
|
+
return {
|
|
7
|
+
ok: false,
|
|
8
|
+
index: -1,
|
|
9
|
+
reason: `A patch may contain at most ${MAX_DECK_PATCH_OPERATIONS} operations.`,
|
|
10
|
+
};
|
|
11
|
+
let value;
|
|
12
|
+
try {
|
|
13
|
+
value = clone(document);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return { ok: false, index: -1, reason: 'The document must be finite plain JSON data.' };
|
|
17
|
+
}
|
|
18
|
+
for (let index = 0; index < operations.length; index++) {
|
|
19
|
+
const operation = operations[index];
|
|
20
|
+
try {
|
|
21
|
+
if (!operation ||
|
|
22
|
+
!['add', 'remove', 'replace', 'move', 'copy', 'test'].includes(operation.op))
|
|
23
|
+
throw new TypeError('Unsupported RFC 6902 operation.');
|
|
24
|
+
const path = pointer(operation.path);
|
|
25
|
+
if (operation.op === 'move' || operation.op === 'copy') {
|
|
26
|
+
const from = pointer(operation.from);
|
|
27
|
+
if (operation.op === 'move' &&
|
|
28
|
+
path.length > from.length &&
|
|
29
|
+
from.every((part, i) => path[i] === part))
|
|
30
|
+
throw new TypeError('Cannot move a value into its own descendant.');
|
|
31
|
+
const got = read(value, from);
|
|
32
|
+
if (operation.op === 'move')
|
|
33
|
+
value = mutate(value, from, 'remove');
|
|
34
|
+
value = mutate(value, path, 'add', clone(got));
|
|
35
|
+
}
|
|
36
|
+
else if (operation.op === 'test') {
|
|
37
|
+
if (!equal(read(value, path), operation.value))
|
|
38
|
+
throw new TypeError('The test operation failed.');
|
|
39
|
+
}
|
|
40
|
+
else
|
|
41
|
+
value = mutate(value, path, operation.op, operation.op === 'remove' ? undefined : clone(operation.value));
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
index,
|
|
47
|
+
reason: error instanceof Error ? error.message : 'Patch operation refused.',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { ok: true, value };
|
|
52
|
+
}
|
|
53
|
+
function pointer(value) {
|
|
54
|
+
if (typeof value !== 'string' || (value !== '' && !value.startsWith('/')))
|
|
55
|
+
throw new TypeError('A JSON Pointer must be empty or begin with /.');
|
|
56
|
+
const parts = value === ''
|
|
57
|
+
? []
|
|
58
|
+
: value
|
|
59
|
+
.slice(1)
|
|
60
|
+
.split('/')
|
|
61
|
+
.map((part) => {
|
|
62
|
+
if (/~(?![01])/u.test(part))
|
|
63
|
+
throw new TypeError('Malformed JSON Pointer escape.');
|
|
64
|
+
const decoded = part.replace(/~1/gu, '/').replace(/~0/gu, '~');
|
|
65
|
+
if (unsafe.has(decoded))
|
|
66
|
+
throw new TypeError('Prototype-related JSON Pointer paths are refused.');
|
|
67
|
+
return decoded;
|
|
68
|
+
});
|
|
69
|
+
return parts;
|
|
70
|
+
}
|
|
71
|
+
function clone(value) {
|
|
72
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
73
|
+
return value;
|
|
74
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
75
|
+
return value;
|
|
76
|
+
if (Array.isArray(value))
|
|
77
|
+
return value.map(clone);
|
|
78
|
+
if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
|
|
79
|
+
const result = {};
|
|
80
|
+
for (const [key, child] of Object.entries(value)) {
|
|
81
|
+
if (unsafe.has(key))
|
|
82
|
+
throw new TypeError('Prototype-related keys are refused.');
|
|
83
|
+
result[key] = clone(child);
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
throw new TypeError('The document must be finite plain JSON data.');
|
|
88
|
+
}
|
|
89
|
+
function parentAt(root, parts) {
|
|
90
|
+
if (!parts.length)
|
|
91
|
+
throw new TypeError('The document root cannot be used as a parent path.');
|
|
92
|
+
let current = root;
|
|
93
|
+
for (const part of parts.slice(0, -1))
|
|
94
|
+
current = child(current, part);
|
|
95
|
+
if (current === null || typeof current !== 'object')
|
|
96
|
+
throw new TypeError('The JSON Pointer parent does not exist.');
|
|
97
|
+
const key = parts.at(-1);
|
|
98
|
+
if (key === undefined)
|
|
99
|
+
throw new TypeError('The JSON Pointer parent path is empty.');
|
|
100
|
+
return { parent: current, key };
|
|
101
|
+
}
|
|
102
|
+
function child(value, key) {
|
|
103
|
+
if (Array.isArray(value)) {
|
|
104
|
+
const index = arrayIndex(key, value.length, false);
|
|
105
|
+
return value[index];
|
|
106
|
+
}
|
|
107
|
+
if (value && typeof value === 'object' && Object.hasOwn(value, key))
|
|
108
|
+
return value[key];
|
|
109
|
+
throw new TypeError('The JSON Pointer does not exist.');
|
|
110
|
+
}
|
|
111
|
+
function read(root, parts) {
|
|
112
|
+
return parts.reduce(child, root);
|
|
113
|
+
}
|
|
114
|
+
function mutate(root, parts, operation, next) {
|
|
115
|
+
if (!parts.length) {
|
|
116
|
+
if (operation === 'remove')
|
|
117
|
+
throw new TypeError('The document root cannot be removed.');
|
|
118
|
+
return next;
|
|
119
|
+
}
|
|
120
|
+
const { parent, key } = parentAt(root, parts);
|
|
121
|
+
if (Array.isArray(parent)) {
|
|
122
|
+
const index = arrayIndex(key, parent.length, operation === 'add');
|
|
123
|
+
if (operation === 'add')
|
|
124
|
+
parent.splice(index, 0, next);
|
|
125
|
+
else if (operation === 'remove')
|
|
126
|
+
parent.splice(index, 1);
|
|
127
|
+
else {
|
|
128
|
+
if (index >= parent.length)
|
|
129
|
+
throw new TypeError('The array index does not exist.');
|
|
130
|
+
parent[index] = next;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else if (operation === 'add')
|
|
134
|
+
parent[key] = next;
|
|
135
|
+
else if (!Object.hasOwn(parent, key))
|
|
136
|
+
throw new TypeError('The object property does not exist.');
|
|
137
|
+
else if (operation === 'remove')
|
|
138
|
+
delete parent[key];
|
|
139
|
+
else
|
|
140
|
+
parent[key] = next;
|
|
141
|
+
return root;
|
|
142
|
+
}
|
|
143
|
+
function arrayIndex(value, length, allowEnd) {
|
|
144
|
+
if (value === '-' && allowEnd)
|
|
145
|
+
return length;
|
|
146
|
+
if (!/^(?:0|[1-9]\d*)$/u.test(value))
|
|
147
|
+
throw new TypeError('Invalid array index.');
|
|
148
|
+
const index = Number(value);
|
|
149
|
+
if (!Number.isSafeInteger(index) || index > length || (!allowEnd && index === length))
|
|
150
|
+
throw new TypeError('Array index is out of bounds.');
|
|
151
|
+
return index;
|
|
152
|
+
}
|
|
153
|
+
function equal(left, right) {
|
|
154
|
+
if (Object.is(left, right))
|
|
155
|
+
return true;
|
|
156
|
+
if (Array.isArray(left) && Array.isArray(right))
|
|
157
|
+
return left.length === right.length && left.every((value, index) => equal(value, right[index]));
|
|
158
|
+
if (left && right && typeof left === 'object' && typeof right === 'object') {
|
|
159
|
+
const a = Object.keys(left);
|
|
160
|
+
const b = Object.keys(right);
|
|
161
|
+
return (a.length === b.length &&
|
|
162
|
+
a.every((key) => Object.hasOwn(right, key) &&
|
|
163
|
+
equal(left[key], right[key])));
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type DeckCatalogLayoutId, type LayoutPacing } from '../model/index.js';
|
|
2
|
+
export interface LayoutRequest {
|
|
3
|
+
id: string;
|
|
4
|
+
role: string;
|
|
5
|
+
pacing?: LayoutPacing | string;
|
|
6
|
+
purpose?: string;
|
|
7
|
+
layout?: string;
|
|
8
|
+
blockTypes?: readonly string[];
|
|
9
|
+
}
|
|
10
|
+
export interface LayoutChoice {
|
|
11
|
+
id: string;
|
|
12
|
+
layout: DeckCatalogLayoutId | 'free';
|
|
13
|
+
rationale: string;
|
|
14
|
+
override: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** Deterministic role/block/pacing selection; author overrides always win. */
|
|
17
|
+
export declare function selectDeckLayouts(requests: readonly LayoutRequest[], options?: {
|
|
18
|
+
aspect?: string;
|
|
19
|
+
}): LayoutChoice[];
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { DECK_LAYOUTS } from "../model/index.js";
|
|
2
|
+
const roleLayouts = Object.freeze({
|
|
3
|
+
title: ['title-hero'],
|
|
4
|
+
agenda: ['section-divider', 'process-steps'],
|
|
5
|
+
close: ['closing-cta'],
|
|
6
|
+
ask: ['closing-cta'],
|
|
7
|
+
section: ['section-divider'],
|
|
8
|
+
hook: ['title-hero', 'big-stat', 'quote'],
|
|
9
|
+
problem: ['two-column-split', 'image-left'],
|
|
10
|
+
solution: ['three-column-cards', 'process-steps'],
|
|
11
|
+
metrics: ['big-stat', 'chart'],
|
|
12
|
+
chart: ['chart', 'big-stat'],
|
|
13
|
+
comparison: ['comparison-table'],
|
|
14
|
+
team: ['team-avatars'],
|
|
15
|
+
quote: ['quote'],
|
|
16
|
+
steps: ['process-steps'],
|
|
17
|
+
timeline: ['timeline'],
|
|
18
|
+
roadmap: ['timeline', 'process-steps'],
|
|
19
|
+
code: ['code-with-callout'],
|
|
20
|
+
demo: ['image-centered', 'video-centered'],
|
|
21
|
+
findings: ['three-column-cards', 'chart'],
|
|
22
|
+
risks: ['two-column-split'],
|
|
23
|
+
recommendations: ['process-steps'],
|
|
24
|
+
});
|
|
25
|
+
const blockLayouts = Object.freeze({
|
|
26
|
+
chart: ['chart'],
|
|
27
|
+
table: ['comparison-table'],
|
|
28
|
+
steps: ['process-steps'],
|
|
29
|
+
timeline: ['timeline'],
|
|
30
|
+
quote: ['quote'],
|
|
31
|
+
code: ['code-with-callout'],
|
|
32
|
+
image: ['image-centered', 'image-left'],
|
|
33
|
+
video: ['video-centered', 'video-left'],
|
|
34
|
+
stat: ['big-stat'],
|
|
35
|
+
});
|
|
36
|
+
const pacingLayouts = Object.freeze({
|
|
37
|
+
punch: ['big-stat', 'quote', 'section-divider', 'free'],
|
|
38
|
+
idea: ['two-column-split', 'three-column-cards', 'process-steps', 'free'],
|
|
39
|
+
diagram: ['process-steps', 'timeline', 'chart', 'comparison-table', 'free'],
|
|
40
|
+
story: ['timeline', 'process-steps', 'quote', 'image-full-bleed', 'free'],
|
|
41
|
+
});
|
|
42
|
+
/** Deterministic role/block/pacing selection; author overrides always win. */
|
|
43
|
+
export function selectDeckLayouts(requests, options = {}) {
|
|
44
|
+
const choices = [];
|
|
45
|
+
const previous = [];
|
|
46
|
+
for (const request of requests) {
|
|
47
|
+
const override = request.layout?.trim();
|
|
48
|
+
if (override && (override === 'free' || Object.hasOwn(DECK_LAYOUTS, override))) {
|
|
49
|
+
choices.push({
|
|
50
|
+
id: request.id,
|
|
51
|
+
layout: override,
|
|
52
|
+
rationale: `Author override: layout "${override}".`,
|
|
53
|
+
override: true,
|
|
54
|
+
});
|
|
55
|
+
previous.push(override);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const pacing = normalizePacing(request.pacing);
|
|
59
|
+
const role = roleLayouts[request.role.trim().toLowerCase()] ?? [];
|
|
60
|
+
const block = (request.blockTypes ?? []).flatMap((type) => blockLayouts[type] ?? []);
|
|
61
|
+
const candidates = unique([...block, ...role, ...pacingLayouts[pacing], 'free']).filter((id) => id === 'free' ||
|
|
62
|
+
DECK_LAYOUTS[id].pacing.includes(pacing) ||
|
|
63
|
+
role.includes(id) ||
|
|
64
|
+
block.includes(id));
|
|
65
|
+
let chosen = candidates[0] ?? 'free';
|
|
66
|
+
let avoidedRepeat = false;
|
|
67
|
+
if (previous.length >= 2 && previous.at(-1) === previous.at(-2) && previous.at(-1) === chosen) {
|
|
68
|
+
const alternate = candidates.find((candidate) => candidate !== chosen);
|
|
69
|
+
if (alternate) {
|
|
70
|
+
chosen = alternate;
|
|
71
|
+
avoidedRepeat = true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const label = chosen === 'free' ? 'Free slide' : DECK_LAYOUTS[chosen].label;
|
|
75
|
+
choices.push({
|
|
76
|
+
id: request.id,
|
|
77
|
+
layout: chosen,
|
|
78
|
+
rationale: `Role "${request.role}" with pacing "${pacing}"; fits catalog "${chosen}" (${label})${avoidedRepeat ? '; avoided a third identical layout in a row' : ''}${options.aspect && options.aspect !== '16:9' ? `; aspect ${options.aspect}` : ''}.`,
|
|
79
|
+
override: false,
|
|
80
|
+
});
|
|
81
|
+
previous.push(chosen);
|
|
82
|
+
}
|
|
83
|
+
return choices;
|
|
84
|
+
}
|
|
85
|
+
function normalizePacing(value) {
|
|
86
|
+
return value === 'punch' || value === 'diagram' || value === 'story' ? value : 'idea';
|
|
87
|
+
}
|
|
88
|
+
function unique(values) {
|
|
89
|
+
return [...new Set(values)];
|
|
90
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Injectable, deterministic auto-slide scheduler.
|
|
3
|
+
*
|
|
4
|
+
* Pure with respect to the clock: pass a {@link SchedulerClock} in tests.
|
|
5
|
+
* User input pauses the timer (`input()`); `resume()` starts it again.
|
|
6
|
+
* No DOM. The default clock reads `globalThis` only when a method is called.
|
|
7
|
+
*/
|
|
8
|
+
export interface SchedulerClock {
|
|
9
|
+
setTimeout(callback: () => void, delayMs: number): unknown;
|
|
10
|
+
clearTimeout(handle: unknown): void;
|
|
11
|
+
now(): number;
|
|
12
|
+
}
|
|
13
|
+
export interface AutoSlideOptions {
|
|
14
|
+
/** Interval in ms; clamped to 1_000…600_000. */
|
|
15
|
+
intervalMs: number;
|
|
16
|
+
/** Called when the timer fires (typically `controller.next({ source: 'auto' })`). */
|
|
17
|
+
onAdvance(): void;
|
|
18
|
+
clock?: SchedulerClock;
|
|
19
|
+
/** Start paused (the host calls `resume()` when ready). */
|
|
20
|
+
startPaused?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface AutoSlideScheduler {
|
|
23
|
+
start(): void;
|
|
24
|
+
pause(): void;
|
|
25
|
+
resume(): void;
|
|
26
|
+
stop(): void;
|
|
27
|
+
isPaused(): boolean;
|
|
28
|
+
isRunning(): boolean;
|
|
29
|
+
/** User input: pause and clear the pending tick. */
|
|
30
|
+
input(): void;
|
|
31
|
+
}
|
|
32
|
+
export declare function createAutoSlideScheduler(options: AutoSlideOptions): AutoSlideScheduler;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Injectable, deterministic auto-slide scheduler.
|
|
3
|
+
*
|
|
4
|
+
* Pure with respect to the clock: pass a {@link SchedulerClock} in tests.
|
|
5
|
+
* User input pauses the timer (`input()`); `resume()` starts it again.
|
|
6
|
+
* No DOM. The default clock reads `globalThis` only when a method is called.
|
|
7
|
+
*/
|
|
8
|
+
const MIN_MS = 1_000;
|
|
9
|
+
const MAX_MS = 600_000;
|
|
10
|
+
const defaultClock = {
|
|
11
|
+
setTimeout: (callback, delayMs) => globalThis.setTimeout(callback, delayMs),
|
|
12
|
+
clearTimeout: (handle) => {
|
|
13
|
+
globalThis.clearTimeout(handle);
|
|
14
|
+
},
|
|
15
|
+
now: () => Date.now(),
|
|
16
|
+
};
|
|
17
|
+
export function createAutoSlideScheduler(options) {
|
|
18
|
+
const clock = options.clock ?? defaultClock;
|
|
19
|
+
const interval = Math.max(MIN_MS, Math.min(MAX_MS, Math.floor(Number.isFinite(options.intervalMs) ? options.intervalMs : MIN_MS)));
|
|
20
|
+
let timer;
|
|
21
|
+
let paused = options.startPaused === true;
|
|
22
|
+
let stopped = true;
|
|
23
|
+
const clear = () => {
|
|
24
|
+
if (timer !== undefined) {
|
|
25
|
+
clock.clearTimeout(timer);
|
|
26
|
+
timer = undefined;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const schedule = () => {
|
|
30
|
+
clear();
|
|
31
|
+
if (stopped || paused)
|
|
32
|
+
return;
|
|
33
|
+
timer = clock.setTimeout(() => {
|
|
34
|
+
timer = undefined;
|
|
35
|
+
if (stopped || paused)
|
|
36
|
+
return;
|
|
37
|
+
options.onAdvance();
|
|
38
|
+
schedule();
|
|
39
|
+
}, interval);
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
start() {
|
|
43
|
+
stopped = false;
|
|
44
|
+
schedule();
|
|
45
|
+
},
|
|
46
|
+
pause() {
|
|
47
|
+
paused = true;
|
|
48
|
+
clear();
|
|
49
|
+
},
|
|
50
|
+
resume() {
|
|
51
|
+
if (stopped)
|
|
52
|
+
return;
|
|
53
|
+
paused = false;
|
|
54
|
+
schedule();
|
|
55
|
+
},
|
|
56
|
+
stop() {
|
|
57
|
+
stopped = true;
|
|
58
|
+
clear();
|
|
59
|
+
},
|
|
60
|
+
isPaused: () => paused,
|
|
61
|
+
isRunning: () => !stopped && !paused && timer !== undefined,
|
|
62
|
+
input() {
|
|
63
|
+
paused = true;
|
|
64
|
+
clear();
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Virtual canvas fit and letterboxing. Independent of browser globals.
|
|
3
|
+
*
|
|
4
|
+
* Aspects: 16:9 → 1280×720, 4:3 → 1024×768, 9:16 → 720×1280, 1:1 → 1080×1080.
|
|
5
|
+
* `fit: 'contain' | 'container'` letterboxes; `cover` fills and may crop.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DECK_ASPECTS: readonly ["16:9", "4:3", "9:16", "1:1"];
|
|
8
|
+
export type DeckAspect = (typeof DECK_ASPECTS)[number];
|
|
9
|
+
export declare const DEFAULT_DECK_ASPECT: DeckAspect;
|
|
10
|
+
export declare const DECK_CANVAS: Readonly<{
|
|
11
|
+
readonly '16:9': Readonly<{
|
|
12
|
+
width: 1280;
|
|
13
|
+
height: 720;
|
|
14
|
+
}>;
|
|
15
|
+
readonly '4:3': Readonly<{
|
|
16
|
+
width: 1024;
|
|
17
|
+
height: 768;
|
|
18
|
+
}>;
|
|
19
|
+
readonly '9:16': Readonly<{
|
|
20
|
+
width: 720;
|
|
21
|
+
height: 1280;
|
|
22
|
+
}>;
|
|
23
|
+
readonly '1:1': Readonly<{
|
|
24
|
+
width: 1080;
|
|
25
|
+
height: 1080;
|
|
26
|
+
}>;
|
|
27
|
+
}>;
|
|
28
|
+
export type DeckCanvasSize = Readonly<{
|
|
29
|
+
width: number;
|
|
30
|
+
height: number;
|
|
31
|
+
}>;
|
|
32
|
+
export type DeckFitMode = 'contain' | 'cover' | 'container';
|
|
33
|
+
export interface CanvasFit {
|
|
34
|
+
/** Uniform scale applied to the virtual canvas. */
|
|
35
|
+
scale: number;
|
|
36
|
+
/** Rendered width after scale (CSS px). */
|
|
37
|
+
width: number;
|
|
38
|
+
/** Rendered height after scale (CSS px). */
|
|
39
|
+
height: number;
|
|
40
|
+
/** Letterbox offset on the inline axis. */
|
|
41
|
+
offsetX: number;
|
|
42
|
+
/** Letterbox offset on the block axis. */
|
|
43
|
+
offsetY: number;
|
|
44
|
+
canvasWidth: number;
|
|
45
|
+
canvasHeight: number;
|
|
46
|
+
aspect: DeckAspect;
|
|
47
|
+
fit: DeckFitMode;
|
|
48
|
+
}
|
|
49
|
+
export declare function isDeckAspect(value: unknown): value is DeckAspect;
|
|
50
|
+
/** The virtual canvas size for an aspect; unknown values fall back to 16:9. */
|
|
51
|
+
export declare function deckCanvasSize(aspect?: unknown): DeckCanvasSize;
|
|
52
|
+
/**
|
|
53
|
+
* Scale and letterbox a virtual canvas inside a container.
|
|
54
|
+
* `container` is an alias of `contain` for embedded decks.
|
|
55
|
+
*/
|
|
56
|
+
export declare function fitDeckCanvas(containerWidth: number, containerHeight: number, aspect?: unknown, fit?: DeckFitMode): CanvasFit;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Virtual canvas fit and letterboxing. Independent of browser globals.
|
|
3
|
+
*
|
|
4
|
+
* Aspects: 16:9 → 1280×720, 4:3 → 1024×768, 9:16 → 720×1280, 1:1 → 1080×1080.
|
|
5
|
+
* `fit: 'contain' | 'container'` letterboxes; `cover` fills and may crop.
|
|
6
|
+
*/
|
|
7
|
+
export const DECK_ASPECTS = ['16:9', '4:3', '9:16', '1:1'];
|
|
8
|
+
export const DEFAULT_DECK_ASPECT = '16:9';
|
|
9
|
+
export const DECK_CANVAS = Object.freeze({
|
|
10
|
+
'16:9': Object.freeze({ width: 1280, height: 720 }),
|
|
11
|
+
'4:3': Object.freeze({ width: 1024, height: 768 }),
|
|
12
|
+
'9:16': Object.freeze({ width: 720, height: 1280 }),
|
|
13
|
+
'1:1': Object.freeze({ width: 1080, height: 1080 }),
|
|
14
|
+
});
|
|
15
|
+
export function isDeckAspect(value) {
|
|
16
|
+
return typeof value === 'string' && DECK_ASPECTS.includes(value);
|
|
17
|
+
}
|
|
18
|
+
/** The virtual canvas size for an aspect; unknown values fall back to 16:9. */
|
|
19
|
+
export function deckCanvasSize(aspect = DEFAULT_DECK_ASPECT) {
|
|
20
|
+
return DECK_CANVAS[isDeckAspect(aspect) ? aspect : DEFAULT_DECK_ASPECT];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Scale and letterbox a virtual canvas inside a container.
|
|
24
|
+
* `container` is an alias of `contain` for embedded decks.
|
|
25
|
+
*/
|
|
26
|
+
export function fitDeckCanvas(containerWidth, containerHeight, aspect = DEFAULT_DECK_ASPECT, fit = 'contain') {
|
|
27
|
+
const resolvedAspect = isDeckAspect(aspect) ? aspect : DEFAULT_DECK_ASPECT;
|
|
28
|
+
const canvas = DECK_CANVAS[resolvedAspect];
|
|
29
|
+
const width = Number.isFinite(containerWidth) ? Math.max(0, containerWidth) : 0;
|
|
30
|
+
const height = Number.isFinite(containerHeight) ? Math.max(0, containerHeight) : 0;
|
|
31
|
+
const mode = fit === 'cover' ? 'cover' : fit === 'container' ? 'container' : 'contain';
|
|
32
|
+
const sx = canvas.width > 0 ? width / canvas.width : 0;
|
|
33
|
+
const sy = canvas.height > 0 ? height / canvas.height : 0;
|
|
34
|
+
const scale = mode === 'cover' ? Math.max(sx, sy) : Math.min(sx, sy);
|
|
35
|
+
const renderedWidth = canvas.width * scale;
|
|
36
|
+
const renderedHeight = canvas.height * scale;
|
|
37
|
+
return {
|
|
38
|
+
scale,
|
|
39
|
+
width: renderedWidth,
|
|
40
|
+
height: renderedHeight,
|
|
41
|
+
offsetX: (width - renderedWidth) / 2,
|
|
42
|
+
offsetY: (height - renderedHeight) / 2,
|
|
43
|
+
canvasWidth: canvas.width,
|
|
44
|
+
canvasHeight: canvas.height,
|
|
45
|
+
aspect: resolvedAspect,
|
|
46
|
+
fit: mode,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Imperative framework-free controller with typed events and ordered plugin hooks.
|
|
3
|
+
*
|
|
4
|
+
* `createDeckController(deck, options)` owns navigation, overview/blackout/pause
|
|
5
|
+
* flags, optional auto-slide, and routes `handleInput` through `decideDeckInput`.
|
|
6
|
+
* Stage chrome (help, laser, fullscreen, presenter window) stays with the host:
|
|
7
|
+
* the controller emits the action and leaves rendering to the stage.
|
|
8
|
+
*/
|
|
9
|
+
import { type SchedulerClock } from './auto-slide.js';
|
|
10
|
+
import { type DeckAction, type DeckInputEvent, type DeckInputState } from './controls.js';
|
|
11
|
+
import { type DeckLocation, type EngineDeck, type EngineSlide, type NavigatorOptions } from './navigation.js';
|
|
12
|
+
export interface DeckState extends DeckLocation {
|
|
13
|
+
overview: boolean;
|
|
14
|
+
blackout: boolean;
|
|
15
|
+
paused: boolean;
|
|
16
|
+
}
|
|
17
|
+
export type DeckActionSource = 'api' | 'keyboard' | 'pointer' | 'swipe' | 'auto' | 'presenter' | 'route';
|
|
18
|
+
export interface DeckStateChange {
|
|
19
|
+
state: DeckState;
|
|
20
|
+
previous: DeckState;
|
|
21
|
+
source: DeckActionSource;
|
|
22
|
+
}
|
|
23
|
+
/** Read-only context the stage passes around a paint (plugins may observe only). */
|
|
24
|
+
export interface DeckRenderContext {
|
|
25
|
+
state: DeckState;
|
|
26
|
+
/** The slide at `state.slideIndex`, when the deck still has it. */
|
|
27
|
+
slide?: EngineSlide;
|
|
28
|
+
source?: DeckActionSource;
|
|
29
|
+
}
|
|
30
|
+
export type DeckEventMap = {
|
|
31
|
+
slidechange: DeckStateChange;
|
|
32
|
+
stepchange: DeckStateChange;
|
|
33
|
+
statechange: DeckStateChange;
|
|
34
|
+
overview: {
|
|
35
|
+
open: boolean;
|
|
36
|
+
state: DeckState;
|
|
37
|
+
};
|
|
38
|
+
blackout: {
|
|
39
|
+
active: boolean;
|
|
40
|
+
state: DeckState;
|
|
41
|
+
};
|
|
42
|
+
pause: {
|
|
43
|
+
paused: boolean;
|
|
44
|
+
state: DeckState;
|
|
45
|
+
};
|
|
46
|
+
action: {
|
|
47
|
+
action: DeckAction;
|
|
48
|
+
source: DeckActionSource;
|
|
49
|
+
};
|
|
50
|
+
destroy: {
|
|
51
|
+
state: DeckState;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
export type DeckEventName = keyof DeckEventMap;
|
|
55
|
+
export type DeckListener<K extends DeckEventName> = (detail: DeckEventMap[K]) => void;
|
|
56
|
+
/** Plugin beforeNavigate result: cancel, redirect, or continue (void/undefined). */
|
|
57
|
+
export type BeforeNavigateResult = boolean | DeckLocation | undefined;
|
|
58
|
+
export interface DeckPlugin {
|
|
59
|
+
name: string;
|
|
60
|
+
/** Called once after the controller is built; may return a cleanup. */
|
|
61
|
+
setup?(controller: DeckController): undefined | (() => void);
|
|
62
|
+
/**
|
|
63
|
+
* Return `false` to cancel, a location to redirect, or void to continue.
|
|
64
|
+
* Plugins run in registration order.
|
|
65
|
+
*/
|
|
66
|
+
beforeNavigate?(target: string | number | DeckLocation, current: DeckState): // Plugins may return bare void to continue.
|
|
67
|
+
BeforeNavigateResult | void;
|
|
68
|
+
afterNavigate?(state: DeckState, previous: DeckState): void;
|
|
69
|
+
/**
|
|
70
|
+
* Called by the stage immediately before a paint. Read-only: plugins must
|
|
71
|
+
* not inject HTML or mutate the model. Runs in registration order.
|
|
72
|
+
*/
|
|
73
|
+
beforeRender?(context: DeckRenderContext): void;
|
|
74
|
+
/**
|
|
75
|
+
* Called by the stage immediately after a paint. Runs in registration order.
|
|
76
|
+
*/
|
|
77
|
+
afterRender?(context: DeckRenderContext): void;
|
|
78
|
+
onAction?(action: DeckAction, source: DeckActionSource): void;
|
|
79
|
+
onDestroy?(): void;
|
|
80
|
+
}
|
|
81
|
+
export interface DeckControllerOptions extends NavigatorOptions {
|
|
82
|
+
plugins?: readonly DeckPlugin[];
|
|
83
|
+
/** Override deck.navigation.autoSlideMs; 0 or omitted disables. */
|
|
84
|
+
autoSlideMs?: number;
|
|
85
|
+
clock?: SchedulerClock;
|
|
86
|
+
/** Start the auto-slide scheduler paused. */
|
|
87
|
+
autoSlidePaused?: boolean;
|
|
88
|
+
}
|
|
89
|
+
export interface DeckController {
|
|
90
|
+
goTo(target: string | number | DeckLocation, options?: {
|
|
91
|
+
source?: DeckActionSource;
|
|
92
|
+
}): DeckState;
|
|
93
|
+
next(options?: {
|
|
94
|
+
source?: DeckActionSource;
|
|
95
|
+
}): DeckState;
|
|
96
|
+
prev(options?: {
|
|
97
|
+
source?: DeckActionSource;
|
|
98
|
+
}): DeckState;
|
|
99
|
+
first(options?: {
|
|
100
|
+
source?: DeckActionSource;
|
|
101
|
+
}): DeckState;
|
|
102
|
+
last(options?: {
|
|
103
|
+
source?: DeckActionSource;
|
|
104
|
+
}): DeckState;
|
|
105
|
+
getState(): DeckState;
|
|
106
|
+
subscribe(listener: (state: DeckState, previous: DeckState) => void): () => void;
|
|
107
|
+
subscribe<K extends DeckEventName>(event: K, listener: DeckListener<K>): () => void;
|
|
108
|
+
setOverview(open: boolean): void;
|
|
109
|
+
setBlackout(active: boolean): void;
|
|
110
|
+
pause(): void;
|
|
111
|
+
resume(): void;
|
|
112
|
+
handleInput(event: DeckInputEvent, state?: DeckInputState, source?: DeckActionSource): DeckAction | null;
|
|
113
|
+
/**
|
|
114
|
+
* Stage calls this immediately before painting. Invokes each plugin's
|
|
115
|
+
* `beforeRender` in registration order with a read-only context.
|
|
116
|
+
*/
|
|
117
|
+
beforeRender(context?: Partial<Omit<DeckRenderContext, 'state'>>): void;
|
|
118
|
+
/**
|
|
119
|
+
* Stage calls this immediately after painting. Invokes each plugin's
|
|
120
|
+
* `afterRender` in registration order.
|
|
121
|
+
*/
|
|
122
|
+
afterRender(context?: Partial<Omit<DeckRenderContext, 'state'>>): void;
|
|
123
|
+
destroy(): void;
|
|
124
|
+
}
|
|
125
|
+
export declare function createDeckController(deck: EngineDeck, options?: DeckControllerOptions): DeckController;
|
|
126
|
+
export type { EngineSlide };
|