@pie-players/pie-assessment-toolkit 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 (142) hide show
  1. package/README.md +449 -541
  2. package/custom-elements.json +1315 -0
  3. package/dist/attempt/TestSession.d.ts +0 -1
  4. package/dist/attempt/TestSession.js +10 -5
  5. package/dist/components/ItemScope.custom-element.js +1 -1
  6. package/dist/components/ItemToolBar.custom-element.js +1 -1
  7. package/dist/components/PieAssessmentToolkit.custom-element.js +8 -8
  8. package/dist/components/SectionToolBar.custom-element.js +1 -1
  9. package/dist/components/chunks/{ItemToolBar-7tt7v2xp.js → ItemToolBar-3zrc4p3d.js} +4 -4
  10. package/dist/components/chunks/{ItemToolBar-jx38w99s.js → ItemToolBar-66hcwmfw.js} +2 -2
  11. package/dist/components/chunks/ItemToolBar-9gqhfxtn.js +40 -0
  12. package/dist/components/chunks/ItemToolBar-hzc5wyjp.js +2 -0
  13. package/dist/components/chunks/{ItemToolBar-97hx60z4.js → ItemToolBar-kkj69ptr.js} +1 -1
  14. package/dist/components/chunks/sre-engine-bj997w4b.js +1 -0
  15. package/dist/context/assessment-toolkit-context.d.ts +6 -0
  16. package/dist/index.d.ts +7 -7
  17. package/dist/index.js +4 -10
  18. package/dist/policy/core/PolicySource.d.ts +1 -1
  19. package/dist/policy/core/ToolPolicyEngine.d.ts +46 -7
  20. package/dist/policy/core/ToolPolicyEngine.js +73 -16
  21. package/dist/policy/core/compose-decision.d.ts +3 -3
  22. package/dist/policy/core/compose-decision.js +30 -30
  23. package/dist/policy/core/decision-types.d.ts +44 -32
  24. package/dist/policy/core/feature-decision.d.ts +17 -15
  25. package/dist/policy/core/feature-decision.js +11 -13
  26. package/dist/policy/core/pnp-policy-inputs.d.ts +5 -2
  27. package/dist/policy/core/pnp-policy-inputs.js +12 -32
  28. package/dist/policy/core/provenance.d.ts +1 -1
  29. package/dist/policy/core/tool-parameters.d.ts +11 -0
  30. package/dist/policy/core/tool-parameters.js +15 -0
  31. package/dist/policy/engine.d.ts +1 -1
  32. package/dist/policy/sources/PnpPolicySource.d.ts +14 -22
  33. package/dist/policy/sources/PnpPolicySource.js +30 -28
  34. package/dist/runtime/SectionControllerBinding.d.ts +6 -2
  35. package/dist/runtime/SectionControllerBinding.js +21 -15
  36. package/dist/runtime/adapter/subscriber-fanout.d.ts +0 -1
  37. package/dist/runtime/adapter/subscriber-fanout.js +1 -4
  38. package/dist/runtime/composition-emit-scheduler.d.ts +3 -3
  39. package/dist/runtime/composition-emit-scheduler.js +1 -1
  40. package/dist/runtime/content-region.d.ts +0 -4
  41. package/dist/runtime/content-region.js +2 -2
  42. package/dist/runtime/core/SectionEngineCore.d.ts +0 -2
  43. package/dist/runtime/core/SectionEngineCore.js +0 -4
  44. package/dist/runtime/core/cohort.d.ts +0 -5
  45. package/dist/runtime/core/cohort.js +0 -9
  46. package/dist/runtime/core/engine-output.d.ts +3 -2
  47. package/dist/runtime/core/engine-transition.js +3 -2
  48. package/dist/runtime/registration-events.d.ts +11 -14
  49. package/dist/runtime/runtime-id.d.ts +1 -1
  50. package/dist/runtime/runtime-id.js +1 -1
  51. package/dist/runtime/shell-scope.d.ts +4 -4
  52. package/dist/runtime/shell-scope.js +15 -9
  53. package/dist/runtime/tool-host-contract.d.ts +0 -4
  54. package/dist/runtime/tool-host-contract.js +1 -1
  55. package/dist/services/AccessibilityCatalogResolver.d.ts +21 -31
  56. package/dist/services/AccessibilityCatalogResolver.js +29 -42
  57. package/dist/services/ElementToolStateStore.d.ts +32 -61
  58. package/dist/services/ElementToolStateStore.js +37 -67
  59. package/dist/services/HighlightCoordinator.d.ts +9 -21
  60. package/dist/services/HighlightCoordinator.js +52 -80
  61. package/dist/services/RangeSerializer.d.ts +4 -35
  62. package/dist/services/RangeSerializer.js +4 -48
  63. package/dist/services/SSMLExtractor.d.ts +8 -31
  64. package/dist/services/SSMLExtractor.js +18 -40
  65. package/dist/services/TTSService.d.ts +49 -56
  66. package/dist/services/TTSService.js +308 -283
  67. package/dist/services/ToolCoordinator.d.ts +6 -49
  68. package/dist/services/ToolCoordinator.js +25 -61
  69. package/dist/services/ToolRegistry.d.ts +82 -80
  70. package/dist/services/ToolRegistry.js +91 -67
  71. package/dist/services/ToolkitCoordinator.d.ts +83 -81
  72. package/dist/services/ToolkitCoordinator.js +186 -174
  73. package/dist/services/framework-error.d.ts +30 -7
  74. package/dist/services/framework-error.js +10 -13
  75. package/dist/services/interfaces.d.ts +47 -71
  76. package/dist/services/section-controller-types.d.ts +7 -7
  77. package/dist/services/section-session-assignment.js +6 -8
  78. package/dist/services/spoken-audio-cards.d.ts +2 -4
  79. package/dist/services/spoken-audio-cards.js +2 -4
  80. package/dist/services/tool-config-validation.d.ts +5 -7
  81. package/dist/services/tool-config-validation.js +11 -26
  82. package/dist/services/tool-context.d.ts +7 -23
  83. package/dist/services/tool-context.js +99 -53
  84. package/dist/services/tool-failure.d.ts +30 -0
  85. package/dist/services/tool-failure.js +11 -0
  86. package/dist/services/tool-icons.d.ts +1 -2
  87. package/dist/services/tool-icons.js +2 -2
  88. package/dist/services/tool-instance-id.d.ts +8 -6
  89. package/dist/services/tool-instance-id.js +12 -18
  90. package/dist/services/tool-providers/TTSToolProvider.d.ts +6 -43
  91. package/dist/services/tool-providers/TTSToolProvider.js +7 -63
  92. package/dist/services/tool-providers/ToolProviderApi.d.ts +0 -7
  93. package/dist/services/tool-providers/ToolProviderRegistry.d.ts +7 -42
  94. package/dist/services/tool-providers/ToolProviderRegistry.js +21 -63
  95. package/dist/services/tool-request.d.ts +4 -0
  96. package/dist/services/tool-request.js +7 -2
  97. package/dist/services/toolbar-decided-tools.d.ts +1 -0
  98. package/dist/services/toolbar-decided-tools.js +1 -0
  99. package/dist/services/toolbar-items.js +2 -5
  100. package/dist/services/tools-config-normalizer.d.ts +18 -9
  101. package/dist/services/tools-config-normalizer.js +25 -19
  102. package/dist/services/tts/browser-provider.d.ts +2 -5
  103. package/dist/services/tts/browser-provider.js +4 -20
  104. package/dist/services/tts/catalog-span-alignment.js +14 -7
  105. package/dist/services/tts/flat-tree.d.ts +0 -7
  106. package/dist/services/tts/flat-tree.js +2 -2
  107. package/dist/services/tts/generated-speech/dom/to-chunk-inputs.d.ts +2 -2
  108. package/dist/services/tts/generated-speech/index.d.ts +1 -1
  109. package/dist/services/tts/generated-speech/index.js +1 -1
  110. package/dist/services/tts/generated-speech/types.d.ts +1 -1
  111. package/dist/services/tts/generated-speech/types.js +1 -1
  112. package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +10 -12
  113. package/dist/services/tts/highlight-target-resolver.d.ts +12 -0
  114. package/dist/services/tts/math-aware-text-processing.js +10 -29
  115. package/dist/services/tts/math-speech.d.ts +1 -1
  116. package/dist/services/tts/math-speech.js +3 -0
  117. package/dist/services/tts/mathml-sanitization.d.ts +7 -0
  118. package/dist/services/tts/mathml-sanitization.js +9 -1
  119. package/dist/services/tts/provider-options.d.ts +12 -1
  120. package/dist/services/tts/ssml/spoken-text.js +1 -1
  121. package/dist/services/tts/start-failure.d.ts +0 -7
  122. package/dist/services/tts/start-failure.js +1 -1
  123. package/dist/services/tts/text-segmentation.d.ts +5 -0
  124. package/dist/services/tts/text-segmentation.js +13 -10
  125. package/dist/services/tts-runtime-config.d.ts +32 -12
  126. package/dist/services/tts-runtime-config.js +14 -20
  127. package/dist/tools/content-capability-resolution.d.ts +2 -2
  128. package/dist/tools/registration.d.ts +6 -5
  129. package/dist/tools/registration.js +10 -9
  130. package/dist/tools/registrations/toolbar-registration-helpers.d.ts +6 -0
  131. package/dist/tools/registrations/toolbar-registration-helpers.js +6 -0
  132. package/dist/tools/tool-surface-host.js +1 -8
  133. package/package.json +8 -6
  134. package/dist/components/chunks/ItemToolBar-bxwyhr5p.js +0 -2
  135. package/dist/components/chunks/ItemToolBar-rd2bb4p6.js +0 -40
  136. package/dist/components/chunks/sre-engine-jd8cc08x.js +0 -1
  137. package/dist/services/ContextVariableStore.d.ts +0 -113
  138. package/dist/services/ContextVariableStore.js +0 -206
  139. package/dist/services/tts-control-events.d.ts +0 -13
  140. package/dist/services/tts-control-events.js +0 -7
  141. package/dist/utils/logger.d.ts +0 -6
  142. package/dist/utils/logger.js +0 -10
@@ -1,21 +1,22 @@
1
1
  /**
2
- * ElementToolStateStore - Manages element-level ephemeral tool state
2
+ * ElementToolStateStore: ephemeral UI tool state (answer eliminations, flags and
3
+ * the like), kept out of the session data sent to the server for scoring.
3
4
  *
4
- * Provides centralized storage for UI tool state (answer eliminations, flags, etc.)
5
- * that should NOT be sent to the server for scoring. Each element is uniquely identified
6
- * across the entire assessment using composite keys.
7
- *
8
- * Key features:
9
- * - Element-level granularity (not item-level)
10
- * - Composite keys for global uniqueness: `assessmentId:sectionId:itemId:elementId`
11
- * - Cross-section state persistence
12
- * - Reactive subscriptions
13
- * - Optional persistence callback integration
14
- * - Generic solution for any tool
5
+ * State is keyed by a composite global element id,
6
+ * `assessmentId:sectionId:attemptId:itemId:elementId`, unique across the
7
+ * assessment and per attempt: two attempts at one section never share a key.
8
+ * Subscribers see every change, and an optional persistence callback
9
+ * (`setOnStateChange`) receives the serialized state.
10
+ */
11
+ /**
12
+ * The parts of a global element id. `attemptId` is `""` when the host names no
13
+ * attempt. A tool whose state belongs to an item or a section rather than to
14
+ * one element leaves `elementId`, and for a section `itemId`, as `""`.
15
15
  */
16
16
  export interface ElementIdComponents {
17
17
  assessmentId: string;
18
18
  sectionId: string;
19
+ attemptId: string;
19
20
  itemId: string;
20
21
  elementId: string;
21
22
  }
@@ -24,70 +25,51 @@ export declare class ElementToolStateStore {
24
25
  private listeners;
25
26
  private onStateChange;
26
27
  /**
27
- * Generates a globally unique element ID by combining assessment, section, item, and element IDs.
28
- * Format: `${assessmentId}:${sectionId}:${itemId}:${elementId}`, each part with
29
- * `%` written as `%25` and `:` as `%3A`, so an id containing `:` round-trips
30
- * and cannot collide with another split of the same characters.
28
+ * Generates a globally unique element ID from its parts.
29
+ * Format: `${assessmentId}:${sectionId}:${attemptId}:${itemId}:${elementId}`,
30
+ * each part with `%` written as `%25` and `:` as `%3A`, so an id containing
31
+ * `:` round-trips and cannot collide with another split of the same characters.
31
32
  *
32
33
  * @example
33
- * getGlobalElementId('demo-assessment', 'section-1', 'q1', 'mc1')
34
- * // Returns: "demo-assessment:section-1:q1:mc1"
34
+ * getGlobalElementId({ assessmentId: 'demo-assessment', sectionId: 'section-1',
35
+ * attemptId: 'attempt-1', itemId: 'q1', elementId: 'mc1' })
36
+ * // Returns: "demo-assessment:section-1:attempt-1:q1:mc1"
35
37
  */
36
- getGlobalElementId(assessmentId: string, sectionId: string, itemId: string, elementId: string): string;
38
+ getGlobalElementId(parts: ElementIdComponents): string;
37
39
  /**
38
40
  * Parses a global element ID into its component parts.
39
41
  *
40
42
  * @example
41
- * parseGlobalElementId('demo-assessment:section-1:q1:mc1')
42
- * // Returns: { assessmentId: 'demo-assessment', sectionId: 'section-1', itemId: 'q1', elementId: 'mc1' }
43
+ * parseGlobalElementId('demo-assessment:section-1:attempt-1:q1:mc1')
44
+ * // Returns: { assessmentId: 'demo-assessment', sectionId: 'section-1',
45
+ * // attemptId: 'attempt-1', itemId: 'q1', elementId: 'mc1' }
43
46
  */
44
47
  parseGlobalElementId(globalElementId: string): ElementIdComponents | null;
45
48
  /**
46
49
  * Sets tool state for a specific element.
47
50
  * Notifies all subscribers and triggers persistence callback.
48
- *
49
- * @param globalElementId Composite key identifying the element
50
- * @param toolId Tool identifier (e.g., 'answerEliminator', 'flagging')
51
- * @param state Tool-specific state object
52
51
  */
53
52
  setState(globalElementId: string, toolId: string, state: unknown): void;
54
- /**
55
- * Gets tool state for a specific element.
56
- *
57
- * @param globalElementId Composite key identifying the element
58
- * @param toolId Tool identifier
59
- * @returns Tool state or undefined if not found
60
- */
53
+ /** One tool's state for an element, or `undefined` when none is set. */
61
54
  getState(globalElementId: string, toolId: string): unknown;
62
- /**
63
- * Gets all tool states for a specific element.
64
- *
65
- * @param globalElementId Composite key identifying the element
66
- * @returns Object with tool states keyed by toolId
67
- */
55
+ /** Every tool's state for an element, keyed by toolId. */
68
56
  getElementState(globalElementId: string): Record<string, unknown>;
69
57
  /**
70
- * Gets all tool states for all elements.
71
- * Used for persistence and debugging.
72
- *
73
- * @returns Nested object: { globalElementId: { toolId: state } }
58
+ * All state as `{ globalElementId: { toolId: state } }`, for persistence and
59
+ * debugging.
74
60
  */
75
61
  getAllState(): Record<string, Record<string, unknown>>;
76
62
  /**
77
63
  * Subscribes to state changes.
78
64
  * Callback is invoked whenever any element's tool state changes, including
79
- * a `loadState` restore, with a copy of the state that it may keep.
80
- *
81
- * @param callback Function to call on state changes
82
- * @returns Unsubscribe function
65
+ * a `loadState` restore, with a copy of the state that it may keep. Returns
66
+ * the unsubscribe function.
83
67
  */
84
68
  subscribe(callback: (state: Map<string, Map<string, unknown>>) => void): () => void;
85
69
  private _notifyListeners;
86
70
  /**
87
71
  * Sets a callback to be invoked on state changes for persistence integration.
88
72
  * Used by demo/app layer to persist tool state to localStorage or server.
89
- *
90
- * @param callback Function to call with serialized state on changes
91
73
  */
92
74
  setOnStateChange(callback: (state: Record<string, Record<string, unknown>>) => void): void;
93
75
  private _notifyStateChange;
@@ -95,29 +77,18 @@ export declare class ElementToolStateStore {
95
77
  * Loads tool state from serialized format.
96
78
  * Used to restore state from localStorage or server. Subscribers are
97
79
  * notified; the persistence callback is not, so a restore never writes back.
98
- *
99
- * @param state Serialized state object
100
80
  */
101
81
  loadState(state: Record<string, Record<string, unknown>>): void;
102
- /**
103
- * Clears all tool state for a specific element.
104
- *
105
- * @param globalElementId Composite key identifying the element
106
- */
82
+ /** Clears all tool state for a specific element. */
107
83
  clearElement(globalElementId: string): void;
108
84
  /**
109
85
  * Clears state for a specific tool across all elements.
110
86
  * Useful when disabling a tool globally.
111
- *
112
- * @param toolId Tool identifier
113
87
  */
114
88
  clearTool(toolId: string): void;
115
89
  /**
116
- * Clears all tool state for a specific section.
90
+ * Clears all tool state for a specific section, across its attempts.
117
91
  * Useful when unmounting/disposing a section.
118
- *
119
- * @param assessmentId Assessment identifier
120
- * @param sectionId Section identifier
121
92
  */
122
93
  clearSection(assessmentId: string, sectionId: string): void;
123
94
  /**
@@ -1,18 +1,20 @@
1
1
  /**
2
- * ElementToolStateStore - Manages element-level ephemeral tool state
2
+ * ElementToolStateStore: ephemeral UI tool state (answer eliminations, flags and
3
+ * the like), kept out of the session data sent to the server for scoring.
3
4
  *
4
- * Provides centralized storage for UI tool state (answer eliminations, flags, etc.)
5
- * that should NOT be sent to the server for scoring. Each element is uniquely identified
6
- * across the entire assessment using composite keys.
7
- *
8
- * Key features:
9
- * - Element-level granularity (not item-level)
10
- * - Composite keys for global uniqueness: `assessmentId:sectionId:itemId:elementId`
11
- * - Cross-section state persistence
12
- * - Reactive subscriptions
13
- * - Optional persistence callback integration
14
- * - Generic solution for any tool
5
+ * State is keyed by a composite global element id,
6
+ * `assessmentId:sectionId:attemptId:itemId:elementId`, unique across the
7
+ * assessment and per attempt: two attempts at one section never share a key.
8
+ * Subscribers see every change, and an optional persistence callback
9
+ * (`setOnStateChange`) receives the serialized state.
15
10
  */
11
+ const ID_PARTS = [
12
+ "assessmentId",
13
+ "sectionId",
14
+ "attemptId",
15
+ "itemId",
16
+ "elementId",
17
+ ];
16
18
  function escapeIdPart(part) {
17
19
  return part.replace(/%/g, "%25").replace(/:/g, "%3A");
18
20
  }
@@ -24,41 +26,37 @@ export class ElementToolStateStore {
24
26
  listeners = new Set();
25
27
  onStateChange = null;
26
28
  /**
27
- * Generates a globally unique element ID by combining assessment, section, item, and element IDs.
28
- * Format: `${assessmentId}:${sectionId}:${itemId}:${elementId}`, each part with
29
- * `%` written as `%25` and `:` as `%3A`, so an id containing `:` round-trips
30
- * and cannot collide with another split of the same characters.
29
+ * Generates a globally unique element ID from its parts.
30
+ * Format: `${assessmentId}:${sectionId}:${attemptId}:${itemId}:${elementId}`,
31
+ * each part with `%` written as `%25` and `:` as `%3A`, so an id containing
32
+ * `:` round-trips and cannot collide with another split of the same characters.
31
33
  *
32
34
  * @example
33
- * getGlobalElementId('demo-assessment', 'section-1', 'q1', 'mc1')
34
- * // Returns: "demo-assessment:section-1:q1:mc1"
35
+ * getGlobalElementId({ assessmentId: 'demo-assessment', sectionId: 'section-1',
36
+ * attemptId: 'attempt-1', itemId: 'q1', elementId: 'mc1' })
37
+ * // Returns: "demo-assessment:section-1:attempt-1:q1:mc1"
35
38
  */
36
- getGlobalElementId(assessmentId, sectionId, itemId, elementId) {
37
- return [assessmentId, sectionId, itemId, elementId]
38
- .map(escapeIdPart)
39
- .join(":");
39
+ getGlobalElementId(parts) {
40
+ return ID_PARTS.map((name) => escapeIdPart(parts[name])).join(":");
40
41
  }
41
42
  /**
42
43
  * Parses a global element ID into its component parts.
43
44
  *
44
45
  * @example
45
- * parseGlobalElementId('demo-assessment:section-1:q1:mc1')
46
- * // Returns: { assessmentId: 'demo-assessment', sectionId: 'section-1', itemId: 'q1', elementId: 'mc1' }
46
+ * parseGlobalElementId('demo-assessment:section-1:attempt-1:q1:mc1')
47
+ * // Returns: { assessmentId: 'demo-assessment', sectionId: 'section-1',
48
+ * // attemptId: 'attempt-1', itemId: 'q1', elementId: 'mc1' }
47
49
  */
48
50
  parseGlobalElementId(globalElementId) {
49
51
  const parts = globalElementId.split(":");
50
- if (parts.length !== 4)
52
+ if (parts.length !== ID_PARTS.length)
51
53
  return null;
52
- const [assessmentId, sectionId, itemId, elementId] = parts.map(unescapeIdPart);
53
- return { assessmentId, sectionId, itemId, elementId };
54
+ const [assessmentId, sectionId, attemptId, itemId, elementId] = parts.map(unescapeIdPart);
55
+ return { assessmentId, sectionId, attemptId, itemId, elementId };
54
56
  }
55
57
  /**
56
58
  * Sets tool state for a specific element.
57
59
  * Notifies all subscribers and triggers persistence callback.
58
- *
59
- * @param globalElementId Composite key identifying the element
60
- * @param toolId Tool identifier (e.g., 'answerEliminator', 'flagging')
61
- * @param state Tool-specific state object
62
60
  */
63
61
  setState(globalElementId, toolId, state) {
64
62
  let elementState = this.elementStates.get(globalElementId);
@@ -70,22 +68,11 @@ export class ElementToolStateStore {
70
68
  this._notifyListeners();
71
69
  this._notifyStateChange();
72
70
  }
73
- /**
74
- * Gets tool state for a specific element.
75
- *
76
- * @param globalElementId Composite key identifying the element
77
- * @param toolId Tool identifier
78
- * @returns Tool state or undefined if not found
79
- */
71
+ /** One tool's state for an element, or `undefined` when none is set. */
80
72
  getState(globalElementId, toolId) {
81
73
  return this.elementStates.get(globalElementId)?.get(toolId);
82
74
  }
83
- /**
84
- * Gets all tool states for a specific element.
85
- *
86
- * @param globalElementId Composite key identifying the element
87
- * @returns Object with tool states keyed by toolId
88
- */
75
+ /** Every tool's state for an element, keyed by toolId. */
89
76
  getElementState(globalElementId) {
90
77
  const elementState = this.elementStates.get(globalElementId);
91
78
  if (!elementState)
@@ -93,10 +80,8 @@ export class ElementToolStateStore {
93
80
  return Object.fromEntries(elementState.entries());
94
81
  }
95
82
  /**
96
- * Gets all tool states for all elements.
97
- * Used for persistence and debugging.
98
- *
99
- * @returns Nested object: { globalElementId: { toolId: state } }
83
+ * All state as `{ globalElementId: { toolId: state } }`, for persistence and
84
+ * debugging.
100
85
  */
101
86
  getAllState() {
102
87
  const result = {};
@@ -108,10 +93,8 @@ export class ElementToolStateStore {
108
93
  /**
109
94
  * Subscribes to state changes.
110
95
  * Callback is invoked whenever any element's tool state changes, including
111
- * a `loadState` restore, with a copy of the state that it may keep.
112
- *
113
- * @param callback Function to call on state changes
114
- * @returns Unsubscribe function
96
+ * a `loadState` restore, with a copy of the state that it may keep. Returns
97
+ * the unsubscribe function.
115
98
  */
116
99
  subscribe(callback) {
117
100
  this.listeners.add(callback);
@@ -136,8 +119,6 @@ export class ElementToolStateStore {
136
119
  /**
137
120
  * Sets a callback to be invoked on state changes for persistence integration.
138
121
  * Used by demo/app layer to persist tool state to localStorage or server.
139
- *
140
- * @param callback Function to call with serialized state on changes
141
122
  */
142
123
  setOnStateChange(callback) {
143
124
  this.onStateChange = callback;
@@ -156,8 +137,6 @@ export class ElementToolStateStore {
156
137
  * Loads tool state from serialized format.
157
138
  * Used to restore state from localStorage or server. Subscribers are
158
139
  * notified; the persistence callback is not, so a restore never writes back.
159
- *
160
- * @param state Serialized state object
161
140
  */
162
141
  loadState(state) {
163
142
  this.elementStates.clear();
@@ -167,11 +146,7 @@ export class ElementToolStateStore {
167
146
  }
168
147
  this._notifyListeners();
169
148
  }
170
- /**
171
- * Clears all tool state for a specific element.
172
- *
173
- * @param globalElementId Composite key identifying the element
174
- */
149
+ /** Clears all tool state for a specific element. */
175
150
  clearElement(globalElementId) {
176
151
  this.elementStates.delete(globalElementId);
177
152
  this._notifyListeners();
@@ -180,8 +155,6 @@ export class ElementToolStateStore {
180
155
  /**
181
156
  * Clears state for a specific tool across all elements.
182
157
  * Useful when disabling a tool globally.
183
- *
184
- * @param toolId Tool identifier
185
158
  */
186
159
  clearTool(toolId) {
187
160
  for (const elementState of this.elementStates.values()) {
@@ -191,11 +164,8 @@ export class ElementToolStateStore {
191
164
  this._notifyStateChange();
192
165
  }
193
166
  /**
194
- * Clears all tool state for a specific section.
167
+ * Clears all tool state for a specific section, across its attempts.
195
168
  * Useful when unmounting/disposing a section.
196
- *
197
- * @param assessmentId Assessment identifier
198
- * @param sectionId Section identifier
199
169
  */
200
170
  clearSection(assessmentId, sectionId) {
201
171
  const prefix = `${escapeIdPart(assessmentId)}:${escapeIdPart(sectionId)}:`;
@@ -11,8 +11,6 @@
11
11
  * - Configurable colors and styles
12
12
  * - Annotation persistence via RangeSerializer
13
13
  *
14
- * Part of PIE Assessment Toolkit.
15
- *
16
14
  * Browser Support:
17
15
  * - Chrome/Edge 105+
18
16
  * - Safari 17.2+
@@ -63,6 +61,11 @@ export declare class HighlightCoordinator implements HighlightCoordinatorApi {
63
61
  private rangeSerializer;
64
62
  private themeObserver;
65
63
  constructor();
64
+ /**
65
+ * Re-adapt the TTS colors when the page's theme moves: on the document
66
+ * element, on a `<pie-theme>` host, and when a `<pie-theme>` mounts, which
67
+ * themes its target as it connects.
68
+ */
66
69
  private setupThemeObservation;
67
70
  private parseColor;
68
71
  private relativeLuminance;
@@ -100,12 +103,10 @@ export declare class HighlightCoordinator implements HighlightCoordinatorApi {
100
103
  * lives in a font-driven pseudo-element a CSS range cannot paint) and
101
104
  * whole-expression fallbacks.
102
105
  *
103
- * The supplied element is painted exactly; we deliberately do NOT walk up to
104
- * a containing `<math>` / `<mjx-container>`. That escalation is what made
105
- * MathJax-rendered math highlight as a full block while native MathML tracked
106
- * per-token: a resolved single token must paint as a token, and only a
107
- * genuine expression fallback (where the element already is the container)
108
- * should paint the whole expression.
106
+ * The supplied element is painted exactly, with no walk up to a containing
107
+ * `<math>` / `<mjx-container>`: a resolved token paints as a token, and only
108
+ * an expression fallback, where the element already is the container, paints
109
+ * the whole expression.
109
110
  */
110
111
  highlightTTSWordElement(element: Element): void;
111
112
  /**
@@ -196,23 +197,10 @@ export declare class HighlightCoordinator implements HighlightCoordinatorApi {
196
197
  color: HighlightColor;
197
198
  timestamp?: number;
198
199
  }>, root?: Element): number;
199
- /**
200
- * Highlight a range as the given type: the TTS types paint as
201
- * {@link highlightTTSWord} and {@link highlightTTSSentence} do.
202
- */
203
- highlightRange(range: Range, type: HighlightType, color?: HighlightColor): void;
204
- /**
205
- * Clear highlights of a specific type (interface method)
206
- */
207
- clearHighlights(type: HighlightType): void;
208
200
  /**
209
201
  * Clear all highlights (interface method)
210
202
  */
211
203
  clearAll(): void;
212
- /**
213
- * Check if CSS Highlight API is supported (interface method)
214
- */
215
- isSupported(): boolean;
216
204
  /**
217
205
  * Update TTS highlight style dynamically
218
206
  *
@@ -11,8 +11,6 @@
11
11
  * - Configurable colors and styles
12
12
  * - Annotation persistence via RangeSerializer
13
13
  *
14
- * Part of PIE Assessment Toolkit.
15
- *
16
14
  * Browser Support:
17
15
  * - Chrome/Edge 105+
18
16
  * - Safari 17.2+
@@ -383,31 +381,55 @@ export class HighlightCoordinator {
383
381
  this.applyAdaptiveTTSStyle();
384
382
  this.setupThemeObservation();
385
383
  }
384
+ /**
385
+ * Re-adapt the TTS colors when the page's theme moves: on the document
386
+ * element, on a `<pie-theme>` host, and when a `<pie-theme>` mounts, which
387
+ * themes its target as it connects.
388
+ */
386
389
  setupThemeObservation() {
387
390
  if (typeof document === "undefined")
388
391
  return;
389
392
  if (typeof MutationObserver === "undefined")
390
393
  return;
391
- const refresh = () => this.applyAdaptiveTTSStyle();
392
- this.themeObserver = new MutationObserver(refresh);
393
- this.themeObserver.observe(document.documentElement, {
394
+ const themeHosts = document.getElementsByTagName("pie-theme");
395
+ const observedHosts = new WeakSet();
396
+ const observeNewHosts = () => {
397
+ let found = false;
398
+ for (const host of themeHosts) {
399
+ if (observedHosts.has(host))
400
+ continue;
401
+ observedHosts.add(host);
402
+ observer.observe(host, {
403
+ attributes: true,
404
+ attributeFilter: [
405
+ "theme",
406
+ "scheme",
407
+ "provider",
408
+ "variables",
409
+ "style",
410
+ "data-theme",
411
+ "data-color-scheme",
412
+ ],
413
+ });
414
+ found = true;
415
+ }
416
+ return found;
417
+ };
418
+ const observer = new MutationObserver((records) => {
419
+ const mounted = observeNewHosts();
420
+ if (mounted || records.some((record) => record.type === "attributes")) {
421
+ this.applyAdaptiveTTSStyle();
422
+ }
423
+ });
424
+ this.themeObserver = observer;
425
+ observer.observe(document.documentElement, {
394
426
  attributes: true,
395
427
  attributeFilter: ["style", "data-theme", "data-color-scheme", "class"],
396
428
  });
397
- for (const host of document.querySelectorAll("pie-theme")) {
398
- this.themeObserver.observe(host, {
399
- attributes: true,
400
- attributeFilter: [
401
- "theme",
402
- "scheme",
403
- "provider",
404
- "variables",
405
- "style",
406
- "data-theme",
407
- "data-color-scheme",
408
- ],
409
- });
410
- }
429
+ // Insertions, so a `<pie-theme>` mounted later is found. An insertion
430
+ // costs a walk of the live host list, which holds a handful at most.
431
+ observer.observe(document, { childList: true, subtree: true });
432
+ observeNewHosts();
411
433
  }
412
434
  parseColor(input) {
413
435
  if (!input)
@@ -511,6 +533,8 @@ export class HighlightCoordinator {
511
533
  : [0.8, 0.72, 0.68, 0.62, 0.56, 0.5];
512
534
  let selectedOpacity = opacityCandidates[opacityCandidates.length - 1];
513
535
  let bestScore = -Infinity;
536
+ // The first opacity whose blend clears 1.25:1 against the background and
537
+ // 2.4:1 against the text wins; otherwise the best weighted score.
514
538
  for (const opacity of opacityCandidates) {
515
539
  const blended = this.blend(accent, background, opacity);
516
540
  const backgroundDelta = this.contrastRatio(blended, background);
@@ -593,7 +617,6 @@ export class HighlightCoordinator {
593
617
  return;
594
618
  if (typeof document === "undefined")
595
619
  return; // SSR guard
596
- // Check if styles already exist
597
620
  if (document.getElementById("pie-highlight-styles"))
598
621
  return;
599
622
  const style = document.createElement("style");
@@ -633,12 +656,10 @@ export class HighlightCoordinator {
633
656
  * lives in a font-driven pseudo-element a CSS range cannot paint) and
634
657
  * whole-expression fallbacks.
635
658
  *
636
- * The supplied element is painted exactly; we deliberately do NOT walk up to
637
- * a containing `<math>` / `<mjx-container>`. That escalation is what made
638
- * MathJax-rendered math highlight as a full block while native MathML tracked
639
- * per-token: a resolved single token must paint as a token, and only a
640
- * genuine expression fallback (where the element already is the container)
641
- * should paint the whole expression.
659
+ * The supplied element is painted exactly, with no walk up to a containing
660
+ * `<math>` / `<mjx-container>`: a resolved token paints as a token, and only
661
+ * an expression fallback, where the element already is the container, paints
662
+ * the whole expression.
642
663
  */
643
664
  highlightTTSWordElement(element) {
644
665
  if (!this.supported)
@@ -660,7 +681,6 @@ export class HighlightCoordinator {
660
681
  if (!this.supported)
661
682
  return;
662
683
  this.applyAdaptiveTTSStyle(startElementOf(ranges));
663
- // Clear previous sentence highlight
664
684
  this.clearTTSSentence();
665
685
  const highlight = sharedHighlight(TTS_SENTENCE_HIGHLIGHT);
666
686
  for (const range of ranges) {
@@ -689,12 +709,11 @@ export class HighlightCoordinator {
689
709
  // The read-along (sentence) band legitimately covers whole equations and
690
710
  // replaced elements, so it escalates to them.
691
711
  static SENTENCE_FALLBACK_SELECTOR = "math, mjx-container, svg, img, canvas, [role='img']";
692
- // The word layer must NOT escalate a multi-node range to the enclosing
693
- // `<math>` / `<mjx-container>`: that is what made an equation flash as a
694
- // whole block whenever a spoken word's visible range crossed several math
695
- // glyph nodes. Math is tracked per token (or held / region) by the highlight
696
- // pipeline instead. Replaced elements (svg/img/canvas) still need the element
697
- // fallback because a CSS range cannot paint them.
712
+ // The word layer falls back to the element only for replaced elements
713
+ // (svg/img/canvas), which a CSS range cannot paint. Escalating to the
714
+ // enclosing `<math>` / `<mjx-container>` flashed the whole equation whenever
715
+ // a word crossed several glyph nodes, so math is tracked per token, held or
716
+ // by region in the highlight pipeline.
698
717
  static WORD_FALLBACK_SELECTOR = "svg, img, canvas, [role='img']";
699
718
  highlightTTSElementFallbacks(ranges) {
700
719
  if (typeof Element === "undefined")
@@ -796,10 +815,8 @@ export class HighlightCoordinator {
796
815
  */
797
816
  addAnnotation(range, color = HighlightColor.YELLOW) {
798
817
  const id = `annotation-${this.nextAnnotationId++}`;
799
- // Clone the range to store
800
818
  const clonedRange = range.cloneRange();
801
819
  adoptHighlightStylesFor(clonedRange.startContainer);
802
- // Store annotation data
803
820
  const annotation = {
804
821
  id,
805
822
  range: clonedRange,
@@ -913,44 +930,6 @@ export class HighlightCoordinator {
913
930
  // ============================================================================
914
931
  // HighlightCoordinatorApi interface implementation
915
932
  // ============================================================================
916
- /**
917
- * Highlight a range as the given type: the TTS types paint as
918
- * {@link highlightTTSWord} and {@link highlightTTSSentence} do.
919
- */
920
- highlightRange(range, type, color = HighlightColor.YELLOW) {
921
- if (!this.supported)
922
- return;
923
- switch (type) {
924
- case HighlightType.TTS_WORD:
925
- this.highlightTTSWord([range]);
926
- break;
927
- case HighlightType.TTS_SENTENCE:
928
- this.highlightTTSSentence([range]);
929
- break;
930
- case HighlightType.ANNOTATION:
931
- // For annotations, use the existing method
932
- this.addAnnotation(range, color);
933
- break;
934
- }
935
- }
936
- /**
937
- * Clear highlights of a specific type (interface method)
938
- */
939
- clearHighlights(type) {
940
- if (!this.supported)
941
- return;
942
- switch (type) {
943
- case HighlightType.TTS_WORD:
944
- this.clearTTSWord();
945
- break;
946
- case HighlightType.TTS_SENTENCE:
947
- this.clearTTSSentence();
948
- break;
949
- case HighlightType.ANNOTATION:
950
- this.clearAnnotations();
951
- break;
952
- }
953
- }
954
933
  /**
955
934
  * Clear all highlights (interface method)
956
935
  */
@@ -960,12 +939,6 @@ export class HighlightCoordinator {
960
939
  this.clearTTS();
961
940
  this.clearAnnotations();
962
941
  }
963
- /**
964
- * Check if CSS Highlight API is supported (interface method)
965
- */
966
- isSupported() {
967
- return this.supported;
968
- }
969
942
  /**
970
943
  * Update TTS highlight style dynamically
971
944
  *
@@ -980,7 +953,6 @@ export class HighlightCoordinator {
980
953
  const styleEl = document.getElementById("pie-highlight-styles");
981
954
  if (!styleEl)
982
955
  return;
983
- // Convert hex to rgba
984
956
  const hexToRgb = (hex) => {
985
957
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
986
958
  return result