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

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,6 +278,34 @@ 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
 
281
+ ### Item card media region
282
+
283
+ An item card can carry a `data-region="media"` region beside its content region.
284
+ It is a host surface named `item-media`, not a feature: whatever capability
285
+ declares `surfaces: ['item-media']` in its registration renders there, and this
286
+ package names none of them. The region appears only when policy grants the
287
+ capability *and* the capability's own `requiresAuthoredContent` resolves, so
288
+ there is no dead affordance on items that carry nothing.
289
+
290
+ Today's occupant is `@pie-players/pie-tool-sign-language` — a signed (ASL)
291
+ translation gated on the `signLanguage` PNP support. It is not part of the
292
+ packaged capability set: a deployment opts in by registering it on the tool
293
+ registry it passes to the player.
294
+
295
+ It sits to the right of the question content and is resizable via a
296
+ keyboard-accessible divider (`role="separator"`; arrow keys, `Home`/`End`,
297
+ `Escape` to cancel a drag). Below a card width of 560px the region stacks under
298
+ the content and the divider is withdrawn. Placement is fixed in this iteration:
299
+ there is no orientation toggle and no free repositioning.
300
+
301
+ This package owns the region's share of the card width and nothing inside it. A
302
+ capability mounted here sizes its own content: signing legibility needs height for
303
+ hands and face, so it is sized by an aspect-ratio target with a height floor
304
+ rather than by width alone. The `--pie-section-player-item-media-*` tokens hosts
305
+ set for that belong to `@pie-players/pie-tool-sign-language` and are documented
306
+ with their defaults in [its README](../tool-sign-language/README.md) — they keep
307
+ the `pie-section-player` prefix because hosts already set them by those names.
308
+
235
309
  ### API direction: CE defaults first, JS customization for advanced cases
236
310
 
237
311
  The intended usage model is:
@@ -0,0 +1,50 @@
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
+ * Default and bounds for the media region's share of the card width.
23
+ *
24
+ * Docked media generally needs height rather than width — a person on camera
25
+ * needs room for hands and face — so the region is sized by an aspect-ratio
26
+ * target with a minimum height rather than by width alone. A flat viewport
27
+ * percentage either wastes space on a short clip or crushes it on a narrow
28
+ * device. The percentage below only decides how the card's width is split;
29
+ * legibility is defended by the mounted element's own CSS.
30
+ */
31
+ export declare const MEDIA_REGION_DEFAULT_PERCENT = 34;
32
+ export declare const MEDIA_REGION_MIN_PERCENT = 20;
33
+ export declare const MEDIA_REGION_MAX_PERCENT = 55;
34
+ /** Below this card width the split is dropped and the region stacks. */
35
+ export declare const MEDIA_REGION_STACK_BREAKPOINT_PX = 560;
36
+ export declare function clampMediaRegionPercent(value: number, min?: number, max?: number): number;
37
+ /**
38
+ * Convert a pointer drag into a media-region width percentage.
39
+ *
40
+ * Container-relative, unlike the passage/items divider's fixed 0.1%-per-pixel
41
+ * factor: the same drag has to mean the same thing in a wide card and a narrow
42
+ * one. The region is on the right, so dragging left grows it.
43
+ */
44
+ export declare function mediaRegionPercentFromDrag(args: {
45
+ startPercent: number;
46
+ deltaX: number;
47
+ containerWidthPx: number;
48
+ min?: number;
49
+ max?: number;
50
+ }): 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,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;