@pie-players/pie-players-shared 0.3.75 → 1.0.0-beta.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.
Files changed (63) hide show
  1. package/README.md +88 -77
  2. package/dist/components/vendor/nds/README.md +4 -5
  3. package/dist/config/profile.d.ts +1 -1
  4. package/dist/config/profile.js +1 -1
  5. package/dist/i18n/index.d.ts +1 -1
  6. package/dist/i18n/index.js +1 -1
  7. package/dist/i18n/language-tags.d.ts +9 -1
  8. package/dist/i18n/language-tags.js +11 -1
  9. package/dist/i18n/messages/en-US.d.ts +33 -5
  10. package/dist/i18n/messages/en-US.js +33 -5
  11. package/dist/i18n/messages/nl-NL.d.ts +33 -5
  12. package/dist/i18n/messages/nl-NL.js +33 -5
  13. package/dist/i18n/provider.d.ts +14 -7
  14. package/dist/i18n/provider.js +26 -29
  15. package/dist/index.js +0 -1
  16. package/dist/instrumentation/providers/BaseInstrumentationProvider.js +0 -14
  17. package/dist/loader-config.d.ts +0 -2
  18. package/dist/loader-config.js +0 -2
  19. package/dist/loaders/element-loader.js +13 -6
  20. package/dist/loaders/esm-adapter.js +48 -37
  21. package/dist/loaders/iife-adapter.d.ts +3 -3
  22. package/dist/loaders/iife-adapter.js +36 -35
  23. package/dist/media/index.d.ts +4 -6
  24. package/dist/media/index.js +6 -8
  25. package/dist/object/index.d.ts +8 -3
  26. package/dist/object/index.js +8 -6
  27. package/dist/pie/asset-handler.d.ts +0 -17
  28. package/dist/pie/asset-handler.js +1 -1
  29. package/dist/pie/authoring.js +5 -7
  30. package/dist/pie/elements-rendered.d.ts +0 -2
  31. package/dist/pie/elements-rendered.js +1 -1
  32. package/dist/pie/index.d.ts +1 -0
  33. package/dist/pie/index.js +1 -0
  34. package/dist/pie/initialization.d.ts +13 -14
  35. package/dist/pie/initialization.js +61 -75
  36. package/dist/pie/item-session-contract.js +7 -9
  37. package/dist/pie/resource-monitor.d.ts +34 -32
  38. package/dist/pie/resource-monitor.js +152 -231
  39. package/dist/pie/stages.d.ts +4 -3
  40. package/dist/pie/stages.js +4 -3
  41. package/dist/pie/updates.js +5 -4
  42. package/dist/pie/utils.d.ts +0 -2
  43. package/dist/pie/utils.js +0 -2
  44. package/dist/pie/versioned-tag.d.ts +10 -0
  45. package/dist/pie/versioned-tag.js +27 -0
  46. package/dist/security/sanitize-item-markup.d.ts +4 -4
  47. package/dist/security/sanitize-item-markup.js +4 -4
  48. package/dist/security/sanitize-svg-icon.js +2 -2
  49. package/dist/security/wrap-overwide-images.d.ts +1 -1
  50. package/dist/security/wrap-overwide-images.js +1 -1
  51. package/dist/timed-media/media-element-source.js +1 -2
  52. package/dist/types/index.d.ts +88 -52
  53. package/dist/types/index.js +2 -2
  54. package/dist/ui/attribute-coercion.d.ts +5 -0
  55. package/dist/ui/attribute-coercion.js +6 -4
  56. package/dist/ui/content-styles.d.ts +6 -10
  57. package/dist/ui/content-styles.js +8 -13
  58. package/dist/ui/first-focusable.d.ts +0 -1
  59. package/dist/ui/first-focusable.js +0 -1
  60. package/dist/ui/focus-trap.js +2 -3
  61. package/dist/ui/scope-css.d.ts +23 -26
  62. package/dist/ui/scope-css.js +23 -26
  63. package/package.json +3 -2
@@ -4,9 +4,10 @@
4
4
  * A cohort (one section and attempt) moves through `composed`,
5
5
  * `engine-ready`, `interactive` and `disposed`, in that order. The section
6
6
  * runtime engine in `@pie-players/pie-assessment-toolkit` is the only emitter;
7
- * it dispatches the event on the layout element. `pie-loading-complete`
8
- * reports that every item loaded and is not a stage. `readiness.mode =
9
- * "strict"` only delays `interactive`.
7
+ * it dispatches the event on the layout element. `pie-loading-complete` is
8
+ * not a stage; it fires once per cohort when the section controller is ready
9
+ * and the element pre-warm has resolved. `readiness.mode = "strict"` only
10
+ * delays `interactive`.
10
11
  *
11
12
  * `status` is `entered`, or `failed` for the stage a non-recoverable
12
13
  * framework error stopped the chain at, with `skipped` for each later stage
@@ -4,9 +4,10 @@
4
4
  * A cohort (one section and attempt) moves through `composed`,
5
5
  * `engine-ready`, `interactive` and `disposed`, in that order. The section
6
6
  * runtime engine in `@pie-players/pie-assessment-toolkit` is the only emitter;
7
- * it dispatches the event on the layout element. `pie-loading-complete`
8
- * reports that every item loaded and is not a stage. `readiness.mode =
9
- * "strict"` only delays `interactive`.
7
+ * it dispatches the event on the layout element. `pie-loading-complete` is
8
+ * not a stage; it fires once per cohort when the section controller is ready
9
+ * and the element pre-warm has resolved. `readiness.mode = "strict"` only
10
+ * delays `interactive`.
10
11
  *
11
12
  * `status` is `entered`, or `failed` for the stage a non-recoverable
12
13
  * framework error stopped the chain at, with `skipped` for each later stage
@@ -8,7 +8,6 @@ import { createPieLogger, isGlobalDebugEnabled } from "./logger.js";
8
8
  import { findPieController } from "./scoring.js";
9
9
  import { defaultPieElementOptions } from "./types.js";
10
10
  import { findOrAddSession } from "./utils.js";
11
- // Create module-level logger (respects global debug flag - pass function for dynamic checking)
12
11
  const logger = createPieLogger("pie-updates", () => isGlobalDebugEnabled());
13
12
  const describeControllerShape = (controller) => {
14
13
  if (!controller)
@@ -67,8 +66,10 @@ const trackRemoval = (element) => {
67
66
  return () => wasConnected && !element.isConnected;
68
67
  };
69
68
  /**
70
- * Helper function to apply controller to element
71
- * Extracted to eliminate duplication and ensure consistent controller invocation
69
+ * Runs the controller's `model()` and assigns the result to the element, with
70
+ * the authored model's `id` and `element` as defaults, together with the
71
+ * session the controller may have written to. Assigns nothing once the element
72
+ * was removed.
72
73
  */
73
74
  const applyControllerToElement = async (element, model, elementSession, controller, env, logPrefix, wasRemoved, onElementSessionUpdate) => {
74
75
  logger.debug(`${logPrefix} Using controller, env:`, env);
@@ -118,7 +119,7 @@ const applyControllerToElement = async (element, model, elementSession, controll
118
119
  }
119
120
  catch (err) {
120
121
  logger.error(`${logPrefix} ❌ Controller error:`, err);
121
- throw err; // Re-throw - controller errors are fatal
122
+ throw err; // The caller reports it and falls back to the authored model.
122
123
  }
123
124
  };
124
125
  const resolveAndValidateUpdateOptions = (opts) => {
@@ -60,8 +60,6 @@ export declare const getPackageWithoutVersion: (packages: string) => string;
60
60
  /**
61
61
  * Find or add a session entry for a given element
62
62
  *
63
- * TODO: kinda gnarly, copied from player project
64
- *
65
63
  * @param data - Session data array
66
64
  * @param id - Model/element ID
67
65
  * @param element - Element tag name (optional)
package/dist/pie/utils.js CHANGED
@@ -103,8 +103,6 @@ export const getPackageWithoutVersion = (packages) => packages
103
103
  /**
104
104
  * Find or add a session entry for a given element
105
105
  *
106
- * TODO: kinda gnarly, copied from player project
107
- *
108
106
  * @param data - Session data array
109
107
  * @param id - Model/element ID
110
108
  * @param element - Element tag name (optional)
@@ -17,3 +17,13 @@ export declare function parseVersionedTagName(tagName: string): {
17
17
  export declare function toPackageVersionedTag(tagName: string, packageSpec: string, options?: {
18
18
  preserveUnversionedTag?: boolean;
19
19
  }): string;
20
+ /**
21
+ * The tags a player can define an authored element under, most specific first:
22
+ * the versioned tag for its package spec, aligned to the page's preloaded
23
+ * registration, then the authored tag, which an unversioned spec keeps. With no
24
+ * spec the authored tag is the only candidate.
25
+ *
26
+ * For code that looks an element's class up by tag without rendering it, such as
27
+ * reading what the class declares before the element is in the DOM.
28
+ */
29
+ export declare function runtimeElementTags(tagName: string, packageSpec?: string): string[];
@@ -1,3 +1,4 @@
1
+ import { alignPreloadedElementVersions } from "../loaders/preloaded-alignment.js";
1
2
  import { parsePackageName } from "./utils.js";
2
3
  const VERSION_DELIMITER = "--version-";
3
4
  const TAG_VERSION_PATTERN = "[0-9A-Za-z-]+";
@@ -45,3 +46,29 @@ export function toPackageVersionedTag(tagName, packageSpec, options = {}) {
45
46
  // package versions at runtime, which requires updating the cloned runtime tag.
46
47
  return `${baseName}${VERSION_DELIMITER}${targetEncodedVersion}`;
47
48
  }
49
+ /**
50
+ * The tags a player can define an authored element under, most specific first:
51
+ * the versioned tag for its package spec, aligned to the page's preloaded
52
+ * registration, then the authored tag, which an unversioned spec keeps. With no
53
+ * spec the authored tag is the only candidate.
54
+ *
55
+ * For code that looks an element's class up by tag without rendering it, such as
56
+ * reading what the class declares before the element is in the DOM.
57
+ */
58
+ export function runtimeElementTags(tagName, packageSpec) {
59
+ if (!packageSpec)
60
+ return [tagName];
61
+ let versioned;
62
+ try {
63
+ const { elements } = alignPreloadedElementVersions({
64
+ elements: { [tagName]: packageSpec },
65
+ });
66
+ versioned = toPackageVersionedTag(tagName, elements[tagName], {
67
+ preserveUnversionedTag: true,
68
+ });
69
+ }
70
+ catch {
71
+ return [tagName];
72
+ }
73
+ return versioned === tagName ? [tagName] : [versioned, tagName];
74
+ }
@@ -31,8 +31,8 @@ export interface SanitizeItemMarkupOptions {
31
31
  * Sanitize raw item/passage markup before it is injected into the DOM.
32
32
  *
33
33
  * - Strips `<script>`, event-handler attributes, unknown protocols and
34
- * a standard set of dangerous tags (`iframe`, `object`, `embed`, `base`,
35
- * `form`, `meta`, `link`).
34
+ * the other tags in `SANITIZER_FORBIDDEN_TAGS` (`iframe`, `object`, `embed`,
35
+ * `base`, `form`, `meta`, `link`, `style`, `foreignObject`).
36
36
  * - Preserves PIE custom elements (`pie-*`) and any extra tags listed in
37
37
  * `allowedCustomElements`.
38
38
  * - Marks elements that carry an authored color, as `markAuthoredColors`
@@ -46,8 +46,8 @@ export interface SanitizeItemMarkupOptions {
46
46
  export declare function sanitizeItemMarkup(markup: string, options?: SanitizeItemMarkupOptions): string;
47
47
  /**
48
48
  * Build the default `ItemMarkupSanitizer` used by the players. The returned
49
- * function is stable for a given set of allowed custom elements so callers
50
- * can safely use reference equality when deciding whether to re-sanitize.
49
+ * function captures the options once; each call returns a new function, so a
50
+ * caller that compares sanitizers by identity keeps the one it built.
51
51
  */
52
52
  export declare function createDefaultItemMarkupSanitizer(options?: SanitizeItemMarkupOptions): ItemMarkupSanitizer;
53
53
  /**
@@ -102,8 +102,8 @@ function resolvePurifier() {
102
102
  * Sanitize raw item/passage markup before it is injected into the DOM.
103
103
  *
104
104
  * - Strips `<script>`, event-handler attributes, unknown protocols and
105
- * a standard set of dangerous tags (`iframe`, `object`, `embed`, `base`,
106
- * `form`, `meta`, `link`).
105
+ * the other tags in `SANITIZER_FORBIDDEN_TAGS` (`iframe`, `object`, `embed`,
106
+ * `base`, `form`, `meta`, `link`, `style`, `foreignObject`).
107
107
  * - Preserves PIE custom elements (`pie-*`) and any extra tags listed in
108
108
  * `allowedCustomElements`.
109
109
  * - Marks elements that carry an authored color, as `markAuthoredColors`
@@ -167,8 +167,8 @@ export function sanitizeItemMarkup(markup, options = {}) {
167
167
  }
168
168
  /**
169
169
  * Build the default `ItemMarkupSanitizer` used by the players. The returned
170
- * function is stable for a given set of allowed custom elements so callers
171
- * can safely use reference equality when deciding whether to re-sanitize.
170
+ * function captures the options once; each call returns a new function, so a
171
+ * caller that compares sanitizers by identity keeps the one it built.
172
172
  */
173
173
  export function createDefaultItemMarkupSanitizer(options = {}) {
174
174
  const { allowedCustomElements, wrapOverwideContent } = options;
@@ -51,8 +51,8 @@ export function sanitizeSvgIcon(icon) {
51
51
  });
52
52
  const str = typeof result === "string" ? result : String(result ?? "");
53
53
  if (stringCache.size >= STRING_CACHE_MAX) {
54
- // Naive LRU: clear oldest half when the cache fills up. Tool icons are
55
- // a small fixed set per assessment so this is rarely hit.
54
+ // Evict the oldest-inserted half when full; a hit does not refresh an entry.
55
+ // Tool icons are a small fixed set per assessment, so this rarely runs.
56
56
  const keys = [...stringCache.keys()];
57
57
  for (let i = 0; i < keys.length / 2; i += 1) {
58
58
  stringCache.delete(keys[i]);
@@ -24,7 +24,7 @@
24
24
  * they never appeared in the authored markup string.
25
25
  *
26
26
  * The wrapping itself lives in `./wrap-overwide.js`, shared with the table
27
- * wrapper: only the four values below and the accessible name differ.
27
+ * wrapper: only the spec below differs.
28
28
  */
29
29
  import { type WrapOverwideOptions } from "./wrap-overwide.js";
30
30
  export type WrapOverwideImagesInElementOptions = WrapOverwideOptions;
@@ -24,7 +24,7 @@
24
24
  * they never appeared in the authored markup string.
25
25
  *
26
26
  * The wrapping itself lives in `./wrap-overwide.js`, shared with the table
27
- * wrapper: only the four values below and the accessible name differ.
27
+ * wrapper: only the spec below differs.
28
28
  */
29
29
  import { isOverwideWrapMutation, wrapOverwideInElement, wrapOverwideMarkup, } from "./wrap-overwide.js";
30
30
  function buildAriaLabel(image) {
@@ -116,6 +116,5 @@ export function createMediaElementTimeSource(element, options = {}) {
116
116
  export function findMediaElement(root) {
117
117
  if (!root)
118
118
  return null;
119
- const found = root.querySelector("video, audio");
120
- return found ?? null;
119
+ return root.querySelector("video, audio");
121
120
  }
@@ -1,11 +1,12 @@
1
1
  /**
2
- * This contains copies of the data types we use in PIEOneer. These types, unlike
3
- * the ones in the datastore package, should not have any dependencies, and might in some
4
- * cases be more shallow than their originals. The main reason for these is that they should
5
- * be able to be used in front-end code (as well as the PIEOneer backend code).
2
+ * The PIE content and assessment model the players share: item, passage and
3
+ * section configs, the QTI-aligned assessment structure, session snapshots,
4
+ * accessibility catalogs and media references, `Env`, the element controller
5
+ * contract and authoring asset events.
6
6
  *
7
- * INCLUDING REFERENCES TO THE DATASTORE PACKAGE INSTEAD WILL MAKE PIEONEER THROW 500
8
- * EVERYWHERE WITHOUT ANY FURTHER EXPLANATION
7
+ * The `*Entity` shapes mirror content-store records and may be shallower than
8
+ * them. The module depends on nothing beyond this package's own type modules,
9
+ * so front-end and server code can both import it.
9
10
  */
10
11
  import type { FormativeDeliveryPolicy, FormativeItemPolicy, FormativeSectionSlice } from "../formative/types.js";
11
12
  import type { TimedMediaSectionData, TimedMediaSectionSessionSlice } from "../timed-media/types.js";
@@ -55,10 +56,12 @@ export interface ConfigEntity {
55
56
  markup: string;
56
57
  elements: ConfigElements;
57
58
  models: PieModel[];
59
+ /** Configure settings for authoring mode. */
58
60
  configuration?: Record<string, any>;
59
61
  /**
60
62
  * QTI 3.0: Extracted accessibility catalogs from embedded SSML.
61
- * Automatically generated by SSMLExtractor when <speak> tags are found in content.
63
+ * Written by SSMLExtractor when a host's preprocessing runs it over content
64
+ * holding <speak> tags; the players register what this carries.
62
65
  */
63
66
  extractedCatalogs?: AccessibilityCatalog[];
64
67
  [key: string]: any;
@@ -77,12 +80,9 @@ export interface PassageEntity extends VersionEntity, ConfigContainerEntity, Sea
77
80
  export interface ItemEntity extends VersionEntity, ConfigContainerEntity, SearchMetaDataEntity {
78
81
  name?: string;
79
82
  /**
80
- * `null` as well as absent, because importers write it. JSON has no
81
- * `undefined`, so an item transformed from another format carries an explicit
82
- * `passage: null` for "no passage" — the runtime has always tested for
83
- * null, so it expected it while the type denied it. Excluding null
84
- * only meant a host feeding real importer output through a typed path had to
85
- * cast it away.
83
+ * `null` as well as absent, because importers write it: JSON has no
84
+ * `undefined`, so an item transformed from another format carries an
85
+ * explicit `passage: null` for "no passage", and the runtime tests for it.
86
86
  */
87
87
  passage?: string | PassageEntity | null;
88
88
  /** QTI/APIP-style accessibility catalogs owned by the item root. */
@@ -241,11 +241,9 @@ export interface TestPart {
241
241
  * The host-facing persistence snapshot for one section, returned by
242
242
  * `SectionControllerHandle.getSession()` and accepted by `applySession()`.
243
243
  *
244
- * Canonical here rather than in a player or the toolkit: the toolkit is
245
- * complementary to a player rather than an alternative to one and sits beneath
246
- * both entry paths, so a shape both need belongs alongside `AssessmentSection`.
247
- * The delivery slices it carries are defined in this package too, which is what
248
- * previously forced them to reference this shape in prose instead of types.
244
+ * Canonical here, beside `AssessmentSection`: both entry paths need it, and the
245
+ * toolkit complements a player and sits beneath both of them. The delivery
246
+ * slices it carries live in this package too, so they reference it as a type.
249
247
  */
250
248
  export interface SectionControllerSessionState {
251
249
  currentItemIndex?: number;
@@ -371,7 +369,7 @@ export interface MediaFragmentRange {
371
369
  * inside `qti-card-entry` — multiple sources, MIME types, poster, and an
372
370
  * optional time range.
373
371
  *
374
- * Note there is no `kind` discriminant: the card's `catalog` field is QTI's
372
+ * It carries no `kind` discriminant: the card's `catalog` field is QTI's
375
373
  * `qti-card@support` and is the only discriminator. Restating it here would
376
374
  * create a second source of truth that can disagree with the first.
377
375
  */
@@ -382,15 +380,12 @@ export interface SignLanguageCardPayload {
382
380
  * signed alternate is LSM, not ASL, so this must never be inferred from the
383
381
  * item or assessment content language.
384
382
  *
385
- * Optional, and redundant when it equals the card's `language`. The card's
386
- * `language` is QTI's `xml:lang` on the card entry and is the only field
387
- * catalog resolution selects on — it decides *which* card is returned, before
388
- * anything knows the card is a signing card. This one is read after
389
- * resolution, to name the language in the region's accessible label and to
390
- * refuse a card in a sign language the learner did not ask for. Author it only
391
- * where the two genuinely differ, which is a card tagged with the item's
392
- * content language (`language: "en-US"`, `signLang: "ase"`) so that resolution
393
- * reaches it by the default-language rung.
383
+ * Optional: when absent, the card's `language` stands in, so author it only
384
+ * where the two differ. The sign-language tool reads every signing card in
385
+ * the owner's scope and selects on this value: a card in the requested sign
386
+ * language wins, a card naming no language is the fallback, and a card in
387
+ * another sign language is never shown. It also names the language in the
388
+ * region's accessible label.
394
389
  */
395
390
  signLang?: string;
396
391
  media: MediaAssetRef;
@@ -401,8 +396,7 @@ export interface SignLanguageCardPayload {
401
396
  *
402
397
  * QTI 3 treats recorded audio and synthesized speech as the *same* support:
403
398
  * both are `spoken`, and a card holds recorded audio through `qti-file-href`
404
- * plus a MIME type. So this is not a new accommodation, it is the other form the
405
- * existing one can take.
399
+ * plus a MIME type. A recording is the other form that accommodation takes.
406
400
  *
407
401
  * A node commonly carries both this and a `content` card in the same language —
408
402
  * APIP's pattern, which QTI's migration guidance keeps, because the script is
@@ -435,15 +429,13 @@ export type CatalogCardPayload = SignLanguageCardPayload | SpokenAudioCardPayloa
435
429
  * one of `content` or `payload`.
436
430
  *
437
431
  * One generic `payload` slot, not a field per accommodation. This is the shape
438
- * `pie-elements-ng` (PIE-879) and the `pie-api-aws` Learnosity importer
439
- * (PIE-881) restate structurally; all three read the same authored JSON and none
440
- * of them takes a package dependency on the others, so keeping the declarations
441
- * identical is the only thing holding interop together. It briefly was not:
442
- * those two producers wrote the signing payload under `signLanguage`, this
443
- * repo tolerated that as an input alias, and the alias was folded in on the
444
- * resolution path but not the enumeration path — so an imported card rendered
445
- * its signing video and simultaneously reported that the item had no signed
446
- * alternate. Both producers now emit `payload` and the alias is gone.
432
+ * `pie-elements-ng` and the PIE API service's Learnosity importer restate
433
+ * structurally; all three read the same authored JSON and none of them takes a
434
+ * package dependency on the others, so keeping the declarations identical is
435
+ * the only thing holding interop together; a `signLanguage` input alias, folded
436
+ * in on the resolution path but not the enumeration path, once had an imported
437
+ * card render its signing video while reporting no signed alternate. Both
438
+ * producers now emit `payload`.
447
439
  */
448
440
  export interface CatalogCard {
449
441
  catalog: string;
@@ -472,9 +464,25 @@ export interface AccessibilityCatalog {
472
464
  identifier: string;
473
465
  cards: CatalogCard[];
474
466
  }
467
+ /**
468
+ * A learner's personal needs profile, in tool ids. An AfA PNP 3.0 record maps to
469
+ * it through the AfA adapter the packaged tool set exports.
470
+ */
475
471
  export interface PersonalNeedsProfile {
472
+ /** Tools the learner may turn on. */
476
473
  supports: string[];
474
+ /** Tools withheld from the learner; a prohibition outranks a support. */
477
475
  prohibitedSupports?: string[];
476
+ /**
477
+ * Tools granted as `supports` grants them and turned on when the learner's
478
+ * toolbar first shows them. A block withdraws them as it withdraws a support.
479
+ */
480
+ activateAtInit?: string[];
481
+ /**
482
+ * Parameters by tool id, the lowest layer: the assessment's and an item's
483
+ * `toolParameters` override them key by key.
484
+ */
485
+ toolParameters?: ToolParameters;
478
486
  }
479
487
  export interface AssessmentEntity extends BaseEntity, SearchMetaDataEntity {
480
488
  name?: string;
@@ -517,18 +525,46 @@ export interface AssessmentSettings {
517
525
  toolOverrides?: Record<string, boolean>;
518
526
  };
519
527
  /**
520
- * Parameters by tool id, handed to a tool a grant or requirement admits;
521
- * an item's `toolParameters` entry wins. Provider configuration belongs in
522
- * the toolkit's `tools.providers`.
528
+ * Parameters by tool id, handed to the tool wherever policy shows it. They
529
+ * override the profile's `toolParameters` key by key, and an item's
530
+ * `toolParameters` override them on that item's own toolbar. Provider
531
+ * configuration belongs in the toolkit's `tools.providers`.
532
+ */
533
+ toolParameters?: ToolParameters;
534
+ }
535
+ /**
536
+ * Parameter shapes by tool id. A tool outside the stock set may add its own by
537
+ * declaration merging, with a type literal as the value.
538
+ *
539
+ * The values come from authored content, so a tool still checks what it reads.
540
+ */
541
+ export interface ToolParameterMap {
542
+ calculator: {
543
+ type?: "basic" | "scientific" | "graphing";
544
+ };
545
+ answerEliminator: {
546
+ strategy?: "strikethrough" | "mask";
547
+ };
548
+ signLanguage: {
549
+ signLang?: string;
550
+ };
551
+ /**
552
+ * `rate` multiplies every playback speed, 1 being the provider's normal
553
+ * speed; `pitch` runs 0–2 with 1 normal; `volume` runs 0–1.
523
554
  */
524
- toolConfigs?: {
525
- calculator?: AssessmentCalculatorConfig;
526
- [toolId: string]: Record<string, unknown> | undefined;
555
+ textToSpeech: {
556
+ rate?: number;
557
+ pitch?: number;
558
+ volume?: number;
527
559
  };
528
560
  }
529
- /** Calculator parameters in assessment settings. */
530
- export type AssessmentCalculatorConfig = {
531
- type?: "basic" | "scientific" | "graphing";
561
+ /** The parameter shape of tool `K`: its {@link ToolParameterMap} entry, else open. */
562
+ export type ToolParametersFor<K extends string> = K extends keyof ToolParameterMap ? ToolParameterMap[K] : Record<string, unknown>;
563
+ /** Parameters by tool id: typed for the ids {@link ToolParameterMap} names. */
564
+ export type ToolParameters = {
565
+ [K in keyof ToolParameterMap]?: ToolParameterMap[K];
566
+ } & {
567
+ [toolId: string]: Record<string, unknown> | undefined;
532
568
  };
533
569
  /**
534
570
  * Item-level settings for tool requirements.
@@ -537,8 +573,8 @@ export type AssessmentCalculatorConfig = {
537
573
  export interface ItemSettings {
538
574
  requiredTools?: string[];
539
575
  restrictedTools?: string[];
540
- /** Parameters by tool id; outranks the assessment's `toolConfigs` entry. */
541
- toolParameters?: Record<string, Record<string, unknown>>;
576
+ /** Parameters by tool id; override the assessment's key by key. */
577
+ toolParameters?: ToolParameters;
542
578
  }
543
579
  export type PlayerMode = "gather" | "view" | "evaluate" | "author";
544
580
  export type PlayerRole = "student" | "instructor";
@@ -636,8 +672,8 @@ export interface AdvancedItemConfig {
636
672
  }
637
673
  export type ItemConfig = PieContent | AdvancedItemConfig;
638
674
  /**
639
- * During the loading of elements from PIE bundles, we do a trick where we make
640
- * editor components available as web components by appending this to the element name.
675
+ * Appended to an element's tag name to register the bundle's editor component
676
+ * as a custom element of its own when PIE bundles load.
641
677
  */
642
678
  export declare const editorPostFix = "-pie-editor--";
643
679
  export type ModelUpdatedDetail = {
@@ -1,6 +1,6 @@
1
1
  /**
2
- * During the loading of elements from PIE bundles, we do a trick where we make
3
- * editor components available as web components by appending this to the element name.
2
+ * Appended to an element's tag name to register the bundle's editor component
3
+ * as a custom element of its own when PIE bundles load.
4
4
  */
5
5
  export const editorPostFix = "-pie-editor--";
6
6
  export class ModelUpdatedEvent extends CustomEvent {
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Reads a boolean-like prop or attribute value. Nullish or blank reads as
3
+ * `defaultValue`; `false`, `0`, `off` and `no` (any case) read as false; any
4
+ * other value reads as true.
5
+ */
1
6
  export declare function coerceBooleanLike(value: boolean | string | null | undefined, defaultValue?: boolean): boolean;
2
7
  /**
3
8
  * Reads a boolean attribute value: present is true and absent is false, as in
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Reads a boolean-like prop or attribute value. Nullish or blank reads as
3
+ * `defaultValue`; `false`, `0`, `off` and `no` (any case) read as false; any
4
+ * other value reads as true.
5
+ */
1
6
  export function coerceBooleanLike(value, defaultValue = false) {
2
7
  if (typeof value === "boolean") {
3
8
  return value;
@@ -12,10 +17,7 @@ export function coerceBooleanLike(value, defaultValue = false) {
12
17
  if (["false", "0", "off", "no"].includes(normalizedValue)) {
13
18
  return false;
14
19
  }
15
- if (["true", "1", "on", "yes"].includes(normalizedValue)) {
16
- return true;
17
- }
18
- return Boolean(normalizedValue);
20
+ return true;
19
21
  }
20
22
  /**
21
23
  * Reads a boolean attribute value: present is true and absent is false, as in
@@ -9,11 +9,9 @@
9
9
  * light DOM, so those rules have to exist as a document-level stylesheet —
10
10
  * there is no shadow root to scope them to.
11
11
  *
12
- * Hosts used to be required to import that stylesheet themselves. Nothing
13
- * enforced it and nothing failed loudly when they didn't: the item rendered,
14
- * the passage was simply unstyled, and it surfaced as a visual bug reported by
15
- * hand days later. Players install the stylesheet themselves instead, which
16
- * keeps the host contract at "import the player".
12
+ * Players install that stylesheet themselves, which keeps the host contract at
13
+ * "import the player"; a host-side import was unenforced and failed silently,
14
+ * as an unstyled passage.
17
15
  *
18
16
  * The CSS text is passed in rather than imported here: this package builds with
19
17
  * plain `tsc`, so it cannot inline a stylesheet. Bundler-built player packages
@@ -58,11 +56,9 @@ export declare function contentStylesPresent(): boolean;
58
56
  * hand — first in the entry, before app CSS — so an app rule and a content rule
59
57
  * of equal specificity resolve in the host's favour, exactly as before.
60
58
  *
61
- * A cascade layer looks tempting here and is the wrong tool: unlayered author
62
- * declarations beat *all* layered ones regardless of specificity, so a host
63
- * reset as broad as `p { margin: 0 }` would silently outrank
64
- * `.numbered-paragraph { margin-left: 36px }`. That trades a visible missing
65
- * stylesheet for a subtler override bug, so ordinary specificity wins instead.
59
+ * A cascade layer would invert that: unlayered author declarations beat *all*
60
+ * layered ones regardless of specificity, so a host reset as broad as
61
+ * `p { margin: 0 }` would outrank `.numbered-paragraph { margin-left: 36px }`.
66
62
  *
67
63
  * @param cssText Contents of `@pie-players/pie-theme/components.css`.
68
64
  * @param source Package installing the styles, for diagnostics.
@@ -9,11 +9,9 @@
9
9
  * light DOM, so those rules have to exist as a document-level stylesheet —
10
10
  * there is no shadow root to scope them to.
11
11
  *
12
- * Hosts used to be required to import that stylesheet themselves. Nothing
13
- * enforced it and nothing failed loudly when they didn't: the item rendered,
14
- * the passage was simply unstyled, and it surfaced as a visual bug reported by
15
- * hand days later. Players install the stylesheet themselves instead, which
16
- * keeps the host contract at "import the player".
12
+ * Players install that stylesheet themselves, which keeps the host contract at
13
+ * "import the player"; a host-side import was unenforced and failed silently,
14
+ * as an unstyled passage.
17
15
  *
18
16
  * The CSS text is passed in rather than imported here: this package builds with
19
17
  * plain `tsc`, so it cannot inline a stylesheet. Bundler-built player packages
@@ -48,9 +46,8 @@ const declaresContentStylesSentinel = (rule) => {
48
46
  // Grouping rules hold no declarations of their own, so the sentinel sits one
49
47
  // or more levels down. A host that confines its copy — `@scope
50
48
  // (.item-content) { … }`, `@layer pie-content { … }` — presents exactly one
51
- // top-level rule with an empty `.style`, and a top-level-only scan reads that
52
- // as "no copy here". That made both detection paths blind to the one host
53
- // configuration this module most needs to recognise.
49
+ // top-level rule with an empty `.style`, which a top-level-only scan reads
50
+ // as "no copy here".
54
51
  const nested = rule.cssRules;
55
52
  if (!nested)
56
53
  return false;
@@ -115,11 +112,9 @@ export function contentStylesPresent() {
115
112
  * hand — first in the entry, before app CSS — so an app rule and a content rule
116
113
  * of equal specificity resolve in the host's favour, exactly as before.
117
114
  *
118
- * A cascade layer looks tempting here and is the wrong tool: unlayered author
119
- * declarations beat *all* layered ones regardless of specificity, so a host
120
- * reset as broad as `p { margin: 0 }` would silently outrank
121
- * `.numbered-paragraph { margin-left: 36px }`. That trades a visible missing
122
- * stylesheet for a subtler override bug, so ordinary specificity wins instead.
115
+ * A cascade layer would invert that: unlayered author declarations beat *all*
116
+ * layered ones regardless of specificity, so a host reset as broad as
117
+ * `p { margin: 0 }` would outrank `.numbered-paragraph { margin-left: 36px }`.
123
118
  *
124
119
  * @param cssText Contents of `@pie-players/pie-theme/components.css`.
125
120
  * @param source Package installing the styles, for diagnostics.
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * Selector for elements that are commonly keyboard-focusable in assessment UI.
3
- * Kept in sync with `focus-trap.ts` for consistent tab-order surfaces.
4
3
  */
5
4
  export declare const FOCUSABLE_SELECTOR: string;
6
5
  /**
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * Selector for elements that are commonly keyboard-focusable in assessment UI.
3
- * Kept in sync with `focus-trap.ts` for consistent tab-order surfaces.
4
3
  */
5
4
  export const FOCUSABLE_SELECTOR = [
6
5
  "a[href]",
@@ -57,9 +57,8 @@ export function createFocusTrap(container, options = {}) {
57
57
  container.focus?.();
58
58
  return;
59
59
  }
60
- const current = getDeepActiveElement(container.getRootNode() instanceof ShadowRoot
61
- ? container.getRootNode()
62
- : document);
60
+ const rootNode = container.getRootNode();
61
+ const current = getDeepActiveElement(rootNode instanceof ShadowRoot ? rootNode : document);
63
62
  const currentIndex = focusable.indexOf(current || focusable[0]);
64
63
  if (event.shiftKey) {
65
64
  if (currentIndex <= 0) {