@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.
Files changed (139) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +183 -2
  3. package/dist/authoring/builder.d.ts +351 -0
  4. package/dist/authoring/builder.js +64 -0
  5. package/dist/authoring/index.d.ts +6 -0
  6. package/dist/authoring/index.js +6 -0
  7. package/dist/authoring/lint.d.ts +36 -0
  8. package/dist/authoring/lint.js +325 -0
  9. package/dist/authoring/outlines.d.ts +34 -0
  10. package/dist/authoring/outlines.js +255 -0
  11. package/dist/authoring/patch.d.ts +23 -0
  12. package/dist/authoring/patch.js +166 -0
  13. package/dist/authoring/select-layouts.d.ts +19 -0
  14. package/dist/authoring/select-layouts.js +90 -0
  15. package/dist/engine/auto-slide.d.ts +32 -0
  16. package/dist/engine/auto-slide.js +67 -0
  17. package/dist/engine/canvas.d.ts +56 -0
  18. package/dist/engine/canvas.js +48 -0
  19. package/dist/engine/controller.d.ts +126 -0
  20. package/dist/engine/controller.js +248 -0
  21. package/dist/engine/controls.d.ts +128 -0
  22. package/dist/engine/controls.js +460 -0
  23. package/dist/engine/hash.d.ts +42 -0
  24. package/dist/engine/hash.js +86 -0
  25. package/dist/engine/index.d.ts +88 -0
  26. package/dist/engine/index.js +88 -0
  27. package/dist/engine/messages.d.ts +138 -0
  28. package/dist/engine/messages.js +140 -0
  29. package/dist/engine/motion.d.ts +64 -0
  30. package/dist/engine/motion.js +112 -0
  31. package/dist/engine/navigation.d.ts +87 -0
  32. package/dist/engine/navigation.js +156 -0
  33. package/dist/engine/presenter.d.ts +58 -0
  34. package/dist/engine/presenter.js +157 -0
  35. package/dist/index.d.ts +9 -0
  36. package/dist/index.js +9 -0
  37. package/dist/markdown/attributes.d.ts +24 -0
  38. package/dist/markdown/attributes.js +94 -0
  39. package/dist/markdown/attrs-closed.d.ts +52 -0
  40. package/dist/markdown/attrs-closed.js +520 -0
  41. package/dist/markdown/blocks.d.ts +112 -0
  42. package/dist/markdown/blocks.js +766 -0
  43. package/dist/markdown/frontmatter.d.ts +30 -0
  44. package/dist/markdown/frontmatter.js +165 -0
  45. package/dist/markdown/index.d.ts +29 -0
  46. package/dist/markdown/index.js +29 -0
  47. package/dist/markdown/issues.d.ts +68 -0
  48. package/dist/markdown/issues.js +83 -0
  49. package/dist/markdown/json.d.ts +21 -0
  50. package/dist/markdown/json.js +236 -0
  51. package/dist/markdown/limits.d.ts +32 -0
  52. package/dist/markdown/limits.js +32 -0
  53. package/dist/markdown/parse.d.ts +31 -0
  54. package/dist/markdown/parse.js +283 -0
  55. package/dist/markdown/scan.d.ts +26 -0
  56. package/dist/markdown/scan.js +138 -0
  57. package/dist/markdown/slides.d.ts +32 -0
  58. package/dist/markdown/slides.js +854 -0
  59. package/dist/markdown/text.d.ts +76 -0
  60. package/dist/markdown/text.js +447 -0
  61. package/dist/markdown/twin.d.ts +41 -0
  62. package/dist/markdown/twin.js +374 -0
  63. package/dist/markdown/write.d.ts +17 -0
  64. package/dist/markdown/write.js +688 -0
  65. package/dist/math/index.d.ts +14 -0
  66. package/dist/math/index.js +14 -0
  67. package/dist/math/parse.d.ts +9 -0
  68. package/dist/math/parse.js +648 -0
  69. package/dist/math/symbols.d.ts +44 -0
  70. package/dist/math/symbols.js +333 -0
  71. package/dist/math/types.d.ts +59 -0
  72. package/dist/math/types.js +69 -0
  73. package/dist/model/backgrounds/contrast.d.ts +77 -0
  74. package/dist/model/backgrounds/contrast.js +186 -0
  75. package/dist/model/backgrounds/index.d.ts +7 -0
  76. package/dist/model/backgrounds/index.js +7 -0
  77. package/dist/model/backgrounds/resolve.d.ts +86 -0
  78. package/dist/model/backgrounds/resolve.js +215 -0
  79. package/dist/model/backgrounds/schema.d.ts +178 -0
  80. package/dist/model/backgrounds/schema.js +203 -0
  81. package/dist/model/codes.d.ts +73 -0
  82. package/dist/model/codes.js +67 -0
  83. package/dist/model/deck.d.ts +392 -0
  84. package/dist/model/deck.js +338 -0
  85. package/dist/model/index.d.ts +19 -0
  86. package/dist/model/index.js +19 -0
  87. package/dist/model/layouts/catalog.d.ts +433 -0
  88. package/dist/model/layouts/catalog.js +495 -0
  89. package/dist/model/layouts/index.d.ts +5 -0
  90. package/dist/model/layouts/index.js +5 -0
  91. package/dist/model/layouts/props.d.ts +302 -0
  92. package/dist/model/layouts/props.js +272 -0
  93. package/dist/model/loader.d.ts +37 -0
  94. package/dist/model/loader.js +317 -0
  95. package/dist/model/migrate.d.ts +34 -0
  96. package/dist/model/migrate.js +453 -0
  97. package/dist/model/motion/index.d.ts +6 -0
  98. package/dist/model/motion/index.js +6 -0
  99. package/dist/model/motion/transition.d.ts +96 -0
  100. package/dist/model/motion/transition.js +127 -0
  101. package/dist/model/parse.d.ts +57 -0
  102. package/dist/model/parse.js +307 -0
  103. package/dist/model/plain.d.ts +40 -0
  104. package/dist/model/plain.js +275 -0
  105. package/dist/model/shared.d.ts +63 -0
  106. package/dist/model/shared.js +86 -0
  107. package/dist/model/steps/index.d.ts +7 -0
  108. package/dist/model/steps/index.js +7 -0
  109. package/dist/model/steps/phase.d.ts +34 -0
  110. package/dist/model/steps/phase.js +50 -0
  111. package/dist/model/steps/slide-steps.d.ts +129 -0
  112. package/dist/model/steps/slide-steps.js +256 -0
  113. package/dist/model/style/index.d.ts +8 -0
  114. package/dist/model/style/index.js +8 -0
  115. package/dist/model/style/resolve.d.ts +87 -0
  116. package/dist/model/style/resolve.js +174 -0
  117. package/dist/model/style/schema.d.ts +85 -0
  118. package/dist/model/style/schema.js +53 -0
  119. package/dist/model/style/theme.d.ts +69 -0
  120. package/dist/model/style/theme.js +84 -0
  121. package/dist/schema/builders.d.ts +146 -0
  122. package/dist/schema/builders.js +285 -0
  123. package/dist/schema/diagnostics.d.ts +78 -0
  124. package/dist/schema/diagnostics.js +81 -0
  125. package/dist/schema/index.d.ts +15 -0
  126. package/dist/schema/index.js +14 -0
  127. package/dist/schema/internal.d.ts +40 -0
  128. package/dist/schema/internal.js +209 -0
  129. package/dist/schema/json-schema.d.ts +12 -0
  130. package/dist/schema/json-schema.js +213 -0
  131. package/dist/schema/types.d.ts +166 -0
  132. package/dist/schema/types.js +1 -0
  133. package/dist/schema/validate.d.ts +51 -0
  134. package/dist/schema/validate.js +765 -0
  135. package/dist/templates/index.d.ts +2 -0
  136. package/dist/templates/index.js +2 -0
  137. package/dist/templates/templates.d.ts +11 -0
  138. package/dist/templates/templates.js +47 -0
  139. package/package.json +60 -4
@@ -0,0 +1,460 @@
1
+ /**
2
+ * Controls registry and pure input decisions.
3
+ *
4
+ * `DECK_CONTROLS` is the single source for the toolbar, help dialog and
5
+ * `decideDeckInput` key map. No DOM: the host describes the event target as a
6
+ * path of plain element facts (`inputPathFrom` builds it from a real path).
7
+ *
8
+ * Keys (Protoyard registry + Texuli Enter/Backspace):
9
+ * → ↓ PageDown Space Enter / ← ↑ PageUp Shift+Space Backspace, Home/End,
10
+ * O, F, S|P, B|., L, ?, Ctrl/⌘+P, D, Esc stack.
11
+ * Swipe: ≥50 px, 1.5× dominance, <800 ms.
12
+ */
13
+ export const DECK_INPUT_ACTIONS = [
14
+ 'next',
15
+ 'prev',
16
+ 'first',
17
+ 'last',
18
+ 'overview',
19
+ 'presenter',
20
+ 'fullscreen',
21
+ 'blackout',
22
+ 'laser',
23
+ 'help',
24
+ 'print',
25
+ 'fullscreen-exit',
26
+ 'copy-link',
27
+ 'jump',
28
+ 'debug',
29
+ ];
30
+ export const DECK_CONTROL_GROUPS = [
31
+ 'navigate',
32
+ 'view',
33
+ 'present',
34
+ 'share',
35
+ 'help',
36
+ 'debug',
37
+ ];
38
+ export const DECK_CONTROL_CAPABILITIES = [
39
+ 'fullscreen',
40
+ 'popup',
41
+ 'clipboard',
42
+ 'print',
43
+ 'debug',
44
+ ];
45
+ export const DEFAULT_DECK_CONTROL_ENVIRONMENT = Object.freeze({
46
+ fullscreen: true,
47
+ popup: true,
48
+ clipboard: true,
49
+ print: true,
50
+ debug: false,
51
+ });
52
+ const ESC = { key: 'Escape', label: 'Esc', closes: true };
53
+ function control(spec) {
54
+ const requires = Object.freeze([...(spec.requires ?? [])]);
55
+ return Object.freeze({
56
+ action: spec.action,
57
+ group: spec.group,
58
+ labelKey: spec.labelKey,
59
+ descriptionKey: spec.descriptionKey,
60
+ keys: Object.freeze(spec.keys.map((key) => Object.freeze({ ...key }))),
61
+ toggle: spec.toggle,
62
+ requires,
63
+ available: (env) => requires.every((need) => env[need] === true),
64
+ });
65
+ }
66
+ /** Every deck control, in display order. */
67
+ export const DECK_CONTROLS = Object.freeze([
68
+ control({
69
+ action: 'prev',
70
+ group: 'navigate',
71
+ labelKey: 'controls.previous',
72
+ descriptionKey: 'controls.previousDescription',
73
+ keys: [
74
+ { key: 'ArrowLeft', label: '←' },
75
+ { key: 'ArrowUp', label: '↑' },
76
+ { key: 'PageUp', label: 'Page Up' },
77
+ { key: ' ', label: 'Shift + Space', shift: 'required' },
78
+ { key: 'Backspace', label: 'Backspace' },
79
+ ],
80
+ toggle: false,
81
+ }),
82
+ control({
83
+ action: 'jump',
84
+ group: 'navigate',
85
+ labelKey: 'controls.chooseSlide',
86
+ descriptionKey: 'controls.chooseSlideDescription',
87
+ keys: [],
88
+ toggle: false,
89
+ }),
90
+ control({
91
+ action: 'next',
92
+ group: 'navigate',
93
+ labelKey: 'controls.next',
94
+ descriptionKey: 'controls.nextDescription',
95
+ keys: [
96
+ { key: 'ArrowRight', label: '→' },
97
+ { key: 'ArrowDown', label: '↓' },
98
+ { key: 'PageDown', label: 'Page Down' },
99
+ { key: ' ', label: 'Space' },
100
+ { key: 'Enter', label: 'Enter' },
101
+ ],
102
+ toggle: false,
103
+ }),
104
+ control({
105
+ action: 'first',
106
+ group: 'navigate',
107
+ labelKey: 'controls.first',
108
+ descriptionKey: 'controls.firstDescription',
109
+ keys: [{ key: 'Home', label: 'Home' }],
110
+ toggle: false,
111
+ }),
112
+ control({
113
+ action: 'last',
114
+ group: 'navigate',
115
+ labelKey: 'controls.last',
116
+ descriptionKey: 'controls.lastDescription',
117
+ keys: [{ key: 'End', label: 'End' }],
118
+ toggle: false,
119
+ }),
120
+ control({
121
+ action: 'overview',
122
+ group: 'view',
123
+ labelKey: 'controls.overview',
124
+ descriptionKey: 'controls.overviewDescription',
125
+ keys: [{ key: 'o', label: 'O' }, ESC],
126
+ toggle: true,
127
+ }),
128
+ control({
129
+ action: 'fullscreen',
130
+ group: 'view',
131
+ labelKey: 'controls.fullscreen',
132
+ descriptionKey: 'controls.fullscreenDescription',
133
+ keys: [{ key: 'f', label: 'F' }],
134
+ toggle: true,
135
+ requires: ['fullscreen'],
136
+ }),
137
+ control({
138
+ action: 'fullscreen-exit',
139
+ group: 'view',
140
+ labelKey: 'controls.leaveFullscreen',
141
+ descriptionKey: 'controls.leaveFullscreenDescription',
142
+ keys: [ESC],
143
+ toggle: false,
144
+ requires: ['fullscreen'],
145
+ }),
146
+ control({
147
+ action: 'presenter',
148
+ group: 'present',
149
+ labelKey: 'controls.presenter',
150
+ descriptionKey: 'controls.presenterDescription',
151
+ keys: [
152
+ { key: 's', label: 'S' },
153
+ { key: 'p', label: 'P' },
154
+ ],
155
+ toggle: true,
156
+ }),
157
+ control({
158
+ action: 'blackout',
159
+ group: 'present',
160
+ labelKey: 'controls.blackout',
161
+ descriptionKey: 'controls.blackoutDescription',
162
+ keys: [{ key: 'b', label: 'B' }, { key: '.', label: '.' }, ESC],
163
+ toggle: true,
164
+ }),
165
+ control({
166
+ action: 'laser',
167
+ group: 'present',
168
+ labelKey: 'controls.laser',
169
+ descriptionKey: 'controls.laserDescription',
170
+ keys: [{ key: 'l', label: 'L' }, ESC],
171
+ toggle: true,
172
+ }),
173
+ control({
174
+ action: 'copy-link',
175
+ group: 'share',
176
+ labelKey: 'controls.copyLink',
177
+ descriptionKey: 'controls.copyLinkDescription',
178
+ keys: [],
179
+ toggle: false,
180
+ requires: ['clipboard'],
181
+ }),
182
+ control({
183
+ action: 'print',
184
+ group: 'share',
185
+ labelKey: 'controls.print',
186
+ descriptionKey: 'controls.printDescription',
187
+ keys: [
188
+ { key: 'p', label: 'Ctrl + P', modifier: 'ctrl' },
189
+ { key: 'p', label: '⌘ + P', modifier: 'meta' },
190
+ ],
191
+ toggle: false,
192
+ requires: ['print'],
193
+ }),
194
+ control({
195
+ action: 'help',
196
+ group: 'help',
197
+ labelKey: 'controls.help',
198
+ descriptionKey: 'controls.helpDescription',
199
+ keys: [{ key: '?', label: '?', shift: 'allowed' }, ESC],
200
+ toggle: true,
201
+ }),
202
+ control({
203
+ action: 'debug',
204
+ group: 'debug',
205
+ labelKey: 'controls.debug',
206
+ descriptionKey: 'controls.debugDescription',
207
+ keys: [{ key: 'd', label: 'D' }, ESC],
208
+ toggle: true,
209
+ requires: ['debug'],
210
+ }),
211
+ ]);
212
+ const BY_ACTION = new Map(DECK_CONTROLS.map((item) => [item.action, item]));
213
+ /** Look up a control by action. Throws when the action is unknown. */
214
+ export function deckControlFor(action) {
215
+ const found = BY_ACTION.get(action);
216
+ if (!found)
217
+ throw new RangeError(`No deck control for the action "${action}".`);
218
+ return found;
219
+ }
220
+ /** The controls that work in `env`, in display order. */
221
+ export function availableDeckControls(env = DEFAULT_DECK_CONTROL_ENVIRONMENT) {
222
+ return DECK_CONTROLS.filter((item) => item.available(env));
223
+ }
224
+ /** Help-dialog rows for the controls available in `env`. */
225
+ export function deckKeyBindings(env = DEFAULT_DECK_CONTROL_ENVIRONMENT) {
226
+ return availableDeckControls(env)
227
+ .filter((item) => item.keys.length > 0)
228
+ .map((item) => ({
229
+ keys: item.keys.map((key) => key.label),
230
+ action: item.action,
231
+ labelKey: item.labelKey,
232
+ }));
233
+ }
234
+ /** Default-environment key bindings (debug omitted). */
235
+ export const DECK_KEY_BINDINGS = Object.freeze(deckKeyBindings(DEFAULT_DECK_CONTROL_ENVIRONMENT));
236
+ /** A swipe must travel this far (CSS px) along its axis. */
237
+ export const DECK_SWIPE_MIN_DISTANCE = 50;
238
+ /** …and be this much longer on its axis than across it (axis lock). */
239
+ export const DECK_SWIPE_AXIS_RATIO = 1.5;
240
+ /** Slower drags are not swipes (they are reading or selecting). */
241
+ export const DECK_SWIPE_MAX_DURATION_MS = 800;
242
+ /** Interactive blocks put this on their root so clicks/swipes never move the deck. */
243
+ export const DECK_INTERACTIVE_ATTRIBUTE = 'data-tx-deck-interactive';
244
+ /** Deck chrome (progress, slide number, toolbar) carries this attribute. */
245
+ export const DECK_CHROME_ATTRIBUTE = 'data-tx-deck-chrome';
246
+ const EDITABLE_TAGS = new Set(['input', 'textarea', 'select']);
247
+ const CONTROL_TAGS = new Set([
248
+ 'a',
249
+ 'button',
250
+ 'input',
251
+ 'textarea',
252
+ 'select',
253
+ 'option',
254
+ 'label',
255
+ 'summary',
256
+ 'details',
257
+ 'iframe',
258
+ 'video',
259
+ 'audio',
260
+ 'object',
261
+ 'embed',
262
+ ]);
263
+ const CONTROL_ROLES = new Set([
264
+ 'button',
265
+ 'link',
266
+ 'checkbox',
267
+ 'radio',
268
+ 'switch',
269
+ 'slider',
270
+ 'spinbutton',
271
+ 'tab',
272
+ 'menuitem',
273
+ 'option',
274
+ 'textbox',
275
+ 'combobox',
276
+ 'listbox',
277
+ 'scrollbar',
278
+ ]);
279
+ const hasAttribute = (target, name) => target.attributes?.includes(name) === true;
280
+ const isEditable = (target) => EDITABLE_TAGS.has(target.tag) || target.editable === true || target.role === 'textbox';
281
+ function isGuardedTarget(path) {
282
+ return path.some((target) => CONTROL_TAGS.has(target.tag) ||
283
+ target.href === true ||
284
+ target.editable === true ||
285
+ (target.role !== undefined && CONTROL_ROLES.has(target.role)) ||
286
+ hasAttribute(target, DECK_INTERACTIVE_ATTRIBUTE) ||
287
+ hasAttribute(target, DECK_CHROME_ATTRIBUTE));
288
+ }
289
+ const anyModifier = (event) => event.ctrlKey === true ||
290
+ event.metaKey === true ||
291
+ event.altKey === true ||
292
+ event.shiftKey === true;
293
+ /** Keys derived from the registry (never typed twice). */
294
+ const NAVIGATION_KEYS = {};
295
+ const SHIFTED_NAVIGATION_KEYS = {};
296
+ const TOGGLE_KEYS = {};
297
+ const SHIFT_ALLOWED_TOGGLES = new Set();
298
+ const MODIFIER_KEYS = {};
299
+ for (const item of DECK_CONTROLS) {
300
+ for (const spec of item.keys) {
301
+ if (spec.closes)
302
+ continue;
303
+ const key = spec.key.length === 1 ? spec.key.toLowerCase() : spec.key;
304
+ if (spec.modifier) {
305
+ MODIFIER_KEYS[key] = item.action;
306
+ }
307
+ else if (item.group === 'navigate') {
308
+ const table = spec.shift === 'required' ? SHIFTED_NAVIGATION_KEYS : NAVIGATION_KEYS;
309
+ // Navigation keys keep their KeyboardEvent.key form (ArrowRight, ' ').
310
+ table[spec.key] = item.action;
311
+ }
312
+ else {
313
+ TOGGLE_KEYS[key] = item.action;
314
+ if (spec.shift === 'allowed')
315
+ SHIFT_ALLOWED_TOGGLES.add(key);
316
+ }
317
+ }
318
+ }
319
+ const ESCAPE_ORDER = [
320
+ ['debug', (state) => state.debug === true],
321
+ ['help', (state) => state.help === true],
322
+ ['overview', (state) => state.overview === 'open'],
323
+ ['blackout', (state) => state.blackout === true],
324
+ ['laser', (state) => state.laser === true],
325
+ ['fullscreen-exit', (state) => state.fullscreen === true],
326
+ ];
327
+ /** The actions Esc can reach, innermost first. */
328
+ export const DECK_ESCAPE_ORDER = ESCAPE_ORDER.map(([action]) => action);
329
+ function decideKey(event, state) {
330
+ if (event.isComposing)
331
+ return null;
332
+ const path = event.path ?? [];
333
+ if (path.some(isEditable))
334
+ return null;
335
+ let key = event.key;
336
+ const lower = key.length === 1 ? key.toLowerCase() : key;
337
+ // RTL: flip horizontal arrows before the registry lookup.
338
+ if (state.dir === 'rtl') {
339
+ if (key === 'ArrowLeft')
340
+ key = 'ArrowRight';
341
+ else if (key === 'ArrowRight')
342
+ key = 'ArrowLeft';
343
+ }
344
+ const shortcut = MODIFIER_KEYS[lower];
345
+ if ((event.ctrlKey || event.metaKey) && !event.altKey && shortcut) {
346
+ return event.repeat ? null : shortcut;
347
+ }
348
+ if (event.ctrlKey || event.metaKey || event.altKey)
349
+ return null;
350
+ if (key === 'Escape') {
351
+ if (event.repeat)
352
+ return null;
353
+ return ESCAPE_ORDER.find(([, isOn]) => isOn(state))?.[0] ?? null;
354
+ }
355
+ const found = TOGGLE_KEYS[lower];
356
+ const toggle = found === 'debug' && state.debugAvailable !== true ? undefined : found;
357
+ if (toggle && (SHIFT_ALLOWED_TOGGLES.has(lower) || !event.shiftKey)) {
358
+ if (event.repeat)
359
+ return null;
360
+ if (state.overview === 'closing')
361
+ return null;
362
+ if (state.help)
363
+ return toggle === 'help' ? 'help' : null;
364
+ if (state.overview === 'open')
365
+ return toggle === 'overview' ? 'overview' : null;
366
+ if (state.blackout)
367
+ return toggle === 'blackout' ? 'blackout' : null;
368
+ return toggle;
369
+ }
370
+ const plain = NAVIGATION_KEYS[key];
371
+ const shifted = SHIFTED_NAVIGATION_KEYS[key];
372
+ if (!plain && !shifted)
373
+ return null;
374
+ if (state.overview === 'open' || state.overview === 'closing' || state.help || state.blackout) {
375
+ return null;
376
+ }
377
+ if (isGuardedTarget(path))
378
+ return null;
379
+ if (event.shiftKey)
380
+ return shifted ?? null;
381
+ return plain ?? null;
382
+ }
383
+ function decideClick(event, state) {
384
+ if ((event.button ?? 0) !== 0)
385
+ return null;
386
+ if (anyModifier(event))
387
+ return null;
388
+ if (state.overview === 'open' || state.overview === 'closing')
389
+ return null;
390
+ if (state.help || state.blackout || state.laser)
391
+ return null;
392
+ if ((event.selectionLength ?? 0) > 0)
393
+ return null;
394
+ if (isGuardedTarget(event.path ?? []))
395
+ return null;
396
+ return 'next';
397
+ }
398
+ function decideSwipe(event, state) {
399
+ if (event.pointerType === 'mouse')
400
+ return null;
401
+ if (!Number.isFinite(event.dx) || !Number.isFinite(event.dy))
402
+ return null;
403
+ if (state.overview === 'open' || state.overview === 'closing')
404
+ return null;
405
+ if (state.help || state.blackout)
406
+ return null;
407
+ if (isGuardedTarget(event.path ?? []))
408
+ return null;
409
+ if ((event.durationMs ?? 0) >= DECK_SWIPE_MAX_DURATION_MS)
410
+ return null; // reject at == max (< 800 ms)
411
+ const ax = Math.abs(event.dx);
412
+ const ay = Math.abs(event.dy);
413
+ if (ax < DECK_SWIPE_MIN_DISTANCE || ax < ay * DECK_SWIPE_AXIS_RATIO)
414
+ return null;
415
+ // RTL: a swipe left still means "forward" in reading order? Keep physical
416
+ // direction (left = next) so the gesture matches the slide motion.
417
+ return event.dx < 0 ? 'next' : 'prev';
418
+ }
419
+ /** The action an input event asks for, or `null` when the deck should ignore it. */
420
+ export function decideDeckInput(event, state = {}) {
421
+ switch (event.kind) {
422
+ case 'key':
423
+ return decideKey(event, state);
424
+ case 'click':
425
+ return decideClick(event, state);
426
+ case 'swipe':
427
+ return decideSwipe(event, state);
428
+ default:
429
+ return null;
430
+ }
431
+ }
432
+ const WATCHED_ATTRIBUTES = [DECK_INTERACTIVE_ATTRIBUTE, DECK_CHROME_ATTRIBUTE];
433
+ /**
434
+ * Turns an event path (`event.composedPath()`, innermost first) into the
435
+ * plain targets `decideDeckInput` reads. Non-elements are skipped; the walk
436
+ * stops at `stopAt` (the deck root) when given.
437
+ */
438
+ export function inputPathFrom(nodes, stopAt) {
439
+ const out = [];
440
+ for (const node of nodes) {
441
+ if (stopAt !== undefined && node === stopAt)
442
+ break;
443
+ const el = node;
444
+ if (!el || typeof el.tagName !== 'string' || typeof el.getAttribute !== 'function')
445
+ continue;
446
+ const tag = el.tagName.toLowerCase();
447
+ const role = el.getAttribute('role') ?? undefined;
448
+ const attributes = WATCHED_ATTRIBUTES.filter((name) => typeof el.hasAttribute === 'function'
449
+ ? el.hasAttribute(name)
450
+ : el.getAttribute?.(name) !== null);
451
+ out.push({
452
+ tag,
453
+ ...(role ? { role } : {}),
454
+ ...(el.isContentEditable === true ? { editable: true } : {}),
455
+ ...(tag === 'a' && el.getAttribute('href') !== null ? { href: true } : {}),
456
+ ...(attributes.length > 0 ? { attributes } : {}),
457
+ });
458
+ }
459
+ return out;
460
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Pure hash route parsing and formatting. Browser `location` is owned by hosts.
3
+ *
4
+ * Format: `#/<slide-id>/<step>` where `slide-id` is a lowercase slug and
5
+ * `step` is a non-negative integer ≤ 32. Hostile input (traversal, uppercase,
6
+ * percent-encoding, query/fragment inside the hash) yields `null`.
7
+ * Print and notes flags live on the search string: `?print=1&notes=1`.
8
+ */
9
+ import type { DeckLocation } from './navigation.js';
10
+ /** Maximum slide-id length accepted in a hash (matches model slug bound). */
11
+ export declare const DECK_HASH_ID_MAX = 64;
12
+ /** Maximum step index accepted in a hash (matches deck step cap). */
13
+ export declare const DECK_HASH_STEP_MAX = 32;
14
+ export interface DeckHashLocation {
15
+ slideId: string;
16
+ step: number;
17
+ }
18
+ export interface DeckRouteFlags {
19
+ print: boolean;
20
+ notes: boolean;
21
+ }
22
+ export interface ParsedDeckRoute {
23
+ location: DeckHashLocation | null;
24
+ flags: DeckRouteFlags;
25
+ }
26
+ /**
27
+ * Parse a deck deep link. Accepts `#/id/step`, `#/id`, `/id/step`, or `id/step`.
28
+ * Returns `null` for empty, hostile, or malformed input (never throws).
29
+ */
30
+ export declare function parseDeckHash(raw: string | null | undefined): DeckHashLocation | null;
31
+ /**
32
+ * Format a deck deep link as `#/<slide-id>/<step>`. Clamps step to 0…32.
33
+ * Returns `#/` when the id is empty or invalid.
34
+ */
35
+ export declare function formatDeckHash(slideId: string, step?: number): string;
36
+ /** Parse hash + search into a location and the print/notes flags. */
37
+ export declare function parseDeckRoute(hash: string | null | undefined, search?: string): ParsedDeckRoute;
38
+ /**
39
+ * Format a route string. Flags become a leading query string when set:
40
+ * `?print=1&notes=1#/slide/0`.
41
+ */
42
+ export declare function formatDeckRoute(location: Pick<DeckLocation, 'slideId' | 'step'> | DeckHashLocation, flags?: Partial<DeckRouteFlags>): string;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Pure hash route parsing and formatting. Browser `location` is owned by hosts.
3
+ *
4
+ * Format: `#/<slide-id>/<step>` where `slide-id` is a lowercase slug and
5
+ * `step` is a non-negative integer ≤ 32. Hostile input (traversal, uppercase,
6
+ * percent-encoding, query/fragment inside the hash) yields `null`.
7
+ * Print and notes flags live on the search string: `?print=1&notes=1`.
8
+ */
9
+ /** Maximum slide-id length accepted in a hash (matches model slug bound). */
10
+ export const DECK_HASH_ID_MAX = 64;
11
+ /** Maximum step index accepted in a hash (matches deck step cap). */
12
+ export const DECK_HASH_STEP_MAX = 32;
13
+ const SLIDE_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
14
+ /**
15
+ * Parse a deck deep link. Accepts `#/id/step`, `#/id`, `/id/step`, or `id/step`.
16
+ * Returns `null` for empty, hostile, or malformed input (never throws).
17
+ */
18
+ export function parseDeckHash(raw) {
19
+ if (typeof raw !== 'string' || raw.length === 0 || raw.length > 256)
20
+ return null;
21
+ let value = raw.startsWith('#') ? raw.slice(1) : raw;
22
+ if (value.startsWith('/'))
23
+ value = value.slice(1);
24
+ if (value.includes('?') ||
25
+ value.includes('#') ||
26
+ value.includes('\\') ||
27
+ value.includes('..') ||
28
+ value.includes('//') ||
29
+ value.includes('%')) {
30
+ return null;
31
+ }
32
+ const parts = value.split('/');
33
+ if (parts.length < 1 || parts.length > 2)
34
+ return null;
35
+ const slideId = parts[0] ?? '';
36
+ if (!SLIDE_ID.test(slideId) || slideId.length > DECK_HASH_ID_MAX)
37
+ return null;
38
+ if (parts.length === 1)
39
+ return { slideId, step: 0 };
40
+ const stepRaw = parts[1] ?? '';
41
+ if (!/^\d{1,3}$/.test(stepRaw))
42
+ return null;
43
+ const step = Number(stepRaw);
44
+ if (!Number.isInteger(step) || step < 0 || step > DECK_HASH_STEP_MAX)
45
+ return null;
46
+ return { slideId, step };
47
+ }
48
+ /**
49
+ * Format a deck deep link as `#/<slide-id>/<step>`. Clamps step to 0…32.
50
+ * Returns `#/` when the id is empty or invalid.
51
+ */
52
+ export function formatDeckHash(slideId, step = 0) {
53
+ const id = typeof slideId === 'string' && SLIDE_ID.test(slideId) ? slideId : '';
54
+ const safeStep = Number.isFinite(step)
55
+ ? Math.max(0, Math.min(DECK_HASH_STEP_MAX, Math.floor(step)))
56
+ : 0;
57
+ if (!id)
58
+ return '#/';
59
+ return `#/${id}/${safeStep}`;
60
+ }
61
+ /** Parse hash + search into a location and the print/notes flags. */
62
+ export function parseDeckRoute(hash, search = '') {
63
+ const raw = typeof search === 'string' ? search : '';
64
+ const params = new URLSearchParams(raw.startsWith('?') ? raw.slice(1) : raw);
65
+ return {
66
+ location: parseDeckHash(hash),
67
+ flags: {
68
+ print: params.get('print') === '1',
69
+ notes: params.get('notes') === '1',
70
+ },
71
+ };
72
+ }
73
+ /**
74
+ * Format a route string. Flags become a leading query string when set:
75
+ * `?print=1&notes=1#/slide/0`.
76
+ */
77
+ export function formatDeckRoute(location, flags = {}) {
78
+ const hash = formatDeckHash(location.slideId, location.step);
79
+ const params = new URLSearchParams();
80
+ if (flags.print)
81
+ params.set('print', '1');
82
+ if (flags.notes)
83
+ params.set('notes', '1');
84
+ const query = params.toString();
85
+ return query ? `?${query}${hash}` : hash;
86
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * `@texuli/deck/engine` — framework-free presentation runtime (ADR-009).
3
+ *
4
+ * ## Public API
5
+ *
6
+ * ### Navigation (`navigation.ts`)
7
+ * - `createDeckNavigator(deck, options?)` → `DeckNavigator`
8
+ * - `next` / `prev` / `first` / `last` / `goTo(id | visibleIndex | location, step?)`
9
+ * - `goToVertical(sectionId, verticalIndex, step?)`
10
+ * - `getLocation()` → `DeckLocation` (`slideIndex`, `slideId`, `step`,
11
+ * `stepCount` = max step index / advances, optional `sectionId` /
12
+ * `verticalIndex` for stacked outline sections)
13
+ * - `visibleSlides()`, `stepCountFor(slideId)`
14
+ * - `modelSlideStepCount(slide)` — default advance count from blocks/props/labels
15
+ * - Types: `EngineDeck`, `EngineSlide`, `EngineSection`, `DeckLocation`,
16
+ * `NavigatorOptions`, `DeckNavigator`
17
+ *
18
+ * ### Hash routing (`hash.ts`)
19
+ * - `parseDeckHash(raw)` / `formatDeckHash(slideId, step)` — `#/<id>/<step>`
20
+ * Rejects traversal (`..`), uppercase ids, percent-encoding, `?`/`#`/`\` inside.
21
+ * - `parseDeckRoute(hash, search?)` / `formatDeckRoute(location, flags?)` —
22
+ * `?print=1&notes=1` flags
23
+ * - Constants: `DECK_HASH_ID_MAX` (64), `DECK_HASH_STEP_MAX` (32)
24
+ *
25
+ * ### Controls + input (`controls.ts`)
26
+ * - `DECK_CONTROLS` registry (single source for help/toolbar/docs)
27
+ * - `decideDeckInput(event, state?)` → `DeckAction | null`
28
+ * Keys: →↓PageDown Space Enter / ←↑PageUp Shift+Space Backspace, Home/End,
29
+ * O, F, S|P, B|., L, ?, Ctrl/⌘+P, D, Esc stack (debug→help→overview→
30
+ * blackout→laser→fullscreen-exit). Swipe ≥50 px, 1.5× dominance, <800 ms.
31
+ * Guards: fields/IME/modifiers/interactive targets/`data-tx-deck-*`.
32
+ * - `inputPathFrom(nodes, stopAt?)`, `deckControlFor`, `availableDeckControls`,
33
+ * `deckKeyBindings`, `DECK_KEY_BINDINGS`, `DECK_ESCAPE_ORDER`
34
+ * - Swipe constants, `DECK_INTERACTIVE_ATTRIBUTE`, `DECK_CHROME_ATTRIBUTE`
35
+ *
36
+ * ### Canvas fit (`canvas.ts`)
37
+ * - `DECK_CANVAS` / `deckCanvasSize(aspect)` / `fitDeckCanvas(w, h, aspect?, fit?)`
38
+ * Aspects 16:9, 4:3, 9:16, 1:1. Fit: `contain` | `cover` | `container`
39
+ * (`container` ≡ letterboxed contain for embedded decks).
40
+ *
41
+ * ### Motion (`motion.ts`)
42
+ * - `resolveDeckTransition({ slide, style, motion, reducedMotion })`
43
+ * - `effectiveMotion`, `morphPairsFor`, `TRANSITION_MS`, `MAX_MOTION_MS`
44
+ *
45
+ * ### Auto-slide (`auto-slide.ts`)
46
+ * - `createAutoSlideScheduler({ intervalMs, onAdvance, clock?, startPaused? })`
47
+ * Injectable clock; `input()` pauses; clamp 1s…600s.
48
+ *
49
+ * ### Presenter protocol v1 (`presenter.ts`)
50
+ * - Closed messages `{ type: 'hello'|'sync'|'request', slideId, step }`
51
+ * - Namespace `texuli-deck-presenter/1:<ns>` via `presenterChannelName`
52
+ * - `parsePresenterMessage` / `presenterMessage` / `isPresenterMessage`
53
+ * - Transports (no globals at import): `createPresenterTransport(channel)`,
54
+ * `createMemoryPresenterTransport(ns?)`,
55
+ * `createBroadcastPresenterTransport(ns, factory?)`
56
+ *
57
+ * ### Controller (`controller.ts`)
58
+ * - `createDeckController(deck, options?)` → `DeckController`
59
+ * - Navigation: `goTo` / `next` / `prev` / `first` / `last` / `getState`
60
+ * - `subscribe(listener)` or `subscribe(event, listener)` —
61
+ * events: `slidechange` | `stepchange` | `statechange` | `overview` |
62
+ * `blackout` | `pause` | `action` | `destroy`
63
+ * - `setOverview` / `setBlackout` / `pause` / `resume` / `handleInput` /
64
+ * `destroy`
65
+ * - Plugins (ordered): `setup`, `beforeNavigate`, `afterNavigate`,
66
+ * `beforeRender` (read-only slide/context), `afterRender`, `onAction`,
67
+ * `onDestroy`. Stage calls `controller.beforeRender()` / `afterRender()`
68
+ * around a paint; hooks observe only (no HTML injection).
69
+ *
70
+ * ### Messages (`messages.ts`)
71
+ * - `DECK_MESSAGES` English defaults; `formatMessage(key, values?, overrides?)`
72
+ * - `resolveDeckLocale(deck?, override?)` → `{ locale, dir }`
73
+ *
74
+ * ## Constraints
75
+ * - Framework-free and DOM-free at import (no `window`/`document` access).
76
+ * - Imports stay inside `packages/deck/src/` (the engine uses the package's own model;
77
+ * import-guard enforced), and nothing touches window, document or navigator at load.
78
+ * - Zero third-party runtime dependencies; erasable TypeScript only.
79
+ */
80
+ export { type AutoSlideOptions, type AutoSlideScheduler, createAutoSlideScheduler, type SchedulerClock, } from './auto-slide.js';
81
+ export { type CanvasFit, DECK_ASPECTS, DECK_CANVAS, DEFAULT_DECK_ASPECT, type DeckAspect, type DeckCanvasSize, type DeckFitMode, deckCanvasSize, fitDeckCanvas, isDeckAspect, } from './canvas.js';
82
+ export { type BeforeNavigateResult, createDeckController, type DeckActionSource, type DeckController, type DeckControllerOptions, type DeckEventMap, type DeckEventName, type DeckListener, type DeckPlugin, type DeckRenderContext, type DeckState, type DeckStateChange, } from './controller.js';
83
+ export { availableDeckControls, DECK_CHROME_ATTRIBUTE, DECK_CONTROL_CAPABILITIES, DECK_CONTROL_GROUPS, DECK_CONTROLS, DECK_ESCAPE_ORDER, DECK_INPUT_ACTIONS, DECK_INTERACTIVE_ATTRIBUTE, DECK_KEY_BINDINGS, DECK_SWIPE_AXIS_RATIO, DECK_SWIPE_MAX_DURATION_MS, DECK_SWIPE_MIN_DISTANCE, DEFAULT_DECK_CONTROL_ENVIRONMENT, type DeckAction, type DeckControl, type DeckControlCapability, type DeckControlEnvironment, type DeckControlGroup, type DeckControlKey, type DeckInputAction, type DeckInputEvent, type DeckInputState, type DeckInputTarget, type DeckKeyBinding, decideDeckInput, deckControlFor, deckKeyBindings, inputPathFrom, } from './controls.js';
84
+ export { DECK_HASH_ID_MAX, DECK_HASH_STEP_MAX, type DeckHashLocation, type DeckRouteFlags, formatDeckHash, formatDeckRoute, type ParsedDeckRoute, parseDeckHash, parseDeckRoute, } from './hash.js';
85
+ export { DECK_MESSAGES, type DeckLocale, type DeckMessageKey, type DeckMessages, formatMessage, type MessageValues, resolveDeckLocale, } from './messages.js';
86
+ export { DECK_TRANSITIONS, DEFAULT_MOTION, DEFAULT_TRANSITION, type DeckMotion, type DeckTransition, effectiveMotion, isDeckTransition, isMotionLevel, MAX_MOTION_MS, MOTION_LEVELS, type MorphPair, morphPairsFor, REDUCED_FADE_MS, type ResolvedTransition, type ResolveTransitionInput, resolveDeckTransition, SUBTLE_SCALE, TRANSITION_MS, type TransitionReason, } from './motion.js';
87
+ export { createDeckNavigator, type DeckLocation, type DeckNavigator, type EngineDeck, type EngineSection, type EngineSlide, modelSlideStepCount, type NavigatorOptions, } from './navigation.js';
88
+ export { createBroadcastPresenterTransport, createMemoryPresenterTransport, createPresenterTransport, isPresenterMessage, PRESENTER_MESSAGE_TYPES, PRESENTER_PROTOCOL_PREFIX, PRESENTER_PROTOCOL_VERSION, type PresenterChannelLike, type PresenterMessage, type PresenterMessageType, type PresenterTransport, parsePresenterMessage, presenterChannelName, presenterMessage, sanitizePresenterNamespace, } from './presenter.js';