@pie-players/pie-assessment-toolkit 0.2.7 → 0.2.9

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 (220) hide show
  1. package/README.md +200 -67
  2. package/dist/attempt/TestSession.d.ts +23 -21
  3. package/dist/attempt/TestSession.d.ts.map +1 -1
  4. package/dist/attempt/TestSession.js +17 -15
  5. package/dist/attempt/TestSession.js.map +1 -1
  6. package/dist/attempt/adapters/activity-to-test-attempt-session.d.ts +50 -0
  7. package/dist/attempt/adapters/activity-to-test-attempt-session.d.ts.map +1 -0
  8. package/dist/attempt/adapters/activity-to-test-attempt-session.js +135 -0
  9. package/dist/attempt/adapters/activity-to-test-attempt-session.js.map +1 -0
  10. package/dist/components/ItemToolBar.custom-element.js +628 -0
  11. package/dist/components/PieAssessmentToolkit.custom-element.js +670 -0
  12. package/dist/components/item-toolbar-element.d.ts +3 -0
  13. package/dist/components/item-toolbar-element.d.ts.map +1 -0
  14. package/dist/components/item-toolbar-element.js +2 -0
  15. package/dist/components/item-toolbar-element.js.map +1 -0
  16. package/dist/components/pie-assessment-toolkit-element.d.ts +3 -0
  17. package/dist/components/pie-assessment-toolkit-element.d.ts.map +1 -0
  18. package/dist/components/pie-assessment-toolkit-element.js +2 -0
  19. package/dist/components/pie-assessment-toolkit-element.js.map +1 -0
  20. package/dist/context/assessment-toolkit-context.d.ts +52 -0
  21. package/dist/context/assessment-toolkit-context.d.ts.map +1 -0
  22. package/dist/context/assessment-toolkit-context.js +6 -0
  23. package/dist/context/assessment-toolkit-context.js.map +1 -0
  24. package/dist/context/runtime-context-consumer.d.ts +14 -0
  25. package/dist/context/runtime-context-consumer.d.ts.map +1 -0
  26. package/dist/context/runtime-context-consumer.js +52 -0
  27. package/dist/context/runtime-context-consumer.js.map +1 -0
  28. package/dist/index.d.ts +16 -5
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +13 -11
  31. package/dist/index.js.map +1 -1
  32. package/dist/runtime/RuntimeRegistry.d.ts +13 -0
  33. package/dist/runtime/RuntimeRegistry.d.ts.map +1 -0
  34. package/dist/runtime/RuntimeRegistry.js +50 -0
  35. package/dist/runtime/RuntimeRegistry.js.map +1 -0
  36. package/dist/runtime/SectionRuntimeEngine.d.ts +45 -0
  37. package/dist/runtime/SectionRuntimeEngine.d.ts.map +1 -0
  38. package/dist/runtime/SectionRuntimeEngine.js +81 -0
  39. package/dist/runtime/SectionRuntimeEngine.js.map +1 -0
  40. package/dist/runtime/registration-events.d.ts +19 -0
  41. package/dist/runtime/registration-events.d.ts.map +1 -0
  42. package/dist/runtime/registration-events.js +4 -0
  43. package/dist/runtime/registration-events.js.map +1 -0
  44. package/dist/runtime/runtime-event-guards.d.ts +3 -0
  45. package/dist/runtime/runtime-event-guards.d.ts.map +1 -0
  46. package/dist/runtime/runtime-event-guards.js +9 -0
  47. package/dist/runtime/runtime-event-guards.js.map +1 -0
  48. package/dist/runtime/tool-host-contract.d.ts +30 -0
  49. package/dist/runtime/tool-host-contract.d.ts.map +1 -0
  50. package/dist/runtime/tool-host-contract.js +48 -0
  51. package/dist/runtime/tool-host-contract.js.map +1 -0
  52. package/dist/services/TTSService.d.ts +3 -1
  53. package/dist/services/TTSService.d.ts.map +1 -1
  54. package/dist/services/TTSService.js +13 -12
  55. package/dist/services/TTSService.js.map +1 -1
  56. package/dist/services/ThemeProvider.d.ts +1 -0
  57. package/dist/services/ThemeProvider.d.ts.map +1 -1
  58. package/dist/services/ThemeProvider.js +82 -46
  59. package/dist/services/ThemeProvider.js.map +1 -1
  60. package/dist/services/ToolConfigResolver.d.ts +1 -1
  61. package/dist/services/ToolConfigResolver.js +1 -1
  62. package/dist/services/ToolCoordinator.d.ts +1 -1
  63. package/dist/services/ToolCoordinator.d.ts.map +1 -1
  64. package/dist/services/ToolRegistry.d.ts +64 -67
  65. package/dist/services/ToolRegistry.d.ts.map +1 -1
  66. package/dist/services/ToolRegistry.js +72 -22
  67. package/dist/services/ToolRegistry.js.map +1 -1
  68. package/dist/services/ToolkitCoordinator.d.ts +106 -8
  69. package/dist/services/ToolkitCoordinator.d.ts.map +1 -1
  70. package/dist/services/ToolkitCoordinator.js +502 -71
  71. package/dist/services/ToolkitCoordinator.js.map +1 -1
  72. package/dist/services/createDefaultToolRegistry.d.ts +12 -7
  73. package/dist/services/createDefaultToolRegistry.d.ts.map +1 -1
  74. package/dist/services/createDefaultToolRegistry.js +6 -10
  75. package/dist/services/createDefaultToolRegistry.js.map +1 -1
  76. package/dist/services/defaultPersonalNeedsProfile.d.ts +4 -0
  77. package/dist/services/defaultPersonalNeedsProfile.d.ts.map +1 -0
  78. package/dist/services/defaultPersonalNeedsProfile.js +25 -0
  79. package/dist/services/defaultPersonalNeedsProfile.js.map +1 -0
  80. package/dist/services/interfaces.d.ts +61 -1
  81. package/dist/services/interfaces.d.ts.map +1 -1
  82. package/dist/services/interfaces.js.map +1 -1
  83. package/dist/services/pnp-standard-features.d.ts +3 -4
  84. package/dist/services/pnp-standard-features.d.ts.map +1 -1
  85. package/dist/services/pnp-standard-features.js +0 -3
  86. package/dist/services/pnp-standard-features.js.map +1 -1
  87. package/dist/services/section-controller-types.d.ts +32 -0
  88. package/dist/services/section-controller-types.d.ts.map +1 -0
  89. package/dist/services/section-controller-types.js +2 -0
  90. package/dist/services/section-controller-types.js.map +1 -0
  91. package/dist/services/tool-context.d.ts +4 -4
  92. package/dist/services/tool-context.d.ts.map +1 -1
  93. package/dist/services/tool-context.js +106 -17
  94. package/dist/services/tool-context.js.map +1 -1
  95. package/dist/services/tool-instance-id.d.ts +17 -0
  96. package/dist/services/tool-instance-id.d.ts.map +1 -0
  97. package/dist/services/tool-instance-id.js +61 -0
  98. package/dist/services/tool-instance-id.js.map +1 -0
  99. package/dist/services/tool-providers/IToolProvider.d.ts +2 -3
  100. package/dist/services/tool-providers/IToolProvider.d.ts.map +1 -1
  101. package/dist/services/tool-providers/IToolProvider.js +1 -2
  102. package/dist/services/tool-providers/IToolProvider.js.map +1 -1
  103. package/dist/services/tool-providers/MathJsToolProvider.d.ts +25 -0
  104. package/dist/services/tool-providers/MathJsToolProvider.d.ts.map +1 -0
  105. package/dist/services/tool-providers/MathJsToolProvider.js +53 -0
  106. package/dist/services/tool-providers/MathJsToolProvider.js.map +1 -0
  107. package/dist/services/tool-providers/index.d.ts +2 -0
  108. package/dist/services/tool-providers/index.d.ts.map +1 -1
  109. package/dist/services/tool-providers/index.js +1 -0
  110. package/dist/services/tool-providers/index.js.map +1 -1
  111. package/dist/services/tools-config-normalizer.d.ts +26 -0
  112. package/dist/services/tools-config-normalizer.d.ts.map +1 -0
  113. package/dist/services/tools-config-normalizer.js +63 -0
  114. package/dist/services/tools-config-normalizer.js.map +1 -0
  115. package/dist/tools/calculators/ti-provider.d.ts.map +1 -1
  116. package/dist/tools/calculators/ti-provider.js +127 -4
  117. package/dist/tools/calculators/ti-provider.js.map +1 -1
  118. package/dist/tools/client.d.ts +2 -1
  119. package/dist/tools/client.d.ts.map +1 -1
  120. package/dist/tools/client.js +2 -1
  121. package/dist/tools/client.js.map +1 -1
  122. package/dist/tools/default-tool-module-loaders.d.ts +9 -0
  123. package/dist/tools/default-tool-module-loaders.d.ts.map +1 -0
  124. package/dist/tools/default-tool-module-loaders.js +17 -0
  125. package/dist/tools/default-tool-module-loaders.js.map +1 -0
  126. package/dist/tools/index.d.ts +2 -1
  127. package/dist/tools/index.d.ts.map +1 -1
  128. package/dist/tools/index.js +2 -1
  129. package/dist/tools/index.js.map +1 -1
  130. package/dist/tools/library-loader.d.ts +1 -1
  131. package/dist/tools/library-loader.d.ts.map +1 -1
  132. package/dist/tools/library-loader.js +7 -2
  133. package/dist/tools/library-loader.js.map +1 -1
  134. package/dist/tools/registrations/accessibility-tools.d.ts +1 -9
  135. package/dist/tools/registrations/accessibility-tools.d.ts.map +1 -1
  136. package/dist/tools/registrations/accessibility-tools.js +95 -113
  137. package/dist/tools/registrations/accessibility-tools.js.map +1 -1
  138. package/dist/tools/registrations/calculator.d.ts +2 -2
  139. package/dist/tools/registrations/calculator.d.ts.map +1 -1
  140. package/dist/tools/registrations/calculator.js +34 -55
  141. package/dist/tools/registrations/calculator.js.map +1 -1
  142. package/dist/tools/registrations/interaction-tools.d.ts.map +1 -1
  143. package/dist/tools/registrations/interaction-tools.js +77 -52
  144. package/dist/tools/registrations/interaction-tools.js.map +1 -1
  145. package/dist/tools/registrations/measurement-tools.d.ts.map +1 -1
  146. package/dist/tools/registrations/measurement-tools.js +63 -38
  147. package/dist/tools/registrations/measurement-tools.js.map +1 -1
  148. package/dist/tools/registrations/subject-specific-tools.d.ts.map +1 -1
  149. package/dist/tools/registrations/subject-specific-tools.js +63 -38
  150. package/dist/tools/registrations/subject-specific-tools.js.map +1 -1
  151. package/dist/tools/registrations/tts.d.ts +1 -1
  152. package/dist/tools/registrations/tts.d.ts.map +1 -1
  153. package/dist/tools/registrations/tts.js +27 -41
  154. package/dist/tools/registrations/tts.js.map +1 -1
  155. package/dist/tools/response-discovery.d.ts +12 -14
  156. package/dist/tools/response-discovery.d.ts.map +1 -1
  157. package/dist/tools/response-discovery.js +23 -80
  158. package/dist/tools/response-discovery.js.map +1 -1
  159. package/dist/tools/tool-tag-map.d.ts +3 -3
  160. package/dist/tools/tool-tag-map.d.ts.map +1 -1
  161. package/dist/tools/tool-tag-map.js +2 -3
  162. package/dist/tools/tool-tag-map.js.map +1 -1
  163. package/dist/tools/types.d.ts +3 -1
  164. package/dist/tools/types.d.ts.map +1 -1
  165. package/package.json +25 -28
  166. package/src/README.md +16 -32
  167. package/src/components/ItemToolBar.svelte +489 -0
  168. package/src/components/PNPProvenanceViewer.svelte +14 -4
  169. package/src/components/PieAssessmentToolkit.svelte +517 -0
  170. package/src/components/ToolButton.svelte +11 -11
  171. package/src/components/ToolButtonGroup.svelte +27 -11
  172. package/src/tools/README.md +6 -5
  173. package/src/tools/calculators/README.md +4 -4
  174. package/dist/player/AssessmentPlayer.d.ts +0 -397
  175. package/dist/player/AssessmentPlayer.d.ts.map +0 -1
  176. package/dist/player/AssessmentPlayer.js +0 -974
  177. package/dist/player/AssessmentPlayer.js.map +0 -1
  178. package/dist/player/index.d.ts +0 -17
  179. package/dist/player/index.d.ts.map +0 -1
  180. package/dist/player/index.js +0 -15
  181. package/dist/player/index.js.map +0 -1
  182. package/dist/player/navigation-types.d.ts +0 -63
  183. package/dist/player/navigation-types.d.ts.map +0 -1
  184. package/dist/player/navigation-types.js +0 -7
  185. package/dist/player/navigation-types.js.map +0 -1
  186. package/dist/player/qti-navigation.d.ts +0 -29
  187. package/dist/player/qti-navigation.d.ts.map +0 -1
  188. package/dist/player/qti-navigation.js +0 -160
  189. package/dist/player/qti-navigation.js.map +0 -1
  190. package/dist/reference-layout/index.d.ts +0 -19
  191. package/dist/reference-layout/index.d.ts.map +0 -1
  192. package/dist/reference-layout/index.js +0 -20
  193. package/dist/reference-layout/index.js.map +0 -1
  194. package/dist/services/AnnotationToolbarConfig.d.ts +0 -134
  195. package/dist/services/AnnotationToolbarConfig.d.ts.map +0 -1
  196. package/dist/services/AnnotationToolbarConfig.js +0 -100
  197. package/dist/services/AnnotationToolbarConfig.js.map +0 -1
  198. package/dist/services/AssessmentAuthoringService.d.ts +0 -60
  199. package/dist/services/AssessmentAuthoringService.d.ts.map +0 -1
  200. package/dist/services/AssessmentAuthoringService.js +0 -183
  201. package/dist/services/AssessmentAuthoringService.js.map +0 -1
  202. package/dist/services/PNPMapper.d.ts +0 -81
  203. package/dist/services/PNPMapper.d.ts.map +0 -1
  204. package/dist/services/PNPMapper.js +0 -108
  205. package/dist/services/PNPMapper.js.map +0 -1
  206. package/src/components/PNPProfileTester.example.svelte +0 -188
  207. package/src/components/PNPProfileTester.svelte +0 -397
  208. package/src/components/QuestionToolBar.svelte +0 -483
  209. package/src/player/AssessmentLayout.svelte +0 -68
  210. package/src/player/README.md +0 -154
  211. package/src/reference-layout/README.md +0 -135
  212. package/src/reference-layout/ReferenceLayout.svelte +0 -201
  213. package/src/reference-layout/components/AssessmentContent.svelte +0 -259
  214. package/src/reference-layout/components/AssessmentFooter.svelte +0 -89
  215. package/src/reference-layout/components/AssessmentHeader.svelte +0 -250
  216. package/src/reference-layout/components/AssessmentNavigation.svelte +0 -134
  217. package/src/reference-layout/components/AssessmentToolsBar.svelte +0 -176
  218. package/src/reference-layout/components/ItemPanel.svelte +0 -200
  219. package/src/reference-layout/components/NotesPanel.svelte +0 -179
  220. package/src/reference-layout/components/PassagePanel.svelte +0 -76
@@ -0,0 +1,489 @@
1
+ <svelte:options
2
+ customElement={{
3
+ tag: 'pie-item-toolbar',
4
+ shadow: 'open',
5
+ props: {
6
+ itemId: { type: 'String', attribute: 'item-id' },
7
+ catalogId: { type: 'String', attribute: 'catalog-id' },
8
+ tools: { type: 'String', attribute: 'tools' },
9
+ contentKind: { type: 'String', attribute: 'content-kind' },
10
+ size: { type: 'String', attribute: 'size' },
11
+ language: { type: 'String', attribute: 'language' },
12
+
13
+ // Local runtime contracts (passed as JS properties, not attributes)
14
+ scopeElement: { type: 'Object', reflect: false },
15
+ toolRegistry: { type: 'Object', reflect: false },
16
+ pnpResolver: { type: 'Object', reflect: false },
17
+ assessment: { type: 'Object', reflect: false },
18
+ itemRef: { type: 'Object', reflect: false },
19
+ item: { type: 'Object', reflect: false }
20
+ }
21
+ }}
22
+ />
23
+
24
+ <!--
25
+ ItemToolBar - Inline toolbar for item/passage headers
26
+
27
+ Button visibility is always registry-driven:
28
+ - Pass 1: allowed tool IDs (PNP resolver when available, otherwise explicit tools prop)
29
+ - Pass 2: tool-owned isVisibleInContext(context)
30
+ -->
31
+ <script lang="ts">
32
+ import {
33
+ type AssessmentToolkitShellContext,
34
+ type AssessmentToolkitRuntimeContext
35
+ } from '../context/assessment-toolkit-context.js';
36
+ import {
37
+ connectAssessmentToolkitRuntimeContext,
38
+ connectAssessmentToolkitShellContext,
39
+ } from '../context/runtime-context-consumer.js';
40
+ import type { ToolRegistry, ToolToolbarRenderResult, ToolbarContext } from '../services/ToolRegistry.js';
41
+ import type { PNPToolResolver } from '../services/PNPToolResolver.js';
42
+ import { createDefaultToolRegistry } from '../services/createDefaultToolRegistry.js';
43
+ import { DEFAULT_TOOL_MODULE_LOADERS } from '../tools/default-tool-module-loaders.js';
44
+ import {
45
+ normalizeToolsConfig,
46
+ parseToolList,
47
+ resolveToolsForLevel,
48
+ } from '../services/tools-config-normalizer.js';
49
+ import { createScopedToolId, parseScopedToolId } from '../services/tool-instance-id.js';
50
+ import type { AssessmentEntity, AssessmentItemRef, ItemEntity } from '@pie-players/pie-players-shared/types';
51
+ import type { ElementToolContext, ItemToolContext } from '../services/tool-context.js';
52
+
53
+ const isBrowser = typeof window !== 'undefined';
54
+ const fallbackToolRegistry = createDefaultToolRegistry({
55
+ toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS
56
+ });
57
+
58
+ let activeToolState = $state<Record<string, boolean>>({});
59
+
60
+ // Props
61
+ let {
62
+ itemId = '',
63
+ catalogId = '',
64
+ tools = 'calculator,textToSpeech,answerEliminator',
65
+ contentKind = 'assessment-item',
66
+ scopeElement = null,
67
+ toolRegistry = null,
68
+ pnpResolver = null,
69
+ assessment = null,
70
+ itemRef = null,
71
+ item = null,
72
+ class: className = '',
73
+ size = 'md' as 'sm' | 'md' | 'lg',
74
+ language = 'en-US'
75
+ }: {
76
+ itemId?: string;
77
+ catalogId?: string;
78
+ tools?: string;
79
+ contentKind?: string;
80
+ scopeElement?: HTMLElement | null;
81
+ toolRegistry?: ToolRegistry | null;
82
+ pnpResolver?: PNPToolResolver | null;
83
+ assessment?: AssessmentEntity | null;
84
+ itemRef?: AssessmentItemRef | null;
85
+ item?: ItemEntity | null;
86
+ class?: string;
87
+ size?: 'sm' | 'md' | 'lg';
88
+ language?: string;
89
+ } = $props();
90
+
91
+ let toolbarRootElement = $state<HTMLDivElement | null>(null);
92
+ let runtimeContext = $state<AssessmentToolkitRuntimeContext | null>(null);
93
+ let shellContext = $state<AssessmentToolkitShellContext | null>(null);
94
+ let moduleLoadVersion = $state(0);
95
+
96
+ $effect(() => {
97
+ if (!toolbarRootElement) return;
98
+ return connectAssessmentToolkitRuntimeContext(toolbarRootElement, (value) => {
99
+ runtimeContext = value;
100
+ });
101
+ });
102
+
103
+ $effect(() => {
104
+ if (!toolbarRootElement) return;
105
+ return connectAssessmentToolkitShellContext(toolbarRootElement, (value) => {
106
+ shellContext = value;
107
+ });
108
+ });
109
+
110
+ const effectiveToolCoordinator = $derived(runtimeContext?.toolCoordinator);
111
+ const effectiveTTSService = $derived(runtimeContext?.ttsService);
112
+ const effectiveElementToolStateStore = $derived(runtimeContext?.elementToolStateStore);
113
+ const effectiveAssessmentId = $derived(runtimeContext?.assessmentId ?? '');
114
+ const effectiveSectionId = $derived(runtimeContext?.sectionId ?? '');
115
+ const effectiveItemId = $derived(itemId || shellContext?.itemId || '');
116
+ const effectiveCanonicalItemId = $derived(shellContext?.canonicalItemId || effectiveItemId);
117
+ const effectiveCatalogId = $derived(catalogId || effectiveItemId);
118
+ const effectiveContentKind = $derived(contentKind || shellContext?.contentKind || 'assessment-item');
119
+ const effectiveItem = $derived((item as ItemEntity | null) || (shellContext?.item as ItemEntity | null) || null);
120
+
121
+ // Effective registry for visibility and metadata ownership.
122
+ const effectiveToolRegistry = $derived(toolRegistry || fallbackToolRegistry);
123
+
124
+ const explicitTools = $derived(parseToolList(tools));
125
+ const normalizedExplicitTools = $derived(
126
+ effectiveToolRegistry.normalizeToolIds(explicitTools).filter(Boolean)
127
+ );
128
+ const effectiveToolsConfig = $derived.by(() => {
129
+ const coordinatorConfig = runtimeContext?.toolkitCoordinator?.config?.tools as any;
130
+ return normalizeToolsConfig(coordinatorConfig || {});
131
+ });
132
+ const placementLevel = $derived.by(() =>
133
+ effectiveContentKind === 'rubric-block-stimulus' ? 'passage' : 'item'
134
+ );
135
+ const placementAllowedToolIds = $derived.by(() => {
136
+ const resolved = resolveToolsForLevel(effectiveToolsConfig, placementLevel);
137
+ return effectiveToolRegistry.normalizeToolIds(resolved).filter(Boolean);
138
+ });
139
+
140
+ // Pass 1: determine allowed tools
141
+ const allowedToolIds = $derived.by(() => {
142
+ const configuredTools =
143
+ normalizedExplicitTools.length > 0 ? normalizedExplicitTools : placementAllowedToolIds;
144
+ if (pnpResolver && assessment && itemRef) {
145
+ const allowedByPnp = pnpResolver.getAllowedToolIds(assessment, itemRef);
146
+ if (configuredTools.length === 0) return allowedByPnp;
147
+ return allowedByPnp.filter((toolId) => configuredTools.includes(toolId));
148
+ }
149
+ return configuredTools;
150
+ });
151
+
152
+ const contentReady = $derived.by(() => {
153
+ const config = (effectiveItem as ItemEntity | null)?.config;
154
+ return !!(effectiveItem && config && typeof config === 'object');
155
+ });
156
+
157
+ const toolContext = $derived.by((): ItemToolContext | null => {
158
+ if (!contentReady || !effectiveItem) {
159
+ return null;
160
+ }
161
+ return {
162
+ level: 'item',
163
+ assessment: (assessment || {}) as AssessmentEntity,
164
+ itemRef: (itemRef || ({ id: effectiveCanonicalItemId } as AssessmentItemRef)) as AssessmentItemRef,
165
+ item: effectiveItem as ItemEntity
166
+ };
167
+ });
168
+
169
+ const renderContext = $derived.by((): ItemToolContext => {
170
+ if (toolContext) return toolContext;
171
+ return {
172
+ level: 'item',
173
+ assessment: (assessment || {}) as AssessmentEntity,
174
+ itemRef: (itemRef || ({ id: effectiveCanonicalItemId } as AssessmentItemRef)) as AssessmentItemRef,
175
+ item: ((effectiveItem as ItemEntity | null) || ({ id: effectiveCanonicalItemId, config: {} } as ItemEntity)) as ItemEntity
176
+ };
177
+ });
178
+
179
+ const elementContexts = $derived.by((): ElementToolContext[] => {
180
+ if (!contentReady || !effectiveItem) return [];
181
+ const modelsRaw = (effectiveItem as any)?.config?.models;
182
+ const models = Array.isArray(modelsRaw)
183
+ ? modelsRaw
184
+ : modelsRaw && typeof modelsRaw === 'object'
185
+ ? Object.values(modelsRaw)
186
+ : [];
187
+ return models
188
+ .filter((model: any) => model && typeof model === 'object' && typeof model.id === 'string')
189
+ .map((model: any) => ({
190
+ level: 'element' as const,
191
+ assessment: (assessment || {}) as AssessmentEntity,
192
+ itemRef: (itemRef || ({ id: effectiveCanonicalItemId } as AssessmentItemRef)) as AssessmentItemRef,
193
+ item: effectiveItem as ItemEntity,
194
+ elementId: model.id as string
195
+ }));
196
+ });
197
+
198
+ // Pass 2: tool-owned context filtering (item + element aggregation)
199
+ const visibleToolIds = $derived.by(() => {
200
+ if (!contentReady) {
201
+ // Stage 1: orchestrator-level allow-list only.
202
+ return allowedToolIds;
203
+ }
204
+ if (!toolContext && elementContexts.length === 0) {
205
+ return allowedToolIds;
206
+ }
207
+ const visible = new Set<string>();
208
+ if (toolContext) {
209
+ effectiveToolRegistry.filterVisibleInContext(allowedToolIds, toolContext).forEach((tool) => visible.add(tool.toolId));
210
+ }
211
+
212
+ for (const context of elementContexts) {
213
+ effectiveToolRegistry.filterVisibleInContext(allowedToolIds, context).forEach((tool) => visible.add(tool.toolId));
214
+ }
215
+
216
+ return Array.from(visible);
217
+ });
218
+
219
+ // Dynamically load whatever tools are currently visible.
220
+ // The registry owns module loader configuration by toolId.
221
+ $effect(() => {
222
+ if (!isBrowser) return;
223
+ let cancelled = false;
224
+ void effectiveToolRegistry
225
+ .ensureToolModulesLoaded(visibleToolIds)
226
+ .then(() => {
227
+ if (!cancelled) moduleLoadVersion += 1;
228
+ })
229
+ .catch((error: unknown) => {
230
+ console.error('[ItemToolBar] Failed to load one or more tool modules:', error);
231
+ });
232
+ return () => {
233
+ cancelled = true;
234
+ };
235
+ });
236
+
237
+ function resolveScopeElement(): HTMLElement | null {
238
+ if (scopeElement) return scopeElement;
239
+ return shellContext?.scopeElement || null;
240
+ }
241
+
242
+ function resolveGlobalElementId(): string | null {
243
+ if (!effectiveElementToolStateStore || !effectiveAssessmentId || !effectiveSectionId || !effectiveCanonicalItemId)
244
+ return null;
245
+ return effectiveElementToolStateStore.getGlobalElementId(
246
+ effectiveAssessmentId,
247
+ effectiveSectionId,
248
+ effectiveCanonicalItemId,
249
+ effectiveCanonicalItemId
250
+ );
251
+ }
252
+
253
+ const toolbarContext = $derived.by((): ToolbarContext => {
254
+ const toolkitCoordinator = runtimeContext?.toolkitCoordinator || null;
255
+ const toInstanceToolId = (toolId: string): string => {
256
+ // Accept either base tool IDs (e.g. "calculator") or already-scoped IDs.
257
+ // This keeps toolbar contracts stable across mixed registration implementations.
258
+ return parseScopedToolId(toolId)
259
+ ? toolId
260
+ : createScopedToolId(toolId, 'item', effectiveCanonicalItemId);
261
+ };
262
+ return {
263
+ itemId: effectiveCanonicalItemId,
264
+ catalogId: effectiveCatalogId,
265
+ language,
266
+ ui: {
267
+ size
268
+ },
269
+ getScopeElement: resolveScopeElement,
270
+ getGlobalElementId: resolveGlobalElementId,
271
+ toolCoordinator: effectiveToolCoordinator || null,
272
+ toolkitCoordinator,
273
+ ttsService: effectiveTTSService || null,
274
+ elementToolStateStore: effectiveElementToolStateStore || null,
275
+ toggleTool: (toolId: string) => {
276
+ if (!effectiveToolCoordinator) return;
277
+ effectiveToolCoordinator.toggleTool(toInstanceToolId(toolId));
278
+ },
279
+ isToolVisible: (toolId: string) => {
280
+ if (!effectiveToolCoordinator) return false;
281
+ return effectiveToolCoordinator.isToolVisible(toInstanceToolId(toolId));
282
+ },
283
+ subscribeVisibility: effectiveToolCoordinator
284
+ ? (listener: () => void) => effectiveToolCoordinator.subscribe(listener)
285
+ : null,
286
+ ensureTTSReady: toolkitCoordinator ? () => toolkitCoordinator.ensureTTSReady() : null
287
+ };
288
+ });
289
+
290
+ let renderedTools = $derived.by((): ToolToolbarRenderResult[] => {
291
+ if (!isBrowser) return [];
292
+ moduleLoadVersion;
293
+
294
+ const rendered: ToolToolbarRenderResult[] = [];
295
+ for (const toolId of visibleToolIds) {
296
+ const result = effectiveToolRegistry.renderForToolbar(toolId, renderContext, toolbarContext);
297
+ if (result) {
298
+ rendered.push(result);
299
+ }
300
+ }
301
+ return rendered;
302
+ });
303
+
304
+ function syncRenderedToolsState() {
305
+ const nextActiveState: Record<string, boolean> = {};
306
+ for (const renderedTool of renderedTools) {
307
+ renderedTool.sync?.();
308
+ if (renderedTool.button) {
309
+ nextActiveState[renderedTool.toolId] = renderedTool.button.active ?? false;
310
+ }
311
+ }
312
+ activeToolState = nextActiveState;
313
+ }
314
+
315
+ $effect(() => {
316
+ const unsubs: Array<() => void> = [];
317
+ syncRenderedToolsState();
318
+ for (const renderedTool of renderedTools) {
319
+ if (renderedTool.subscribeActive) {
320
+ const unsubscribe = renderedTool.subscribeActive((active) => {
321
+ activeToolState = {
322
+ ...activeToolState,
323
+ [renderedTool.toolId]: active
324
+ };
325
+ renderedTool.sync?.();
326
+ });
327
+ unsubs.push(unsubscribe);
328
+ }
329
+ }
330
+
331
+ return () => {
332
+ unsubs.forEach((unsub) => unsub());
333
+ };
334
+ });
335
+
336
+ $effect(() => {
337
+ if (!toolbarContext.subscribeVisibility) return;
338
+ return toolbarContext.subscribeVisibility(() => {
339
+ syncRenderedToolsState();
340
+ });
341
+ });
342
+
343
+ function mountElement(node: HTMLSpanElement, element: HTMLElement | null) {
344
+ let mountedElement: HTMLElement | null = null;
345
+ const updateMountedElement = (nextElement: HTMLElement | null) => {
346
+ if (mountedElement === nextElement) return;
347
+ if (mountedElement && mountedElement.parentNode === node) {
348
+ node.removeChild(mountedElement);
349
+ }
350
+ mountedElement = nextElement;
351
+ if (mountedElement) {
352
+ if (mountedElement.parentNode && mountedElement.parentNode !== node) {
353
+ mountedElement.parentNode.removeChild(mountedElement);
354
+ }
355
+ node.appendChild(mountedElement);
356
+ }
357
+ };
358
+ updateMountedElement(element);
359
+ return {
360
+ update(nextElement: HTMLElement | null) {
361
+ updateMountedElement(nextElement);
362
+ },
363
+ destroy() {
364
+ updateMountedElement(null);
365
+ }
366
+ };
367
+ }
368
+ </script>
369
+
370
+ {#if isBrowser}
371
+ <div
372
+ class="item-toolbar {className} item-toolbar--{size}"
373
+ data-content-kind={effectiveContentKind}
374
+ bind:this={toolbarRootElement}
375
+ >
376
+ {#each renderedTools as renderedTool (renderedTool.toolId)}
377
+ {#if renderedTool.inlineElement}
378
+ <span class="item-toolbar__element-host" use:mountElement={renderedTool.inlineElement}></span>
379
+ {/if}
380
+
381
+ {#if renderedTool.button}
382
+ <button
383
+ type="button"
384
+ class="item-toolbar__button {renderedTool.button.className || ''}"
385
+ class:item-toolbar__button--active={activeToolState[renderedTool.toolId] ?? renderedTool.button.active ?? false}
386
+ onclick={() => {
387
+ renderedTool.button?.onClick();
388
+ syncRenderedToolsState();
389
+ }}
390
+ aria-label={renderedTool.button.ariaLabel || renderedTool.button.label}
391
+ aria-pressed={activeToolState[renderedTool.toolId] ?? renderedTool.button.active ?? false}
392
+ title={renderedTool.button.tooltip || renderedTool.button.label}
393
+ disabled={renderedTool.button.disabled}
394
+ >
395
+ {#if renderedTool.button.icon.startsWith('<svg')}
396
+ {@html renderedTool.button.icon}
397
+ {:else if renderedTool.button.icon.startsWith('http')}
398
+ <img class="item-toolbar__icon-image" src={renderedTool.button.icon} alt="" />
399
+ {:else}
400
+ <i class={`icon icon-${renderedTool.button.icon}`} aria-hidden="true"></i>
401
+ {/if}
402
+ </button>
403
+ {/if}
404
+
405
+ {#if renderedTool.overlayElement}
406
+ <span class="item-toolbar__element-host" use:mountElement={renderedTool.overlayElement}></span>
407
+ {/if}
408
+ {/each}
409
+ </div>
410
+ {/if}
411
+
412
+ <style>
413
+ .item-toolbar {
414
+ display: flex;
415
+ align-items: center;
416
+ gap: 0.5rem;
417
+ }
418
+
419
+ .item-toolbar__button {
420
+ display: flex;
421
+ align-items: center;
422
+ justify-content: center;
423
+ width: 2rem;
424
+ height: 2rem;
425
+ padding: 0.25rem;
426
+ border: 1px solid var(--pie-border, #ccc);
427
+ border-radius: 0.25rem;
428
+ background-color: var(--pie-background, white);
429
+ color: var(--pie-text, #333);
430
+ cursor: pointer;
431
+ transition: all 0.15s ease;
432
+ }
433
+
434
+ .item-toolbar__element-host {
435
+ display: contents;
436
+ }
437
+
438
+ .item-toolbar--sm .item-toolbar__button {
439
+ width: 2.75rem;
440
+ height: 2.75rem;
441
+ }
442
+
443
+ .item-toolbar--lg .item-toolbar__button {
444
+ width: 2.5rem;
445
+ height: 2.5rem;
446
+ }
447
+
448
+ .item-toolbar__button:hover:not(:disabled) {
449
+ background-color: var(--pie-secondary-background, #f5f5f5);
450
+ transform: translateY(-1px);
451
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
452
+ }
453
+
454
+ .item-toolbar__button:active:not(:disabled) {
455
+ transform: translateY(0);
456
+ box-shadow: none;
457
+ }
458
+
459
+ .item-toolbar__button--active {
460
+ background-color: var(--pie-primary, #1976d2);
461
+ color: white;
462
+ border-color: var(--pie-primary, #1976d2);
463
+ }
464
+
465
+ .item-toolbar__button--active:hover:not(:disabled) {
466
+ background-color: var(--pie-primary-dark, #1565c0);
467
+ }
468
+
469
+ .item-toolbar__button:focus-visible {
470
+ outline: 2px solid var(--pie-primary, #1976d2);
471
+ outline-offset: 2px;
472
+ }
473
+
474
+ .item-toolbar__button:disabled {
475
+ opacity: 0.5;
476
+ cursor: not-allowed;
477
+ }
478
+
479
+ .item-toolbar__button :global(svg) {
480
+ width: 100%;
481
+ height: 100%;
482
+ }
483
+
484
+ .item-toolbar__icon-image {
485
+ width: 100%;
486
+ height: 100%;
487
+ object-fit: contain;
488
+ }
489
+ </style>
@@ -1,9 +1,8 @@
1
1
  <script lang="ts">
2
- import type { PNPResolutionProvenance } from '@pie-players/pie-assessment-toolkit/services/pnp-provenance';
2
+ import type { PNPResolutionProvenance } from '../services/pnp-provenance.js';
3
3
 
4
4
  // PNP Provenance Viewer Component
5
5
  // Visualizes how PNP resolution decisions were made.
6
- // See PNP_TESTING.md for usage documentation.
7
6
 
8
7
  interface Props {
9
8
  provenance: PNPResolutionProvenance;
@@ -37,6 +36,17 @@
37
36
  };
38
37
  return icons[action] || '?';
39
38
  }
39
+
40
+ function getSourceName(source: unknown): string | undefined {
41
+ if (!source || typeof source !== 'object') return undefined;
42
+ if ('name' in source && typeof (source as any).name === 'string') {
43
+ return (source as any).name;
44
+ }
45
+ if ('title' in source && typeof (source as any).title === 'string') {
46
+ return (source as any).title;
47
+ }
48
+ return undefined;
49
+ }
40
50
  </script>
41
51
 
42
52
  <div class="pnp-provenance-viewer" class:compact>
@@ -75,7 +85,7 @@
75
85
  <div class="flex items-center gap-2">
76
86
  <div class="badge badge-outline">{type}</div>
77
87
  <div class="text-sm">
78
- {source.name || source.id}
88
+ {getSourceName(source) || source.id}
79
89
  <span class="text-xs text-base-content/50">({source.id})</span>
80
90
  </div>
81
91
  </div>
@@ -226,7 +236,7 @@
226
236
  {decision.reason}
227
237
  </div>
228
238
  <div class="text-base-content/50 mt-1">
229
- Source: {decision.source.name || decision.source.type}
239
+ Source: {getSourceName(decision.source) || decision.source.type}
230
240
  </div>
231
241
  </div>
232
242
  </div>