@pie-players/pie-section-player 0.2.12 → 0.2.14

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 (64) hide show
  1. package/README.md +28 -568
  2. package/dist/component-definitions.d.ts +0 -3
  3. package/dist/component-definitions.d.ts.map +1 -1
  4. package/dist/components/section-player-vertical-element.d.ts +2 -0
  5. package/dist/components/section-player-vertical-element.d.ts.map +1 -0
  6. package/dist/components/shared/composition.d.ts +9 -0
  7. package/dist/components/shared/composition.d.ts.map +1 -0
  8. package/dist/components/shared/player-action.d.ts +18 -0
  9. package/dist/components/shared/player-action.d.ts.map +1 -0
  10. package/dist/components/shared/player-preload.d.ts +37 -0
  11. package/dist/components/shared/player-preload.d.ts.map +1 -0
  12. package/dist/components/shared/section-player-runtime.d.ts +104 -0
  13. package/dist/components/shared/section-player-runtime.d.ts.map +1 -0
  14. package/dist/components/shared/section-player-view-state.d.ts +24 -0
  15. package/dist/components/shared/section-player-view-state.d.ts.map +1 -0
  16. package/dist/controllers/SectionContentService.d.ts +3 -0
  17. package/dist/controllers/SectionContentService.d.ts.map +1 -1
  18. package/dist/controllers/SectionController.d.ts +55 -1
  19. package/dist/controllers/SectionController.d.ts.map +1 -1
  20. package/dist/controllers/SectionSessionService.d.ts +0 -1
  21. package/dist/controllers/SectionSessionService.d.ts.map +1 -1
  22. package/dist/controllers/toolkit-section-contracts.d.ts +2 -28
  23. package/dist/controllers/toolkit-section-contracts.d.ts.map +1 -1
  24. package/dist/controllers/types.d.ts +97 -6
  25. package/dist/controllers/types.d.ts.map +1 -1
  26. package/dist/pie-item-player-q4jcP2lZ.js +6196 -0
  27. package/dist/pie-section-player.d.ts +0 -8
  28. package/dist/pie-section-player.d.ts.map +1 -1
  29. package/dist/pie-section-player.js +61631 -11
  30. package/dist/player-preload-CQVG0Bih.js +705 -0
  31. package/dist/utils/player-preload.d.ts +2 -0
  32. package/dist/utils/player-preload.d.ts.map +1 -0
  33. package/dist/utils/player-preload.js +8 -0
  34. package/package.json +24 -32
  35. package/src/components/ItemShellElement.svelte +119 -3
  36. package/src/components/PassageShellElement.svelte +49 -0
  37. package/src/components/PieSectionPlayerBaseElement.svelte +65 -78
  38. package/src/components/PieSectionPlayerSplitPaneElement.svelte +337 -296
  39. package/src/components/PieSectionPlayerVerticalElement.svelte +446 -0
  40. package/src/components/shared/SectionItemCard.svelte +92 -0
  41. package/src/components/shared/SectionPassageCard.svelte +88 -0
  42. package/dist/ItemRenderer-MsjF_Beu.js +0 -467
  43. package/dist/PieItemModeLayoutElement-D7oTzA9T.js +0 -316
  44. package/dist/PieSplitPanelLayoutElement-GUtJ_NlF.js +0 -246
  45. package/dist/PieVerticalLayoutElement-BoA3FO5g.js +0 -194
  46. package/dist/controllers/SectionToolkitService.d.ts +0 -24
  47. package/dist/controllers/SectionToolkitService.d.ts.map +0 -1
  48. package/dist/controllers/SessionPersistenceStrategy.d.ts +0 -15
  49. package/dist/controllers/SessionPersistenceStrategy.d.ts.map +0 -1
  50. package/dist/index.d.ts +0 -2
  51. package/dist/pie-section-player-DJ5NcwdT.js +0 -17078
  52. package/dist/runtime/runtime-event-guards.d.ts +0 -4
  53. package/dist/runtime/runtime-event-guards.d.ts.map +0 -1
  54. package/src/PieSectionPlayer.svelte +0 -826
  55. package/src/components/ItemModeLayout.svelte +0 -172
  56. package/src/components/ItemNavigation.svelte +0 -96
  57. package/src/components/ItemPlayerBridge.svelte +0 -110
  58. package/src/components/ItemRenderer.svelte +0 -248
  59. package/src/components/ItemShell.svelte +0 -86
  60. package/src/components/layout-elements/PieItemModeLayoutElement.svelte +0 -47
  61. package/src/components/layout-elements/PieSplitPanelLayoutElement.svelte +0 -62
  62. package/src/components/layout-elements/PieVerticalLayoutElement.svelte +0 -41
  63. package/src/components/layouts/SplitPanelLayout.svelte +0 -385
  64. package/src/components/layouts/VerticalLayout.svelte +0 -193
@@ -0,0 +1 @@
1
+ {"version":3,"file":"player-preload.d.ts","sourceRoot":"","sources":["../../src/utils/player-preload.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,qBAAqB,EACrB,aAAa,EACb,uBAAuB,EACvB,+BAA+B,EAC/B,qBAAqB,EACrB,KAAK,kBAAkB,GACvB,MAAM,wCAAwC,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { b as r, a as l, g as t, o, p as s } from "../player-preload-CQVG0Bih.js";
2
+ export {
3
+ r as buildPreloadSignature,
4
+ l as getLoaderView,
5
+ t as getRenderablesSignature,
6
+ o as orchestratePlayerElementPreload,
7
+ s as preloadPlayerElements
8
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pie-players/pie-section-player",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
4
4
  "type": "module",
5
5
  "description": "Web component for rendering QTI 3.0 assessment sections with passages and items",
6
6
  "license": "MIT",
@@ -15,35 +15,23 @@
15
15
  "unpkg": "./dist/pie-section-player.js",
16
16
  "jsdelivr": "./dist/pie-section-player.js",
17
17
  "main": "./dist/pie-section-player.js",
18
- "types": "./dist/index.d.ts",
18
+ "types": "./dist/pie-section-player.d.ts",
19
19
  "exports": {
20
20
  ".": {
21
- "types": "./dist/index.d.ts",
22
- "import": "./dist/pie-section-player.js"
23
- },
24
- "./components/ItemShellElement.svelte": {
25
- "types": "./dist/pie-section-player.d.ts",
26
- "import": "./dist/pie-section-player.js"
27
- },
28
- "./components/PassageShellElement.svelte": {
29
- "types": "./dist/pie-section-player.d.ts",
30
- "import": "./dist/pie-section-player.js"
31
- },
32
- "./components/section-player-base-element": {
33
21
  "types": "./dist/pie-section-player.d.ts",
34
22
  "import": "./dist/pie-section-player.js"
35
23
  },
36
- "./components/item-shell-element": {
24
+ "./components/section-player-splitpane-element": {
37
25
  "types": "./dist/pie-section-player.d.ts",
38
26
  "import": "./dist/pie-section-player.js"
39
27
  },
40
- "./components/passage-shell-element": {
28
+ "./components/section-player-vertical-element": {
41
29
  "types": "./dist/pie-section-player.d.ts",
42
30
  "import": "./dist/pie-section-player.js"
43
31
  },
44
- "./components/section-player-splitpane-element": {
45
- "types": "./dist/pie-section-player.d.ts",
46
- "import": "./dist/pie-section-player.js"
32
+ "./utils/player-preload": {
33
+ "types": "./dist/utils/player-preload.d.ts",
34
+ "import": "./dist/utils/player-preload.js"
47
35
  }
48
36
  },
49
37
  "files": [
@@ -61,22 +49,26 @@
61
49
  "svelte": "^5.51.0"
62
50
  },
63
51
  "dependencies": {
64
- "@pie-players/pie-assessment-toolkit": "0.2.9",
65
- "@pie-players/pie-esm-player": "0.2.6",
66
- "@pie-players/pie-fixed-player": "0.2.5",
67
- "@pie-players/pie-iife-player": "0.2.6",
68
- "@pie-players/pie-inline-player": "0.2.5",
69
- "@pie-players/pie-context": "0.1.1",
70
- "@pie-players/pie-players-shared": "0.2.5",
71
- "@pie-players/pie-section-tools-toolbar": "0.2.10",
72
- "@pie-players/pie-tool-answer-eliminator": "0.2.9",
73
- "@pie-players/pie-tool-calculator": "0.1.9",
74
- "@pie-players/pie-tool-calculator-inline": "0.1.6",
75
- "@pie-players/pie-tool-tts-inline": "0.1.6",
76
- "@pie-players/tts-client-server": "0.2.4",
52
+ "@pie-players/pie-assessment-toolkit": "0.2.11",
53
+ "@pie-players/pie-item-player": "0.1.2",
54
+ "@pie-players/pie-context": "0.1.3",
55
+ "@pie-players/pie-players-shared": "0.2.7",
56
+ "@pie-players/pie-toolbars": "0.1.2",
57
+ "@pie-players/pie-tool-answer-eliminator": "0.2.11",
58
+ "@pie-players/pie-tool-annotation-toolbar": "0.1.11",
59
+ "@pie-players/pie-tool-calculator": "0.1.11",
60
+ "@pie-players/pie-tool-theme": "0.1.11",
61
+ "@pie-players/pie-tool-graph": "0.1.11",
62
+ "@pie-players/pie-tool-line-reader": "0.1.11",
63
+ "@pie-players/pie-tool-periodic-table": "0.1.11",
64
+ "@pie-players/pie-tool-protractor": "0.1.11",
65
+ "@pie-players/pie-tool-ruler": "0.1.11",
66
+ "@pie-players/pie-tool-text-to-speech": "0.1.11",
67
+ "@pie-players/tts-client-server": "0.2.6",
77
68
  "daisyui": "^5.5.18"
78
69
  },
79
70
  "devDependencies": {
71
+ "@axe-core/playwright": "^4.11.1",
80
72
  "@biomejs/biome": "^2.3.13",
81
73
  "@playwright/test": "^1.58.2",
82
74
  "@sveltejs/vite-plugin-svelte": "^6.2.4",
@@ -13,8 +13,16 @@
13
13
  }}
14
14
  />
15
15
 
16
+ <script module lang="ts">
17
+ const crossShellSessionDedupe = new Map<
18
+ string,
19
+ { fingerprint: string; timestamp: number }
20
+ >();
21
+ </script>
22
+
16
23
  <script lang="ts">
17
24
  import {
25
+ PIE_INTERNAL_ITEM_SESSION_CHANGED_EVENT,
18
26
  PIE_ITEM_SESSION_CHANGED_EVENT,
19
27
  PIE_REGISTER_EVENT,
20
28
  PIE_UNREGISTER_EVENT,
@@ -23,11 +31,28 @@
23
31
  dispatchCrossBoundaryEvent,
24
32
  type AssessmentToolkitRegionScopeContext,
25
33
  type AssessmentToolkitShellContext,
34
+ type InternalItemSessionChangedDetail,
26
35
  type ItemSessionChangedDetail,
27
36
  type RuntimeRegistrationDetail,
28
37
  } from "@pie-players/pie-assessment-toolkit";
38
+ import { normalizeItemSessionChange } from "@pie-players/pie-players-shared";
29
39
  import { ContextProvider, ContextRoot } from "@pie-players/pie-context";
30
40
 
41
+ const PIE_INTERNAL_CONTENT_LOADED_EVENT = "pie-content-loaded";
42
+ const PIE_INTERNAL_ITEM_PLAYER_ERROR_EVENT = "pie-item-player-error";
43
+ type InternalContentLoadedDetail = {
44
+ itemId: string;
45
+ canonicalItemId?: string;
46
+ contentKind?: string;
47
+ detail?: unknown;
48
+ };
49
+ type InternalItemPlayerErrorDetail = {
50
+ itemId: string;
51
+ canonicalItemId?: string;
52
+ contentKind?: string;
53
+ error: unknown;
54
+ };
55
+
31
56
  let {
32
57
  itemId = "",
33
58
  canonicalItemId = "",
@@ -100,24 +125,115 @@
100
125
  function normalizeAndDispatchSession(event: Event): void {
101
126
  if (!host || !itemId) return;
102
127
  const detail = (event as CustomEvent).detail;
128
+ const internalPayload: InternalItemSessionChangedDetail = {
129
+ itemId,
130
+ session: detail,
131
+ };
132
+ // Internal runtime session wiring must always continue to keep item UI/state synchronized.
133
+ dispatchCrossBoundaryEvent(host, PIE_INTERNAL_ITEM_SESSION_CHANGED_EVENT, internalPayload);
134
+ // Keep public item stream response-focused; metadata-only belongs on canonical session-changed.
135
+ const normalized = normalizeItemSessionChange({
136
+ itemId,
137
+ sessionDetail: detail,
138
+ });
139
+ if (normalized.intent === "metadata-only" || !normalized.session) {
140
+ return;
141
+ }
103
142
  const payload: ItemSessionChangedDetail = {
104
143
  itemId,
105
144
  canonicalItemId: canonicalItemId || itemId,
106
- session: detail,
145
+ session: normalized.session,
107
146
  };
108
147
  dispatchCrossBoundaryEvent(host, PIE_ITEM_SESSION_CHANGED_EVENT, payload);
109
- dispatchCrossBoundaryEvent(host, "item-session-changed", payload);
148
+ }
149
+
150
+ function dispatchLoaded(detail: unknown): void {
151
+ if (!host || !itemId) return;
152
+ const payload: InternalContentLoadedDetail = {
153
+ itemId,
154
+ canonicalItemId: canonicalItemId || itemId,
155
+ contentKind,
156
+ detail,
157
+ };
158
+ dispatchCrossBoundaryEvent(host, PIE_INTERNAL_CONTENT_LOADED_EVENT, payload);
159
+ }
160
+
161
+ function dispatchPlayerError(error: unknown): void {
162
+ if (!host || !itemId) return;
163
+ const payload: InternalItemPlayerErrorDetail = {
164
+ itemId,
165
+ canonicalItemId: canonicalItemId || itemId,
166
+ contentKind,
167
+ error,
168
+ };
169
+ dispatchCrossBoundaryEvent(host, PIE_INTERNAL_ITEM_PLAYER_ERROR_EVENT, payload);
170
+ }
171
+
172
+ function createSessionEventFingerprint(detail: unknown): string {
173
+ const semanticDetail =
174
+ detail && typeof detail === "object"
175
+ ? { ...(detail as Record<string, unknown>) }
176
+ : detail;
177
+ if (semanticDetail && typeof semanticDetail === "object") {
178
+ delete (semanticDetail as Record<string, unknown>).timestamp;
179
+ delete (semanticDetail as Record<string, unknown>).sourceRuntimeId;
180
+ }
181
+ try {
182
+ return JSON.stringify(semanticDetail);
183
+ } catch {
184
+ return String(semanticDetail);
185
+ }
110
186
  }
111
187
 
112
188
  $effect(() => {
113
189
  if (!host) return;
114
190
  dispatchRegistration(PIE_REGISTER_EVENT);
115
191
 
116
- const onSessionChanged = (event: Event) => normalizeAndDispatchSession(event);
192
+ const seenSessionEvents = new WeakSet<Event>();
193
+ let lastForwardedFingerprint = "";
194
+ const CROSS_SHELL_DEDUPE_WINDOW_MS = 500;
195
+ const onSessionChanged = (event: Event) => {
196
+ // Keep framework surface minimal: normalize these descendant events and do not
197
+ // leak raw item-player session events to external listeners.
198
+ event.stopPropagation();
199
+ // Some players emit `session-changed`, others emit `sessionchanged`.
200
+ // Guard against duplicate forwarding when both fire for the same payload.
201
+ if (seenSessionEvents.has(event)) return;
202
+ seenSessionEvents.add(event);
203
+ const fingerprint = createSessionEventFingerprint((event as CustomEvent).detail);
204
+ if (fingerprint === lastForwardedFingerprint) return;
205
+ const dedupeKey = itemId || canonicalItemId || "__unknown-item__";
206
+ const now = Date.now();
207
+ const lastCrossShell = crossShellSessionDedupe.get(dedupeKey);
208
+ if (
209
+ lastCrossShell &&
210
+ lastCrossShell.fingerprint === fingerprint &&
211
+ now - lastCrossShell.timestamp < CROSS_SHELL_DEDUPE_WINDOW_MS
212
+ ) {
213
+ return;
214
+ }
215
+ crossShellSessionDedupe.set(dedupeKey, { fingerprint, timestamp: now });
216
+ lastForwardedFingerprint = fingerprint;
217
+ normalizeAndDispatchSession(event);
218
+ };
117
219
  host.addEventListener("sessionchanged", onSessionChanged);
220
+ host.addEventListener("session-changed", onSessionChanged);
221
+ const onLoadComplete = (event: Event) => {
222
+ event.stopPropagation();
223
+ dispatchLoaded((event as CustomEvent).detail);
224
+ };
225
+ const onPlayerError = (event: Event) => {
226
+ event.stopPropagation();
227
+ dispatchPlayerError((event as CustomEvent).detail);
228
+ };
229
+ host.addEventListener("load-complete", onLoadComplete);
230
+ host.addEventListener("player-error", onPlayerError);
118
231
 
119
232
  return () => {
120
233
  host?.removeEventListener("sessionchanged", onSessionChanged);
234
+ host?.removeEventListener("session-changed", onSessionChanged);
235
+ host?.removeEventListener("load-complete", onLoadComplete);
236
+ host?.removeEventListener("player-error", onPlayerError);
121
237
  dispatchRegistration(PIE_UNREGISTER_EVENT);
122
238
  };
123
239
  });
@@ -26,6 +26,21 @@
26
26
  } from "@pie-players/pie-assessment-toolkit";
27
27
  import { ContextProvider, ContextRoot } from "@pie-players/pie-context";
28
28
 
29
+ const PIE_INTERNAL_CONTENT_LOADED_EVENT = "pie-content-loaded";
30
+ const PIE_INTERNAL_ITEM_PLAYER_ERROR_EVENT = "pie-item-player-error";
31
+ type InternalContentLoadedDetail = {
32
+ itemId: string;
33
+ canonicalItemId?: string;
34
+ contentKind?: string;
35
+ detail?: unknown;
36
+ };
37
+ type InternalItemPlayerErrorDetail = {
38
+ itemId: string;
39
+ canonicalItemId?: string;
40
+ contentKind?: string;
41
+ error: unknown;
42
+ };
43
+
29
44
  let {
30
45
  itemId = "",
31
46
  canonicalItemId = "",
@@ -95,11 +110,45 @@
95
110
  dispatchCrossBoundaryEvent(host, eventName, detail);
96
111
  }
97
112
 
113
+ function dispatchLoaded(detail: unknown): void {
114
+ if (!host || !itemId) return;
115
+ const payload: InternalContentLoadedDetail = {
116
+ itemId,
117
+ canonicalItemId: canonicalItemId || itemId,
118
+ contentKind,
119
+ detail,
120
+ };
121
+ dispatchCrossBoundaryEvent(host, PIE_INTERNAL_CONTENT_LOADED_EVENT, payload);
122
+ }
123
+
124
+ function dispatchPlayerError(error: unknown): void {
125
+ if (!host || !itemId) return;
126
+ const payload: InternalItemPlayerErrorDetail = {
127
+ itemId,
128
+ canonicalItemId: canonicalItemId || itemId,
129
+ contentKind,
130
+ error,
131
+ };
132
+ dispatchCrossBoundaryEvent(host, PIE_INTERNAL_ITEM_PLAYER_ERROR_EVENT, payload);
133
+ }
134
+
98
135
  $effect(() => {
99
136
  if (!host) return;
100
137
  dispatchRegistration(PIE_REGISTER_EVENT);
138
+ const onLoadComplete = (event: Event) => {
139
+ event.stopPropagation();
140
+ dispatchLoaded((event as CustomEvent).detail);
141
+ };
142
+ const onPlayerError = (event: Event) => {
143
+ event.stopPropagation();
144
+ dispatchPlayerError((event as CustomEvent).detail);
145
+ };
146
+ host.addEventListener("load-complete", onLoadComplete);
147
+ host.addEventListener("player-error", onPlayerError);
101
148
 
102
149
  return () => {
150
+ host?.removeEventListener("load-complete", onLoadComplete);
151
+ host?.removeEventListener("player-error", onPlayerError);
103
152
  dispatchRegistration(PIE_UNREGISTER_EVENT);
104
153
  };
105
154
  });
@@ -8,7 +8,6 @@
8
8
  section: { type: "Object", reflect: false },
9
9
  sectionId: { attribute: "section-id", type: "String" },
10
10
  attemptId: { attribute: "attempt-id", type: "String" },
11
- view: { type: "String" },
12
11
  playerType: { attribute: "player-type", type: "String" },
13
12
  player: { type: "Object", reflect: false },
14
13
  lazyInit: { attribute: "lazy-init", type: "Boolean" },
@@ -24,52 +23,33 @@
24
23
 
25
24
  <script lang="ts">
26
25
  import "@pie-players/pie-assessment-toolkit/components/pie-assessment-toolkit-element";
26
+ import "@pie-players/pie-tool-annotation-toolbar";
27
27
  import {
28
28
  createDefaultPersonalNeedsProfile,
29
29
  } from "@pie-players/pie-assessment-toolkit";
30
+ import {
31
+ normalizeToolsConfig,
32
+ resolveToolsForLevel,
33
+ } from "@pie-players/pie-assessment-toolkit";
30
34
  import { createEventDispatcher } from "svelte";
31
35
  import { SectionController } from "../controllers/SectionController.js";
32
36
  import type { SectionCompositionModel } from "../controllers/types.js";
33
37
  import type { AssessmentSection } from "@pie-players/pie-players-shared/types";
34
-
35
- const EMPTY_COMPOSITION: SectionCompositionModel = {
36
- section: null,
37
- assessmentItemRefs: [],
38
- passages: [],
39
- items: [],
40
- rubricBlocks: [],
41
- instructions: [],
42
- currentItemIndex: 0,
43
- currentItem: null,
44
- isPageMode: false,
45
- itemSessionsByItemId: {},
46
- testAttemptSession: null,
47
- };
48
- const DEFAULT_ASSESSMENT_ID = "section-demo-direct";
49
- const DEFAULT_PLAYER_TYPE = "iife";
50
- const DEFAULT_LAZY_INIT = true;
51
- const DEFAULT_ISOLATION = "inherit";
52
- const LEGACY_RUNTIME_WARNING_KEY = "pie-section-player-base:legacy-runtime-props";
53
- const warnedKeys = new Set<string>();
54
- type RuntimeConfig = {
55
- assessmentId?: string;
56
- playerType?: string;
57
- player?: Record<string, unknown> | null;
58
- lazyInit?: boolean;
59
- tools?: Record<string, unknown> | null;
60
- accessibility?: Record<string, unknown> | null;
61
- coordinator?: unknown;
62
- createSectionController?: unknown;
63
- isolation?: string;
64
- env?: Record<string, unknown>;
65
- };
38
+ import { EMPTY_COMPOSITION } from "./shared/composition.js";
39
+ import {
40
+ DEFAULT_ASSESSMENT_ID,
41
+ DEFAULT_ENV,
42
+ DEFAULT_ISOLATION,
43
+ DEFAULT_LAZY_INIT,
44
+ DEFAULT_PLAYER_TYPE,
45
+ type RuntimeConfig,
46
+ } from "./shared/section-player-runtime.js";
66
47
  let {
67
48
  assessmentId = DEFAULT_ASSESSMENT_ID,
68
49
  runtime = null as RuntimeConfig | null,
69
50
  section = null as AssessmentSection | null,
70
51
  sectionId = "",
71
52
  attemptId = "",
72
- view = "candidate",
73
53
  playerType = DEFAULT_PLAYER_TYPE,
74
54
  player = null as Record<string, unknown> | null,
75
55
  lazyInit = DEFAULT_LAZY_INIT,
@@ -82,7 +62,8 @@
82
62
  } = $props();
83
63
 
84
64
  let toolkitElement = $state<any>(null);
85
- let lastCompositionSignature = $state("");
65
+ let activeToolkitCoordinator = $state<any>(null);
66
+ let lastCompositionVersion = $state(-1);
86
67
  type BaseSectionPlayerEvents = {
87
68
  "composition-changed": { composition: SectionCompositionModel };
88
69
  "toolkit-ready": Record<string, unknown>;
@@ -106,7 +87,7 @@
106
87
  () => runtime?.createSectionController ?? createSectionController,
107
88
  );
108
89
  const effectiveIsolation = $derived.by(() => runtime?.isolation ?? isolation);
109
- const effectiveEnv = $derived.by(() => runtime?.env ?? env ?? {});
90
+ const effectiveEnv = $derived.by(() => runtime?.env ?? env ?? DEFAULT_ENV);
110
91
  let resolvedSection = $derived.by(() => {
111
92
  if (!section) return null;
112
93
  const sectionAny = section as any;
@@ -127,27 +108,18 @@
127
108
  dispatch(name, detail);
128
109
  }
129
110
 
130
- function getCompositionSignature(
131
- model: SectionCompositionModel | null | undefined,
132
- ): string {
133
- if (!model) return "";
134
- return JSON.stringify({
135
- sectionId: model.section?.identifier || "",
136
- currentItemIndex: model.currentItemIndex ?? -1,
137
- itemIds: (model.items || []).map((item) => item?.id || ""),
138
- passageIds: (model.passages || []).map((passage) => passage?.id || ""),
139
- sessionByItem: Object.entries(model.itemSessionsByItemId || {})
140
- .sort(([left], [right]) => left.localeCompare(right))
141
- .map(([itemId, session]) => [itemId, JSON.stringify(session ?? null)]),
142
- });
143
- }
144
-
145
111
  function handleCompositionChanged(event: Event): void {
146
- const detail = (event as CustomEvent<{ composition?: SectionCompositionModel }>).detail;
112
+ const detail = (event as CustomEvent<{
113
+ composition?: SectionCompositionModel;
114
+ version?: number;
115
+ }>).detail;
147
116
  const nextComposition = detail?.composition || EMPTY_COMPOSITION;
148
- const nextSignature = getCompositionSignature(nextComposition);
149
- if (nextSignature === lastCompositionSignature) return;
150
- lastCompositionSignature = nextSignature;
117
+ const nextVersion =
118
+ typeof detail?.version === "number"
119
+ ? detail.version
120
+ : lastCompositionVersion + 1;
121
+ if (nextVersion === lastCompositionVersion) return;
122
+ lastCompositionVersion = nextVersion;
151
123
  emit("composition-changed", {
152
124
  composition: nextComposition,
153
125
  });
@@ -158,37 +130,46 @@
158
130
  eventName: Exclude<keyof BaseSectionPlayerEvents, "composition-changed">,
159
131
  ): void {
160
132
  const detail = (event as CustomEvent).detail as Record<string, unknown>;
133
+ if (eventName === "toolkit-ready" && detail?.coordinator) {
134
+ activeToolkitCoordinator = detail.coordinator;
135
+ }
161
136
  emit(eventName, detail || ({} as Record<string, unknown>));
162
137
  }
163
138
 
139
+ const normalizedToolsConfig = $derived.by(() =>
140
+ normalizeToolsConfig((effectiveTools || {}) as any),
141
+ );
142
+ const annotationToolbarPlacementEnabled = $derived.by(() => {
143
+ const levels: Array<"section" | "item" | "passage"> = [
144
+ "section",
145
+ "item",
146
+ "passage",
147
+ ];
148
+ return levels.some((level) =>
149
+ resolveToolsForLevel(normalizedToolsConfig as any, level).includes(
150
+ "annotationToolbar",
151
+ ),
152
+ );
153
+ });
154
+ const annotationToolbarProviderEnabled = $derived.by(() =>
155
+ activeToolkitCoordinator?.isToolEnabled?.("annotationToolbar") ??
156
+ ((normalizedToolsConfig as any)?.providers?.annotationToolbar?.enabled !==
157
+ false),
158
+ );
159
+ const shouldRenderAnnotationToolbar = $derived(
160
+ Boolean(
161
+ activeToolkitCoordinator &&
162
+ annotationToolbarPlacementEnabled &&
163
+ annotationToolbarProviderEnabled,
164
+ ),
165
+ );
166
+
164
167
  $effect(() => {
165
168
  if (!toolkitElement) return;
166
169
  toolkitElement.createSectionController =
167
170
  effectiveCreateSectionController || (() => new SectionController());
168
171
  });
169
172
 
170
- $effect(() => {
171
- if (typeof window === "undefined" || runtime) return;
172
- const usedLegacyProps: string[] = [];
173
- if (assessmentId !== DEFAULT_ASSESSMENT_ID) usedLegacyProps.push("assessmentId");
174
- if (playerType !== DEFAULT_PLAYER_TYPE) usedLegacyProps.push("playerType");
175
- if (player !== null) usedLegacyProps.push("player");
176
- if (lazyInit !== DEFAULT_LAZY_INIT) usedLegacyProps.push("lazyInit");
177
- if (tools !== null) usedLegacyProps.push("tools");
178
- if (accessibility !== null) usedLegacyProps.push("accessibility");
179
- if (coordinator !== null) usedLegacyProps.push("coordinator");
180
- if (createSectionController !== null) usedLegacyProps.push("createSectionController");
181
- if (isolation !== DEFAULT_ISOLATION) usedLegacyProps.push("isolation");
182
- if (env !== null) usedLegacyProps.push("env");
183
- if (usedLegacyProps.length === 0) return;
184
- const key = `${LEGACY_RUNTIME_WARNING_KEY}:${usedLegacyProps.sort().join(",")}`;
185
- if (warnedKeys.has(key)) return;
186
- warnedKeys.add(key);
187
- console.warn(
188
- `[pie-section-player-base] Runtime props (${usedLegacyProps.join(", ")}) are deprecated. Prefer the \`runtime\` object prop.`,
189
- );
190
- });
191
-
192
173
  </script>
193
174
 
194
175
  <pie-assessment-toolkit
@@ -199,7 +180,6 @@
199
180
  attempt-id={attemptId}
200
181
  player-type={effectivePlayerType}
201
182
  player={effectivePlayer}
202
- {view}
203
183
  env={effectiveEnv}
204
184
  lazy-init={effectiveLazyInit}
205
185
  tools={effectiveTools}
@@ -214,6 +194,13 @@
214
194
  onruntime-owned={(event: Event) => handleToolkitEvent(event, "runtime-owned")}
215
195
  onruntime-inherited={(event: Event) => handleToolkitEvent(event, "runtime-inherited")}
216
196
  >
197
+ {#if shouldRenderAnnotationToolbar}
198
+ <pie-tool-annotation-toolbar
199
+ enabled={true}
200
+ ttsService={activeToolkitCoordinator.ttsService}
201
+ highlightCoordinator={activeToolkitCoordinator.highlightCoordinator}
202
+ ></pie-tool-annotation-toolbar>
203
+ {/if}
217
204
  <slot></slot>
218
205
  </pie-assessment-toolkit>
219
206