@bendyline/squisq-react 2.2.0 → 2.3.1
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/NOTICE.md +9 -10
- package/THIRD_PARTY_LICENSES.txt +4469 -0
- package/dist/AudioController-DwMsPe38.d.ts +48 -0
- package/dist/chunk-7FVQ7T3I.js +2529 -0
- package/dist/chunk-D7KN4CRG.js +396 -0
- package/dist/chunk-LR3AIGDD.js +58 -0
- package/dist/chunk-NANF7REM.js +330 -0
- package/dist/chunk-NWZQGZIJ.js +675 -0
- package/dist/chunk-TQH6RTTV.js +789 -0
- package/dist/chunk-TT6ENR6T.js +26 -0
- package/dist/chunk-WLUZTUNZ.js +111 -0
- package/dist/chunk-XYS7HMP4.js +1604 -0
- package/dist/chunk-YQC3AXXU.js +92 -0
- package/dist/hooks/index.d.ts +187 -0
- package/dist/hooks/index.js +32 -0
- package/dist/index.d.ts +14 -904
- package/dist/index.js +53 -6440
- package/dist/json-view/index.d.ts +21 -0
- package/dist/json-view/index.js +10 -0
- package/dist/layers/index.d.ts +124 -0
- package/dist/layers/index.js +24 -0
- package/dist/markdown/index.d.ts +44 -0
- package/dist/markdown/index.js +11 -0
- package/dist/page/index.d.ts +105 -0
- package/dist/page/index.js +19 -0
- package/dist/player/index.d.ts +410 -0
- package/dist/player/index.js +41 -0
- package/dist/squisq-player.full.global.js +364 -363
- package/dist/squisq-player.global.js +60 -59
- package/dist/standalone-source.js +1 -1
- package/dist/styles/index.d.ts +1 -0
- package/package.json +36 -5
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// src/hooks/useModalDialog.ts
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
var FOCUSABLE_SELECTOR = [
|
|
4
|
+
"a[href]",
|
|
5
|
+
"button:not([disabled])",
|
|
6
|
+
"input:not([disabled])",
|
|
7
|
+
"select:not([disabled])",
|
|
8
|
+
"textarea:not([disabled])",
|
|
9
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
10
|
+
].join(",");
|
|
11
|
+
function isolateModal(root) {
|
|
12
|
+
const snapshots = [];
|
|
13
|
+
let branch = root;
|
|
14
|
+
while (branch?.parentElement) {
|
|
15
|
+
for (const sibling of branch.parentElement.children) {
|
|
16
|
+
if (sibling === branch || !(sibling instanceof HTMLElement)) continue;
|
|
17
|
+
snapshots.push({
|
|
18
|
+
element: sibling,
|
|
19
|
+
inert: sibling.inert === true,
|
|
20
|
+
ariaHidden: sibling.getAttribute("aria-hidden")
|
|
21
|
+
});
|
|
22
|
+
sibling.inert = true;
|
|
23
|
+
sibling.setAttribute("aria-hidden", "true");
|
|
24
|
+
}
|
|
25
|
+
branch = branch.parentElement;
|
|
26
|
+
if (branch === document.body) break;
|
|
27
|
+
}
|
|
28
|
+
return snapshots;
|
|
29
|
+
}
|
|
30
|
+
function useModalDialog({
|
|
31
|
+
rootRef,
|
|
32
|
+
dialogRef,
|
|
33
|
+
initialFocusRef,
|
|
34
|
+
returnFocusRef,
|
|
35
|
+
onClose
|
|
36
|
+
}) {
|
|
37
|
+
const onCloseRef = useRef(onClose);
|
|
38
|
+
onCloseRef.current = onClose;
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const root = rootRef.current;
|
|
41
|
+
const dialog = dialogRef.current;
|
|
42
|
+
if (!root || !dialog) return;
|
|
43
|
+
const previousFocus = returnFocusRef?.current ?? (document.activeElement instanceof HTMLElement ? document.activeElement : null);
|
|
44
|
+
const inertSnapshots = isolateModal(root);
|
|
45
|
+
const activeWithinDialog = document.activeElement instanceof HTMLElement && dialog.contains(document.activeElement) ? document.activeElement : null;
|
|
46
|
+
const focusTarget = initialFocusRef?.current ?? activeWithinDialog ?? dialog.querySelector(FOCUSABLE_SELECTOR) ?? dialog;
|
|
47
|
+
focusTarget.focus();
|
|
48
|
+
if (focusTarget instanceof HTMLInputElement || focusTarget instanceof HTMLTextAreaElement) {
|
|
49
|
+
focusTarget.select();
|
|
50
|
+
}
|
|
51
|
+
const handleKeyDown = (event) => {
|
|
52
|
+
if (event.key === "Escape") {
|
|
53
|
+
event.preventDefault();
|
|
54
|
+
event.stopPropagation();
|
|
55
|
+
onCloseRef.current();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (event.key !== "Tab") return;
|
|
59
|
+
const focusable = [...dialog.querySelectorAll(FOCUSABLE_SELECTOR)].filter(
|
|
60
|
+
(element) => !element.hidden && element.getAttribute("aria-hidden") !== "true" && element.getAttribute("aria-disabled") !== "true"
|
|
61
|
+
);
|
|
62
|
+
if (focusable.length === 0) {
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
dialog.focus();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const first = focusable[0];
|
|
68
|
+
const last = focusable[focusable.length - 1];
|
|
69
|
+
if (event.shiftKey && (document.activeElement === first || !dialog.contains(document.activeElement))) {
|
|
70
|
+
event.preventDefault();
|
|
71
|
+
last.focus();
|
|
72
|
+
} else if (!event.shiftKey && (document.activeElement === last || !dialog.contains(document.activeElement))) {
|
|
73
|
+
event.preventDefault();
|
|
74
|
+
first.focus();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
document.addEventListener("keydown", handleKeyDown, true);
|
|
78
|
+
return () => {
|
|
79
|
+
document.removeEventListener("keydown", handleKeyDown, true);
|
|
80
|
+
for (const snapshot of inertSnapshots) {
|
|
81
|
+
snapshot.element.inert = snapshot.inert;
|
|
82
|
+
if (snapshot.ariaHidden === null) snapshot.element.removeAttribute("aria-hidden");
|
|
83
|
+
else snapshot.element.setAttribute("aria-hidden", snapshot.ariaHidden);
|
|
84
|
+
}
|
|
85
|
+
if (previousFocus?.isConnected) previousFocus.focus();
|
|
86
|
+
};
|
|
87
|
+
}, [dialogRef, initialFocusRef, returnFocusRef, rootRef]);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export {
|
|
91
|
+
useModalDialog
|
|
92
|
+
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { RefObject } from 'react';
|
|
3
|
+
import { AudioTrack, ScheduledClip, Theme, Doc, Block, MediaProvider, SurfaceScheme } from '@bendyline/squisq/schemas';
|
|
4
|
+
import { a as AudioController } from '../AudioController-DwMsPe38.js';
|
|
5
|
+
export { A as AudioActions, b as AudioState } from '../AudioController-DwMsPe38.js';
|
|
6
|
+
import { ViewportConfig, ViewportOrientation } from '@bendyline/squisq/doc';
|
|
7
|
+
import { ResourcePolicy } from '@bendyline/squisq/markdown';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* useAudioSync Hook
|
|
11
|
+
*
|
|
12
|
+
* Synchronizes playback state with an audio element. Provides current
|
|
13
|
+
* playback time, playing state, and methods to control audio playback.
|
|
14
|
+
*
|
|
15
|
+
* Handles multiple audio segments (MP3 files) by tracking which segment
|
|
16
|
+
* is currently playing and calculating the overall timeline position.
|
|
17
|
+
*
|
|
18
|
+
* This is the HTML5 Audio implementation of the AudioController interface.
|
|
19
|
+
* Hosts that drive audio through an external player (e.g. a native shell)
|
|
20
|
+
* can supply their own AudioController to DocPlayer instead of this hook.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
type AudioSyncMode = 'media' | 'synthetic';
|
|
24
|
+
declare function useAudioSync(audioRef: RefObject<HTMLAudioElement>, audioTrack: AudioTrack | undefined, basePath?: string, enabled?: boolean, mode?: AudioSyncMode): AudioController;
|
|
25
|
+
|
|
26
|
+
interface ModalDialogOptions {
|
|
27
|
+
/** The backdrop/portal root. Siblings of this branch are made inert. */
|
|
28
|
+
rootRef: RefObject<HTMLElement | null>;
|
|
29
|
+
/** The element with `role="dialog"`; focus is trapped within it. */
|
|
30
|
+
dialogRef: RefObject<HTMLElement | null>;
|
|
31
|
+
initialFocusRef?: RefObject<HTMLElement | null>;
|
|
32
|
+
/** Explicit opener/owner to restore after unmount (important when a child uses autofocus). */
|
|
33
|
+
returnFocusRef?: RefObject<HTMLElement | null>;
|
|
34
|
+
onClose: () => void;
|
|
35
|
+
}
|
|
36
|
+
/** Shared focus, keyboard, background-isolation, and restoration behavior for modal dialogs. */
|
|
37
|
+
declare function useModalDialog({ rootRef, dialogRef, initialFocusRef, returnFocusRef, onClose, }: ModalDialogOptions): void;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* useMediaSchedule
|
|
41
|
+
*
|
|
42
|
+
* Pure follower of the playback clock for the media-clip model. Given the
|
|
43
|
+
* resolved {@link ScheduledClip}s and the current time, it returns the clips
|
|
44
|
+
* the player should mount and which of them are active right now.
|
|
45
|
+
* {@link MediaClipLayer} consumes this to drive one hidden `<audio>` /
|
|
46
|
+
* full-bleed `<video>` element per clip. (Annotation-authored clips all render
|
|
47
|
+
* at the player level; template-produced `VideoLayer`s are a separate path and
|
|
48
|
+
* are not part of the schedule.)
|
|
49
|
+
*
|
|
50
|
+
* It owns no clock: `currentTime`/`isPlaying` come from the existing
|
|
51
|
+
* `useAudioSync` provider via `DocPlayer`. With an empty schedule it returns
|
|
52
|
+
* empty lists, so documents without the new media model are unaffected.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
interface MediaScheduleController {
|
|
56
|
+
/** Clips the player mounts (every scheduled clip). */
|
|
57
|
+
renderClips: ScheduledClip[];
|
|
58
|
+
/** Ids of clips whose [absoluteStart, absoluteEnd) contains currentTime. */
|
|
59
|
+
activeIds: Set<string>;
|
|
60
|
+
}
|
|
61
|
+
declare function useMediaSchedule(schedule: ScheduledClip[], currentTime: number): MediaScheduleController;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* useDocPlayback Hook
|
|
65
|
+
*
|
|
66
|
+
* Manages the playback state for a visual doc, including which block
|
|
67
|
+
* is currently active, transition states, and synchronization with audio.
|
|
68
|
+
*
|
|
69
|
+
* This hook provides:
|
|
70
|
+
* - Current block determination based on time
|
|
71
|
+
* - Transition tracking (entering/exiting blocks)
|
|
72
|
+
* - Manual navigation (next/prev block)
|
|
73
|
+
* - Time-based seeking
|
|
74
|
+
* - Automatic expansion of template blocks
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
interface PlaybackState {
|
|
78
|
+
/** Currently visible block */
|
|
79
|
+
currentBlock: Block | null;
|
|
80
|
+
/** Index of current block */
|
|
81
|
+
currentBlockIndex: number;
|
|
82
|
+
/** Previous block (for transitions) */
|
|
83
|
+
previousBlock: Block | null;
|
|
84
|
+
/** Whether current block is entering */
|
|
85
|
+
isEntering: boolean;
|
|
86
|
+
/** Whether previous block is exiting */
|
|
87
|
+
isExiting: boolean;
|
|
88
|
+
/** Time relative to current block start */
|
|
89
|
+
blockTime: number;
|
|
90
|
+
/** Progress through current block (0-1) */
|
|
91
|
+
blockProgress: number;
|
|
92
|
+
/** Overall progress through doc (0-1) */
|
|
93
|
+
docProgress: number;
|
|
94
|
+
/** Expanded blocks (templates converted to full blocks with layers) */
|
|
95
|
+
blocks: Block[];
|
|
96
|
+
}
|
|
97
|
+
interface PlaybackActions {
|
|
98
|
+
/** Go to next block */
|
|
99
|
+
nextBlock: () => void;
|
|
100
|
+
/** Go to previous block */
|
|
101
|
+
prevBlock: () => void;
|
|
102
|
+
/** Go to specific block by index */
|
|
103
|
+
goToBlock: (index: number) => void;
|
|
104
|
+
/**
|
|
105
|
+
* Let the identified block enter without remounting the outgoing block.
|
|
106
|
+
* Used when another interaction (such as a swipe) already removed it.
|
|
107
|
+
*/
|
|
108
|
+
suppressOutgoingForNextBlock: (blockId: string) => void;
|
|
109
|
+
}
|
|
110
|
+
interface UseDocPlaybackOptions {
|
|
111
|
+
/** Target viewport used to materialize template blocks. */
|
|
112
|
+
viewport?: ViewportConfig;
|
|
113
|
+
/** Active theme used for materialization and transition defaults. */
|
|
114
|
+
theme?: Theme;
|
|
115
|
+
/** Host seek callback used by block navigation actions. */
|
|
116
|
+
onSeek?: (time: number) => void;
|
|
117
|
+
}
|
|
118
|
+
declare function useDocPlayback(script: Doc | null, currentTime: number, options?: UseDocPlaybackOptions): PlaybackState & PlaybackActions;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* useViewportOrientation Hook
|
|
122
|
+
*
|
|
123
|
+
* Detects the current viewport orientation and returns the appropriate
|
|
124
|
+
* VIEWPORT_PRESET for rendering docs. Automatically updates when
|
|
125
|
+
* the window is resized.
|
|
126
|
+
*
|
|
127
|
+
* Thresholds:
|
|
128
|
+
* - Portrait: height > width * 1.2 (significantly taller than wide)
|
|
129
|
+
* - Square: width and height within 20% of each other
|
|
130
|
+
* - Landscape: width > height * 1.2 (significantly wider than tall)
|
|
131
|
+
*/
|
|
132
|
+
|
|
133
|
+
interface UseViewportOrientationResult {
|
|
134
|
+
/** Current viewport preset configuration */
|
|
135
|
+
viewport: ViewportConfig;
|
|
136
|
+
/** Current orientation name */
|
|
137
|
+
orientation: ViewportOrientation;
|
|
138
|
+
/** Current window dimensions */
|
|
139
|
+
windowSize: {
|
|
140
|
+
width: number;
|
|
141
|
+
height: number;
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Hook to detect viewport orientation and return appropriate preset.
|
|
146
|
+
* Updates automatically when window is resized.
|
|
147
|
+
*/
|
|
148
|
+
declare function useViewportOrientation(): UseViewportOrientationResult;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* React context holding the current MediaProvider (or null if none provided).
|
|
152
|
+
*/
|
|
153
|
+
declare const MediaContext: react.Context<MediaProvider | null>;
|
|
154
|
+
/**
|
|
155
|
+
* Policy for document-controlled media URLs. Hosts rendering untrusted
|
|
156
|
+
* documents can provide `LOCAL_ONLY_RESOURCE_POLICY` or an explicit host
|
|
157
|
+
* allow-list without changing their MediaProvider.
|
|
158
|
+
*/
|
|
159
|
+
declare const ResourcePolicyContext: react.Context<ResourcePolicy>;
|
|
160
|
+
declare function useResourcePolicy(): ResourcePolicy;
|
|
161
|
+
/**
|
|
162
|
+
* Hook to access the current MediaProvider from context.
|
|
163
|
+
* Returns null if no provider is set.
|
|
164
|
+
*/
|
|
165
|
+
declare function useMediaProvider(): MediaProvider | null;
|
|
166
|
+
/**
|
|
167
|
+
* Hook to resolve a media URL via the MediaProvider (if available),
|
|
168
|
+
* falling back to basePath-based resolution.
|
|
169
|
+
*
|
|
170
|
+
* Returns the resolved URL string. Updates when the provider or path changes.
|
|
171
|
+
*
|
|
172
|
+
* @param relativePath - Relative media path from the document (e.g., 'hero.jpg')
|
|
173
|
+
* @param basePath - Fallback base path for URL construction
|
|
174
|
+
*/
|
|
175
|
+
declare function useMediaUrl(relativePath: string, basePath: string): string;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Live-track `prefers-color-scheme` and return a stable SurfaceScheme.
|
|
179
|
+
* `enabled: false` short-circuits to LIGHT_SURFACE (callers pass `false`
|
|
180
|
+
* when a static surface was provided so the hook never observes the
|
|
181
|
+
* media query). The `MediaQueryList` and the `subscribe`/`getSnapshot`
|
|
182
|
+
* callbacks are memoized so `useSyncExternalStore` doesn't resubscribe on
|
|
183
|
+
* every parent render.
|
|
184
|
+
*/
|
|
185
|
+
declare function useAutoSurface(enabled: boolean): SurfaceScheme;
|
|
186
|
+
|
|
187
|
+
export { AudioController, MediaContext, type MediaScheduleController, type ModalDialogOptions, ResourcePolicyContext, type UseDocPlaybackOptions, useAudioSync, useAutoSurface, useDocPlayback, useMediaProvider, useMediaSchedule, useMediaUrl, useModalDialog, useResourcePolicy, useViewportOrientation };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useModalDialog
|
|
3
|
+
} from "../chunk-YQC3AXXU.js";
|
|
4
|
+
import {
|
|
5
|
+
useAudioSync,
|
|
6
|
+
useDocPlayback,
|
|
7
|
+
useMediaSchedule,
|
|
8
|
+
useViewportOrientation
|
|
9
|
+
} from "../chunk-NWZQGZIJ.js";
|
|
10
|
+
import {
|
|
11
|
+
useAutoSurface
|
|
12
|
+
} from "../chunk-TT6ENR6T.js";
|
|
13
|
+
import {
|
|
14
|
+
MediaContext,
|
|
15
|
+
ResourcePolicyContext,
|
|
16
|
+
useMediaProvider,
|
|
17
|
+
useMediaUrl,
|
|
18
|
+
useResourcePolicy
|
|
19
|
+
} from "../chunk-LR3AIGDD.js";
|
|
20
|
+
export {
|
|
21
|
+
MediaContext,
|
|
22
|
+
ResourcePolicyContext,
|
|
23
|
+
useAudioSync,
|
|
24
|
+
useAutoSurface,
|
|
25
|
+
useDocPlayback,
|
|
26
|
+
useMediaProvider,
|
|
27
|
+
useMediaSchedule,
|
|
28
|
+
useMediaUrl,
|
|
29
|
+
useModalDialog,
|
|
30
|
+
useResourcePolicy,
|
|
31
|
+
useViewportOrientation
|
|
32
|
+
};
|