@pie-players/pie-players-shared 0.3.75 → 1.0.0-beta.0

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 (59) 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/initialization.d.ts +13 -14
  33. package/dist/pie/initialization.js +61 -75
  34. package/dist/pie/item-session-contract.js +7 -9
  35. package/dist/pie/resource-monitor.d.ts +34 -32
  36. package/dist/pie/resource-monitor.js +152 -231
  37. package/dist/pie/stages.d.ts +4 -3
  38. package/dist/pie/stages.js +4 -3
  39. package/dist/pie/updates.js +5 -4
  40. package/dist/pie/utils.d.ts +0 -2
  41. package/dist/pie/utils.js +0 -2
  42. package/dist/security/sanitize-item-markup.d.ts +4 -4
  43. package/dist/security/sanitize-item-markup.js +4 -4
  44. package/dist/security/sanitize-svg-icon.js +2 -2
  45. package/dist/security/wrap-overwide-images.d.ts +1 -1
  46. package/dist/security/wrap-overwide-images.js +1 -1
  47. package/dist/timed-media/media-element-source.js +1 -2
  48. package/dist/types/index.d.ts +62 -52
  49. package/dist/types/index.js +2 -2
  50. package/dist/ui/attribute-coercion.d.ts +5 -0
  51. package/dist/ui/attribute-coercion.js +6 -4
  52. package/dist/ui/content-styles.d.ts +6 -10
  53. package/dist/ui/content-styles.js +8 -13
  54. package/dist/ui/first-focusable.d.ts +0 -1
  55. package/dist/ui/first-focusable.js +0 -1
  56. package/dist/ui/focus-trap.js +2 -3
  57. package/dist/ui/scope-css.d.ts +23 -26
  58. package/dist/ui/scope-css.js +23 -26
  59. 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)
@@ -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;
@@ -517,18 +509,36 @@ export interface AssessmentSettings {
517
509
  toolOverrides?: Record<string, boolean>;
518
510
  };
519
511
  /**
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`.
512
+ * Parameters by tool id, handed to the tool wherever policy shows it; an
513
+ * item's `toolParameters` entry wins on that item's own toolbar. Provider
514
+ * configuration belongs in the toolkit's `tools.providers`.
523
515
  */
524
- toolConfigs?: {
525
- calculator?: AssessmentCalculatorConfig;
526
- [toolId: string]: Record<string, unknown> | undefined;
516
+ toolParameters?: ToolParameters;
517
+ }
518
+ /**
519
+ * Parameter shapes by tool id. A tool outside the stock set may add its own by
520
+ * declaration merging, with a type literal as the value.
521
+ *
522
+ * The values come from authored content, so a tool still checks what it reads.
523
+ */
524
+ export interface ToolParameterMap {
525
+ calculator: {
526
+ type?: "basic" | "scientific" | "graphing";
527
+ };
528
+ answerEliminator: {
529
+ strategy?: "strikethrough" | "mask";
530
+ };
531
+ signLanguage: {
532
+ signLang?: string;
527
533
  };
528
534
  }
529
- /** Calculator parameters in assessment settings. */
530
- export type AssessmentCalculatorConfig = {
531
- type?: "basic" | "scientific" | "graphing";
535
+ /** The parameter shape of tool `K`: its {@link ToolParameterMap} entry, else open. */
536
+ export type ToolParametersFor<K extends string> = K extends keyof ToolParameterMap ? ToolParameterMap[K] : Record<string, unknown>;
537
+ /** Parameters by tool id: typed for the ids {@link ToolParameterMap} names. */
538
+ export type ToolParameters = {
539
+ [K in keyof ToolParameterMap]?: ToolParameterMap[K];
540
+ } & {
541
+ [toolId: string]: Record<string, unknown> | undefined;
532
542
  };
533
543
  /**
534
544
  * Item-level settings for tool requirements.
@@ -537,8 +547,8 @@ export type AssessmentCalculatorConfig = {
537
547
  export interface ItemSettings {
538
548
  requiredTools?: string[];
539
549
  restrictedTools?: string[];
540
- /** Parameters by tool id; outranks the assessment's `toolConfigs` entry. */
541
- toolParameters?: Record<string, Record<string, unknown>>;
550
+ /** Parameters by tool id; outranks the assessment's `toolParameters` entry. */
551
+ toolParameters?: ToolParameters;
542
552
  }
543
553
  export type PlayerMode = "gather" | "view" | "evaluate" | "author";
544
554
  export type PlayerRole = "student" | "instructor";
@@ -636,8 +646,8 @@ export interface AdvancedItemConfig {
636
646
  }
637
647
  export type ItemConfig = PieContent | AdvancedItemConfig;
638
648
  /**
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.
649
+ * Appended to an element's tag name to register the bundle's editor component
650
+ * as a custom element of its own when PIE bundles load.
641
651
  */
642
652
  export declare const editorPostFix = "-pie-editor--";
643
653
  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) {
@@ -4,37 +4,34 @@
4
4
  * external-style-urls="...">` or `itemConfig.resources.stylesheets[*].url`
5
5
  * styles that player's subtree instead of the whole host document.
6
6
  *
7
- * This replaces a single-regex implementation that prefixed every
8
- * selector-like fragment. That is correct for flat selector rules and wrong for
9
- * everything else: `@media screen { ... }` became
10
- * `.scope @media screen { ... }`, an invalid selector, so the browser dropped
11
- * the whole block and every rule inside it; `@font-face` and `@keyframes` were
12
- * corrupted the same way; and `:root` became `.scope :root`, which can never
13
- * match, because `:root` is `<html>` and is not a descendant of the player.
14
- * At-rules and `:root` custom properties therefore never applied at all.
7
+ * Walks the stylesheet brace-by-brace. Block at-rules that hold rules
8
+ * (`@media`, `@supports`, `@container`, `@layer`, `@scope`) keep their prelude
9
+ * and have their inner rules scoped, every other at-rule passes through, and a
10
+ * leading `:root`, `html` or `body` is replaced by the scope, since
11
+ * `.scope :root` can never match: `:root` is `<html>`, which is no descendant
12
+ * of the player. A regex prefixer fails on exactly these cases: it turned
13
+ * `@media screen { … }` into the invalid `.scope @media screen { … }`, which the
14
+ * browser drops with every rule inside, and corrupted `@font-face`,
15
+ * `@keyframes` and `:root` the same way.
15
16
  *
16
- * The QTI player's `scopeCssRules` (`pie-qti` -
17
- * `packages/item-player/src/components/utils/stylesheetRender.ts`) is not a
18
- * drop-in either. Its `:root` handling is the behaviour we want and is adopted
19
- * here, but it excludes `@` from its selector pattern rather than understanding
20
- * at-rules, so `@media` and `@supports` lose their condition and their inner
21
- * rules are hoisted and applied unconditionally — a mobile-only rule then
22
- * applies at every viewport width, which is a subtler failure than dropping it.
17
+ * The `:root` handling follows the QTI player's `scopeCssRules` (`pie-qti` -
18
+ * `packages/item-player/src/components/utils/stylesheetRender.ts`), which is no
19
+ * drop-in: it excludes `@` from its selector pattern without understanding
20
+ * at-rules, so `@media` and `@supports` lose their condition and their rules
21
+ * apply at every viewport width.
23
22
  *
24
- * So this walks the stylesheet brace-by-brace instead of pattern-matching it.
25
- * A real CSS parser would be the textbook answer and is deliberately not used:
26
- * `@pie-players/pie-item-player` ships with two workspace dependencies, this
27
- * runs in the delivery path, and scoping needs to know about rule boundaries
28
- * only — not about property grammar.
23
+ * No CSS parser: `@pie-players/pie-item-player` ships with two workspace
24
+ * dependencies, this runs in the delivery path, and scoping needs rule
25
+ * boundaries only.
29
26
  *
30
- * Known limits, all preserving existing behaviour rather than adding policy:
27
+ * Known limits:
31
28
  *
32
- * - `@import` is passed through untouched, exactly as the old regex left it. It
33
- * pulls in an unscoped stylesheet, so it defeats scoping, but blocking it is
34
- * a security policy decision and not this function's job. Callers gate
35
- * stylesheet URLs with `validateExternalStyleUrl` before fetching.
29
+ * - `@import` is passed through untouched. It pulls in an unscoped stylesheet,
30
+ * so it defeats scoping, but blocking it is a security policy decision and
31
+ * not this function's job. Callers gate stylesheet URLs with
32
+ * `validateExternalStyleUrl` before fetching.
36
33
  * - Declaration blocks are emitted verbatim, so `url(...)` references are left
37
- * as authored and resolve against the stylesheet's own URL as before.
34
+ * as authored and resolve against the stylesheet's own URL.
38
35
  * - A style rule's block is never rewritten, which is also what native CSS
39
36
  * nesting needs: nested selectors are relative to a parent that has already
40
37
  * been scoped.