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

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
@@ -138,6 +138,7 @@ Both layout elements support:
138
138
  - `content-max-width-with-passage` (number, optional): max width in px when passages are present. Clamped to 320–2200. Unset by default (layout uses available width).
139
139
  - `split-pane-min-region-width` (number, optional): splitpane minimum pane width in px. Clamped to 160–1200. Unset by default (split bounds stay at 20–80). (Ignored by vertical layout; supported for API parity.)
140
140
  - `split-pane-collapse-strategy` (string, optional): splitpane stacked-mode strategy. Supported values: `tabbed` (default) and `vertical`. (Ignored by vertical/tabbed layouts; supported for API parity.)
141
+ - `base-heading-level` (number, optional): the heading level this player's card headings occupy, and the level every descendant's outline derives from. Clamped to 1–6; default 2. See [Heading structure](#heading-structure).
141
142
  - `show-toolbar` (boolean-like): accepts `true/false` and common string forms (`"true"`, `"false"`, `"1"`, `"0"`, `"yes"`, `"no"`)
142
143
  - Host extension props (JS properties only): `toolRegistry`, `sectionHostButtons`, `itemHostButtons`, `passageHostButtons`, `hooks`
143
144
 
@@ -170,6 +171,51 @@ To force splitpane stacked mode to use vertical rendering:
170
171
 
171
172
  By default, splitpane stacked mode uses tabs. The dedicated `pie-section-player-tabbed` layout also always renders passage/items tabs when passages are present.
172
173
 
174
+ ### Heading structure
175
+
176
+ The player publishes one number and every descendant derives its outline from it.
177
+ Set `base-heading-level` to the level the cards should occupy in the surrounding
178
+ page — 2 when the page has its own `<h1>` above the player, 3 when the player sits
179
+ under an `<h2>`, and so on:
180
+
181
+ ```html
182
+ <pie-section-player-splitpane base-heading-level="3"></pie-section-player-splitpane>
183
+ ```
184
+
185
+ At the default of 2 that produces:
186
+
187
+ ```
188
+ h2 Passage <- passage card heading
189
+ h3 Sea Turtles in Trouble <- passage title
190
+ h4 Danger on Land <- authored data-heading content
191
+ h2 Question 1 <- item card heading
192
+ h3 Part A <- authored data-heading content in the prompt
193
+ ```
194
+
195
+ The two content kinds derive different levels from the same number, and the
196
+ difference is deliberate. An item card's heading *is* the item's heading, so the
197
+ item player is told not to emit a screen-reader item heading of its own — one at
198
+ that level already exists, and a second would read as its sibling. A passage
199
+ card's heading is a group label, so the passage player is told to start one level
200
+ deeper, putting the passage's own title beneath it.
201
+
202
+ Authored `data-heading="headingN"` markup in passages and prompts becomes real
203
+ heading elements only when a level is published, which the player now always does.
204
+ Content authored against
205
+ [PIE-151](https://illuminate.atlassian.net/browse/PIE-151) therefore renders as
206
+ structure without any host configuration.
207
+
208
+ A host that needs the element's own screen-reader item heading — because it is not
209
+ supplying question headings of its own — overrides per player through the runtime:
210
+
211
+ ```js
212
+ sectionPlayer.runtime = { player: { includeSrHeading: true } };
213
+ ```
214
+
215
+ The pattern behind this, and the reason the value is published rather than pushed,
216
+ is in
217
+ [`docs/architecture/composition-context.md`](../../docs/architecture/composition-context.md).
218
+
173
219
  ### Tab styling hooks
174
220
 
175
221
  `pie-section-player-tabbed` and splitpane `tabbed` collapse mode expose canonical `pie-*`
@@ -232,34 +278,47 @@ When both max-width attributes are set, the with-passage cap resolves to the gre
232
278
  of the two configured values (after clamp), so with-passage mode never ends up narrower
233
279
  than no-passage mode.
234
280
 
235
- ### Item card media region
281
+ ### Content-card tool surfaces
282
+
283
+ Item and passage cards offer two content-scoped host surfaces:
284
+
285
+ - `content-lead` is a full-width stack before the authored player content.
286
+ - `content-media` is the resizable region beside that content.
287
+
288
+ They are host surfaces, not features: whatever capability declares one of those
289
+ names in `surfaces` may render there, and this package names no capability. A
290
+ surface becomes mountable only when policy grants the capability and its own
291
+ `requiresAuthoredContent` resolves, so an item or passage without the authored
292
+ resource gets no dead affordance.
236
293
 
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.
294
+ Today's occupant is `@pie-players/pie-tool-sign-language` — a signed (ASL)
295
+ translation gated on the `signLanguage` PNP support. It is not part of the
296
+ packaged capability set: a deployment opts in by registering it on the tool
297
+ registry it passes to the player.
242
298
 
243
- It sits to the right of the question content and is resizable via a
299
+ The `content-media` adapter sits to the right of the content and is resizable via a
244
300
  keyboard-accessible divider (`role="separator"`; arrow keys, `Home`/`End`,
245
301
  `Escape` to cancel a drag). Below a card width of 560px the region stacks under
246
302
  the content and the divider is withdrawn. Placement is fixed in this iteration:
247
303
  there is no orientation toggle and no free repositioning.
248
304
 
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
- ```
305
+ This package owns the region's share of the card width and nothing inside it. A
306
+ capability mounted here sizes its own content: signing legibility needs height for
307
+ hands and face, so it is sized by an aspect-ratio target with a height floor
308
+ rather than by width alone. The `--pie-section-player-item-media-*` tokens hosts
309
+ set for that belong to `@pie-players/pie-tool-sign-language` and are documented
310
+ with their defaults in [its README](../tool-sign-language/README.md) — they keep
311
+ the `pie-section-player` prefix because hosts already set them by those names.
312
+
313
+ All three section-player surfaces (`content-lead`, `content-media`, and
314
+ `section-overlay`) share one internal Tool Surface Host. It observes live
315
+ `ToolRegistry` mutations and policy/catalog changes, preserves registration
316
+ order across lazy loads, synchronizes an existing element when its current
317
+ context changes, and always calls `destroy()` before removing it. Surface
318
+ failures are isolated per capability and emitted as recoverable
319
+ `framework-error` warnings; they never block readiness or remove another
320
+ working capability. A `renderSurface()` result of `null` is a normal
321
+ mountable-but-unoccupied result, not an error.
263
322
 
264
323
  ### API direction: CE defaults first, JS customization for advanced cases
265
324
 
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The media region a content card docks an alternate into: its surface name, and
3
+ * how wide it is.
4
+ *
5
+ * Sizing only. Which capability fills the region, and whether it has anything to
6
+ * show, belongs to the capability: the card asks the registry for whatever is
7
+ * registered on the surface below and knows nothing else about it. The
8
+ * signed-alternate resolution that used to live here moved to
9
+ * `@pie-players/pie-tool-sign-language` behind that capability's own
10
+ * `requiresAuthoredContent`.
11
+ */
12
+ /**
13
+ * Host slot for media docked beside a card's content.
14
+ *
15
+ * Named for the relationship rather than for one card kind, because item cards
16
+ * and passage cards open the same slot: an alternate is authored against a
17
+ * content node, and a passage owns content nodes exactly as an item does. A
18
+ * capability declares this surface once and reaches both.
19
+ */
20
+ export declare const CONTENT_MEDIA_SURFACE = "content-media";
21
+ /**
22
+ * Host slot for a text alternate that has to be read in order with the content:
23
+ * full width, above the card body, in document flow.
24
+ *
25
+ * Separate from {@link CONTENT_MEDIA_SURFACE} because the geometry is the whole
26
+ * difference. Docked media is watched beside the content and is sized by an
27
+ * aspect ratio; a transcript or a plain-language alternate is read *before* the
28
+ * control it belongs to, which a sticky side column cannot express.
29
+ */
30
+ export declare const CONTENT_LEAD_SURFACE = "content-lead";
31
+ /**
32
+ * Default and bounds for the media region's share of the card width.
33
+ *
34
+ * Docked media generally needs height rather than width — a person on camera
35
+ * needs room for hands and face — so the region is sized by an aspect-ratio
36
+ * target with a minimum height rather than by width alone. A flat viewport
37
+ * percentage either wastes space on a short clip or crushes it on a narrow
38
+ * device. The percentage below only decides how the card's width is split;
39
+ * legibility is defended by the mounted element's own CSS.
40
+ */
41
+ export declare const MEDIA_REGION_DEFAULT_PERCENT = 34;
42
+ export declare const MEDIA_REGION_MIN_PERCENT = 20;
43
+ export declare const MEDIA_REGION_MAX_PERCENT = 55;
44
+ /** Below this card width the split is dropped and the region stacks. */
45
+ export declare const MEDIA_REGION_STACK_BREAKPOINT_PX = 560;
46
+ export declare function clampMediaRegionPercent(value: number, min?: number, max?: number): number;
47
+ /**
48
+ * Convert a pointer drag into a media-region width percentage.
49
+ *
50
+ * Container-relative, unlike the passage/items divider's fixed 0.1%-per-pixel
51
+ * factor: the same drag has to mean the same thing in a wide card and a narrow
52
+ * one. The region is on the right, so dragging left grows it.
53
+ */
54
+ export declare function mediaRegionPercentFromDrag(args: {
55
+ startPercent: number;
56
+ deltaX: number;
57
+ containerWidthPx: number;
58
+ min?: number;
59
+ max?: number;
60
+ }): number;
@@ -1,6 +1,39 @@
1
1
  import { ItemEntity } from '@pie-players/pie-players-shared/types';
2
2
  import { SectionCompositionModel } from '../../controllers/types.js';
3
3
  import { PlayerElementParams } from './player-action.js';
4
+ /**
5
+ * Heading depth as composition context.
6
+ *
7
+ * Where an item's headings belong in a document outline is a fact about the
8
+ * page, not about the item: the same item renders under a host `<h1>` here and
9
+ * three levels down there. The section player is the publisher — it knows the
10
+ * level its own card headings occupy — and each PIE element is a resolver,
11
+ * reading the level off the player host and deriving the outline it emits.
12
+ * `docs/architecture/composition-context.md` states the pattern and its
13
+ * invariants.
14
+ *
15
+ * The two content kinds get different values from the same level, and the
16
+ * difference is not arbitrary:
17
+ *
18
+ * - An item card's heading ("Question 3") *is* the item's heading. The element
19
+ * must not emit a second one at that level, so it renders no screen-reader
20
+ * item heading and nests authored content one level below.
21
+ * - A passage card's heading ("Passage") is a group label. The passage's own
22
+ * title is real content and belongs beneath it, so the passage player's level
23
+ * is one deeper again.
24
+ *
25
+ * At the default this yields card `h2` -> passage title `h3` -> passage content
26
+ * `h4`, and question `h2` -> prompt content `h3`, which is the outline
27
+ * PIE-159 specifies.
28
+ */
29
+ export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
30
+ /** Matches the level the cards rendered at before it was configurable. */
31
+ export declare const DEFAULT_SECTION_BASE_HEADING_LEVEL: HeadingLevel;
32
+ export declare function normalizeBaseHeadingLevel(value: unknown): HeadingLevel;
33
+ /** Card heading fills the item's slot, so the element adds none of its own. */
34
+ export declare function itemHeadingContext(level: HeadingLevel): Record<string, unknown>;
35
+ /** Card heading is a group label; the passage's own title sits one below it. */
36
+ export declare function passageHeadingContext(level: HeadingLevel): Record<string, unknown>;
4
37
  export type LayoutCompositionSnapshot = {
5
38
  compositionModel: SectionCompositionModel;
6
39
  passages: SectionCompositionModel["passages"];
@@ -17,6 +50,7 @@ export declare function getPassagePlayerParams(args: {
17
50
  resolvedPlayerAttributes: Record<string, string>;
18
51
  resolvedPlayerProps: Record<string, unknown>;
19
52
  playerStrategy: string;
53
+ baseHeadingLevel?: HeadingLevel;
20
54
  }): PlayerElementParams;
21
55
  export declare function getItemPlayerParams(args: {
22
56
  item: ItemEntity;
@@ -26,6 +60,7 @@ export declare function getItemPlayerParams(args: {
26
60
  resolvedPlayerProps: Record<string, unknown>;
27
61
  playerStrategy: string;
28
62
  itemIndex?: number;
63
+ baseHeadingLevel?: HeadingLevel;
29
64
  }): PlayerElementParams;
30
65
  export declare function getCanonicalItemId(args: {
31
66
  compositionModel: SectionCompositionModel;
@@ -0,0 +1,28 @@
1
+ import { CatalogOwnerContext, ToolRegistry, ToolSurfaceServices } from '@pie-players/pie-assessment-toolkit';
2
+ export type ToolSurfaceScope = {
3
+ kind: "content";
4
+ ownerContext: CatalogOwnerContext;
5
+ } | {
6
+ kind: "section";
7
+ assessmentId: string;
8
+ sectionId: string;
9
+ };
10
+ export interface ToolSurfaceHostInput {
11
+ anchor: HTMLElement | null;
12
+ surface: string;
13
+ registry: ToolRegistry | null;
14
+ services: ToolSurfaceServices;
15
+ scope: ToolSurfaceScope;
16
+ }
17
+ export interface ToolSurfaceHostSnapshot {
18
+ /** At least one capability is eligible, including one still loading. */
19
+ mountable: boolean;
20
+ /** At least one capability successfully mounted an element. */
21
+ occupied: boolean;
22
+ }
23
+ export interface ToolSurfaceHost {
24
+ update(input: ToolSurfaceHostInput): void;
25
+ destroy(): void;
26
+ }
27
+ /** Create one lifecycle owner for one host surface anchor. */
28
+ export declare function createToolSurfaceHost(onSnapshot: (snapshot: ToolSurfaceHostSnapshot) => void): ToolSurfaceHost;