@pie-players/pie-section-player 0.3.63 → 0.3.64

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/README.md CHANGED
@@ -232,6 +232,35 @@ When both max-width attributes are set, the with-passage cap resolves to the gre
232
232
  of the two configured values (after clamp), so with-passage mode never ends up narrower
233
233
  than no-passage mode.
234
234
 
235
+ ### Item card media region
236
+
237
+ An item card can carry a `data-region="media"` region beside its content region,
238
+ holding a resolved accessibility-catalog media card — today a sign-language
239
+ (ASL) video, gated on the `signLanguage` PNP support. The region only appears
240
+ when the item carries a matching catalog card *and* policy grants the feature, so
241
+ there is no dead affordance on items without signing.
242
+
243
+ It sits to the right of the question content and is resizable via a
244
+ keyboard-accessible divider (`role="separator"`; arrow keys, `Home`/`End`,
245
+ `Escape` to cancel a drag). Below a card width of 560px the region stacks under
246
+ the content and the divider is withdrawn. Placement is fixed in this iteration:
247
+ there is no orientation toggle and no free repositioning.
248
+
249
+ Signing legibility needs height for hands and face, so the video is sized by an
250
+ aspect-ratio target with a height floor rather than by width alone. Hosts can
251
+ retune it:
252
+
253
+ - `--pie-section-player-item-media-aspect-ratio` (default `3 / 4`)
254
+ - `--pie-section-player-item-media-min-height` (default `220px`)
255
+ - `--pie-section-player-item-media-max-height` (default `60vh`)
256
+
257
+ ```css
258
+ pie-section-player-item-card {
259
+ --pie-section-player-item-media-aspect-ratio: 4 / 5;
260
+ --pie-section-player-item-media-min-height: 260px;
261
+ }
262
+ ```
263
+
235
264
  ### API direction: CE defaults first, JS customization for advanced cases
236
265
 
237
266
  The intended usage model is:
@@ -0,0 +1,88 @@
1
+ import { AccessibilityCatalogResolverApi, CatalogOwnerContext, SignLanguageMedia } from '@pie-players/pie-assessment-toolkit';
2
+ import { ItemEntity } from '@pie-players/pie-players-shared/types';
3
+ /**
4
+ * QTI 3.0 / AfA support id gating signed alternates. Excluded from the computed
5
+ * default PNP profile by `ACCOMMODATION_ONLY_SUPPORT_IDS`, so it is only ever
6
+ * granted deliberately.
7
+ */
8
+ export declare const SIGN_LANGUAGE_FEATURE_ID = "signLanguage";
9
+ /** A catalog on this item that carries at least one sign-language card. */
10
+ export interface SignLanguageCatalogRef {
11
+ catalogId: string;
12
+ /** Set when the catalog is owned by one model rather than the item root. */
13
+ modelId?: string;
14
+ }
15
+ export interface ResolvedSignLanguageAlternate extends SignLanguageMedia {
16
+ catalogId: string;
17
+ }
18
+ /**
19
+ * Collect the catalog identifiers on an item that carry signing cards.
20
+ *
21
+ * The region resolves through the catalog resolver, but the resolver is keyed by
22
+ * identifier within an owner scope — something has to say *which* identifiers
23
+ * this item put in play, and whether each belongs to the item or to one model.
24
+ * That is the same walk the runtime does when it registers an item's catalogs,
25
+ * so it is borrowed rather than repeated: a ref can only name a scope
26
+ * registration actually files under. Only the "does this catalog carry signing"
27
+ * filter is ours.
28
+ *
29
+ * The owner identity here is a placeholder: the assessment and section ids are
30
+ * not known at extraction time and are supplied by the caller at lookup time
31
+ * (see `resolveSignLanguageAlternate`). Nothing but `modelId` is read from the
32
+ * walk's contexts.
33
+ */
34
+ export declare function collectSignLanguageCatalogRefs(item: ItemEntity | null | undefined): SignLanguageCatalogRef[];
35
+ export interface SignLanguageLookupArgs {
36
+ resolver: AccessibilityCatalogResolverApi;
37
+ refs: SignLanguageCatalogRef[];
38
+ /** Owner context for the item, without `modelId` — added per ref. */
39
+ ownerContext: CatalogOwnerContext;
40
+ /** Sign language the learner is entitled to, ISO 639-3. */
41
+ requestedSignLang?: string;
42
+ }
43
+ /**
44
+ * Resolve the signed alternate to show for an item, or `null`.
45
+ *
46
+ * Priority, language fallback and owner scoping are the resolver's job and are
47
+ * not re-implemented here. What *is* enforced here is the one deliberate
48
+ * departure from the resolver's default behaviour: **no cross-sign-language
49
+ * substitution.** The resolver's last fallback rung matches any card of the
50
+ * requested type regardless of language, which for spoken content is helpful
51
+ * and for signing is not — handing an ASL learner a BSL recording is worse than
52
+ * handing them nothing. So a card the resolver returns by that rung is accepted
53
+ * only if its language actually matches, or if the card asserts no language at
54
+ * all.
55
+ *
56
+ * MVP takes the first resolvable card: sample content is one recording per item.
57
+ * The refs stay per-node capable for when choice-level docking lands.
58
+ */
59
+ export declare function resolveSignLanguageAlternate(args: SignLanguageLookupArgs): ResolvedSignLanguageAlternate | null;
60
+ /**
61
+ * Default and bounds for the media region's share of the card width.
62
+ *
63
+ * Signing needs height for hands and face, so the region is sized by an
64
+ * aspect-ratio target with a minimum height rather than by width alone — a flat
65
+ * viewport percentage either wastes space on a short clip or crushes it on a
66
+ * narrow device. The percentage below only decides how the card's width is
67
+ * split; legibility is defended by the region's CSS.
68
+ */
69
+ export declare const MEDIA_REGION_DEFAULT_PERCENT = 34;
70
+ export declare const MEDIA_REGION_MIN_PERCENT = 20;
71
+ export declare const MEDIA_REGION_MAX_PERCENT = 55;
72
+ /** Below this card width the split is dropped and the region stacks. */
73
+ export declare const MEDIA_REGION_STACK_BREAKPOINT_PX = 560;
74
+ export declare function clampMediaRegionPercent(value: number, min?: number, max?: number): number;
75
+ /**
76
+ * Convert a pointer drag into a media-region width percentage.
77
+ *
78
+ * Container-relative, unlike the passage/items divider's fixed 0.1%-per-pixel
79
+ * factor: the same drag has to mean the same thing in a wide card and a narrow
80
+ * one. The region is on the right, so dragging left grows it.
81
+ */
82
+ export declare function mediaRegionPercentFromDrag(args: {
83
+ startPercent: number;
84
+ deltaX: number;
85
+ containerWidthPx: number;
86
+ min?: number;
87
+ max?: number;
88
+ }): number;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Registration dispatch for the item and passage shells.
3
+ *
4
+ * Registration is a statement of fact to the runtime, and the runtime takes it
5
+ * literally: a `pie-register` makes the toolkit unregister and re-register the
6
+ * content's accessibility catalogs, re-run `sectionEngine.register`, and
7
+ * re-notify the section controller. Both shells used to dispatch it from the
8
+ * same effect that attached their listeners, with `pie-unregister` in that
9
+ * effect's cleanup — so every re-run announced a teardown and a rebuild of state
10
+ * that had not moved, and between the two the content had no catalogs at all.
11
+ *
12
+ * Those effects re-run on prop changes, and a shell's props are re-applied
13
+ * whenever its parent card's template updates. That made the dispatch the far
14
+ * half of a cycle: a reader that re-renders in response to a catalog change
15
+ * re-applies a shell's props, which re-registers, which changes catalogs again.
16
+ * It shipped once at roughly a thousand rounds per item, ending in Svelte
17
+ * abandoning the update at its depth limit with the DOM half-applied — and every
18
+ * assertion about the rendered output passed while it was happening, because the
19
+ * elements were all present.
20
+ *
21
+ * So the dispatcher keeps the last identity it announced and says nothing when
22
+ * the next one matches.
23
+ */
24
+ export type ShellRegistrationIdentity = {
25
+ kind: "item" | "passage";
26
+ host: HTMLElement;
27
+ itemId: string;
28
+ canonicalItemId: string;
29
+ contentKind: string;
30
+ item: unknown;
31
+ };
32
+ export type ShellRegistrationDispatcher = {
33
+ /**
34
+ * Announce `identity` if it says anything new, and retire the live
35
+ * registration when there is nothing left to describe — a shell without a host
36
+ * or an id no longer stands for content, and leaving its registration behind
37
+ * would strand it in the runtime.
38
+ *
39
+ * Never dispatches `pie-unregister` before a re-register: both registration
40
+ * paths in the toolkit are keyed by element and replace what is there, so the
41
+ * unregister only ever created the gap.
42
+ */
43
+ sync: (identity: ShellRegistrationIdentity | null) => void;
44
+ /**
45
+ * Retire the live registration, replaying the identity it was made under
46
+ * rather than whatever the props say now — by the time a registration is
47
+ * retired the props may already describe its replacement, and unregistering
48
+ * under the new identity would leave the old one live.
49
+ *
50
+ * Belongs in a teardown that runs on teardown only. Attaching it to an effect
51
+ * that re-runs on prop changes is what made the churn.
52
+ */
53
+ retire: () => void;
54
+ };
55
+ export declare function createShellRegistrationDispatcher(): ShellRegistrationDispatcher;