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

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 (249) 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 +953 -0
  11. package/dist/components/PieAssessmentToolkit.custom-element.js +757 -0
  12. package/dist/components/SectionToolBar.custom-element.js +162 -0
  13. package/dist/components/item-toolbar-element.d.ts +3 -0
  14. package/dist/components/item-toolbar-element.d.ts.map +1 -0
  15. package/dist/components/item-toolbar-element.js +2 -0
  16. package/dist/components/item-toolbar-element.js.map +1 -0
  17. package/dist/components/pie-assessment-toolkit-element.d.ts +3 -0
  18. package/dist/components/pie-assessment-toolkit-element.d.ts.map +1 -0
  19. package/dist/components/pie-assessment-toolkit-element.js +2 -0
  20. package/dist/components/pie-assessment-toolkit-element.js.map +1 -0
  21. package/dist/components/section-toolbar-element.d.ts +3 -0
  22. package/dist/components/section-toolbar-element.d.ts.map +1 -0
  23. package/dist/components/section-toolbar-element.js +2 -0
  24. package/dist/components/section-toolbar-element.js.map +1 -0
  25. package/dist/context/assessment-toolkit-context.d.ts +52 -0
  26. package/dist/context/assessment-toolkit-context.d.ts.map +1 -0
  27. package/dist/context/assessment-toolkit-context.js +6 -0
  28. package/dist/context/assessment-toolkit-context.js.map +1 -0
  29. package/dist/context/runtime-context-consumer.d.ts +14 -0
  30. package/dist/context/runtime-context-consumer.d.ts.map +1 -0
  31. package/dist/context/runtime-context-consumer.js +52 -0
  32. package/dist/context/runtime-context-consumer.js.map +1 -0
  33. package/dist/index.d.ts +19 -6
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +14 -11
  36. package/dist/index.js.map +1 -1
  37. package/dist/runtime/RuntimeRegistry.d.ts +13 -0
  38. package/dist/runtime/RuntimeRegistry.d.ts.map +1 -0
  39. package/dist/runtime/RuntimeRegistry.js +50 -0
  40. package/dist/runtime/RuntimeRegistry.js.map +1 -0
  41. package/dist/runtime/SectionRuntimeEngine.d.ts +45 -0
  42. package/dist/runtime/SectionRuntimeEngine.d.ts.map +1 -0
  43. package/dist/runtime/SectionRuntimeEngine.js +81 -0
  44. package/dist/runtime/SectionRuntimeEngine.js.map +1 -0
  45. package/dist/runtime/registration-events.d.ts +19 -0
  46. package/dist/runtime/registration-events.d.ts.map +1 -0
  47. package/dist/runtime/registration-events.js +4 -0
  48. package/dist/runtime/registration-events.js.map +1 -0
  49. package/dist/runtime/runtime-event-guards.d.ts +3 -0
  50. package/dist/runtime/runtime-event-guards.d.ts.map +1 -0
  51. package/dist/runtime/runtime-event-guards.js +9 -0
  52. package/dist/runtime/runtime-event-guards.js.map +1 -0
  53. package/dist/runtime/tool-host-contract.d.ts +30 -0
  54. package/dist/runtime/tool-host-contract.d.ts.map +1 -0
  55. package/dist/runtime/tool-host-contract.js +48 -0
  56. package/dist/runtime/tool-host-contract.js.map +1 -0
  57. package/dist/services/HighlightCoordinator.d.ts +9 -0
  58. package/dist/services/HighlightCoordinator.d.ts.map +1 -1
  59. package/dist/services/HighlightCoordinator.js +196 -55
  60. package/dist/services/HighlightCoordinator.js.map +1 -1
  61. package/dist/services/TTSService.d.ts +21 -1
  62. package/dist/services/TTSService.d.ts.map +1 -1
  63. package/dist/services/TTSService.js +356 -117
  64. package/dist/services/TTSService.js.map +1 -1
  65. package/dist/services/ThemeProvider.d.ts +1 -0
  66. package/dist/services/ThemeProvider.d.ts.map +1 -1
  67. package/dist/services/ThemeProvider.js +82 -46
  68. package/dist/services/ThemeProvider.js.map +1 -1
  69. package/dist/services/ToolConfigResolver.d.ts +1 -1
  70. package/dist/services/ToolConfigResolver.js +1 -1
  71. package/dist/services/ToolCoordinator.d.ts +1 -1
  72. package/dist/services/ToolCoordinator.d.ts.map +1 -1
  73. package/dist/services/ToolRegistry.d.ts +76 -67
  74. package/dist/services/ToolRegistry.d.ts.map +1 -1
  75. package/dist/services/ToolRegistry.js +73 -22
  76. package/dist/services/ToolRegistry.js.map +1 -1
  77. package/dist/services/ToolkitCoordinator.d.ts +126 -8
  78. package/dist/services/ToolkitCoordinator.d.ts.map +1 -1
  79. package/dist/services/ToolkitCoordinator.js +607 -72
  80. package/dist/services/ToolkitCoordinator.js.map +1 -1
  81. package/dist/services/createDefaultToolRegistry.d.ts +11 -12
  82. package/dist/services/createDefaultToolRegistry.d.ts.map +1 -1
  83. package/dist/services/createDefaultToolRegistry.js +9 -56
  84. package/dist/services/createDefaultToolRegistry.js.map +1 -1
  85. package/dist/services/defaultPersonalNeedsProfile.d.ts +4 -0
  86. package/dist/services/defaultPersonalNeedsProfile.d.ts.map +1 -0
  87. package/dist/services/defaultPersonalNeedsProfile.js +25 -0
  88. package/dist/services/defaultPersonalNeedsProfile.js.map +1 -0
  89. package/dist/services/interfaces.d.ts +62 -2
  90. package/dist/services/interfaces.d.ts.map +1 -1
  91. package/dist/services/interfaces.js.map +1 -1
  92. package/dist/services/pnp-standard-features.d.ts +3 -4
  93. package/dist/services/pnp-standard-features.d.ts.map +1 -1
  94. package/dist/services/pnp-standard-features.js +0 -3
  95. package/dist/services/pnp-standard-features.js.map +1 -1
  96. package/dist/services/section-controller-types.d.ts +37 -0
  97. package/dist/services/section-controller-types.d.ts.map +1 -0
  98. package/dist/services/section-controller-types.js +2 -0
  99. package/dist/services/section-controller-types.js.map +1 -0
  100. package/dist/services/tool-config-defaults.d.ts +14 -0
  101. package/dist/services/tool-config-defaults.d.ts.map +1 -0
  102. package/dist/services/tool-config-defaults.js +23 -0
  103. package/dist/services/tool-config-defaults.js.map +1 -0
  104. package/dist/services/tool-context.d.ts +4 -4
  105. package/dist/services/tool-context.d.ts.map +1 -1
  106. package/dist/services/tool-context.js +156 -20
  107. package/dist/services/tool-context.js.map +1 -1
  108. package/dist/services/tool-instance-id.d.ts +15 -0
  109. package/dist/services/tool-instance-id.d.ts.map +1 -0
  110. package/dist/services/tool-instance-id.js +55 -0
  111. package/dist/services/tool-instance-id.js.map +1 -0
  112. package/dist/services/tool-providers/IToolProvider.d.ts +2 -3
  113. package/dist/services/tool-providers/IToolProvider.d.ts.map +1 -1
  114. package/dist/services/tool-providers/IToolProvider.js +1 -2
  115. package/dist/services/tool-providers/IToolProvider.js.map +1 -1
  116. package/dist/services/tool-providers/MathJsToolProvider.d.ts +25 -0
  117. package/dist/services/tool-providers/MathJsToolProvider.d.ts.map +1 -0
  118. package/dist/services/tool-providers/MathJsToolProvider.js +53 -0
  119. package/dist/services/tool-providers/MathJsToolProvider.js.map +1 -0
  120. package/dist/services/tool-providers/index.d.ts +2 -0
  121. package/dist/services/tool-providers/index.d.ts.map +1 -1
  122. package/dist/services/tool-providers/index.js +1 -0
  123. package/dist/services/tool-providers/index.js.map +1 -1
  124. package/dist/services/toolbar-items.d.ts +26 -0
  125. package/dist/services/toolbar-items.d.ts.map +1 -0
  126. package/dist/services/toolbar-items.js +10 -0
  127. package/dist/services/toolbar-items.js.map +1 -0
  128. package/dist/services/tools-config-normalizer.d.ts +26 -0
  129. package/dist/services/tools-config-normalizer.d.ts.map +1 -0
  130. package/dist/services/tools-config-normalizer.js +62 -0
  131. package/dist/services/tools-config-normalizer.js.map +1 -0
  132. package/dist/services/tts/browser-provider.d.ts.map +1 -1
  133. package/dist/services/tts/browser-provider.js +201 -34
  134. package/dist/services/tts/browser-provider.js.map +1 -1
  135. package/dist/services/tts/text-processing.d.ts +17 -0
  136. package/dist/services/tts/text-processing.d.ts.map +1 -0
  137. package/dist/services/tts/text-processing.js +144 -0
  138. package/dist/services/tts/text-processing.js.map +1 -0
  139. package/dist/tools/calculators/ti-provider.d.ts.map +1 -1
  140. package/dist/tools/calculators/ti-provider.js +127 -4
  141. package/dist/tools/calculators/ti-provider.js.map +1 -1
  142. package/dist/tools/client.d.ts +2 -1
  143. package/dist/tools/client.d.ts.map +1 -1
  144. package/dist/tools/client.js +2 -1
  145. package/dist/tools/client.js.map +1 -1
  146. package/dist/tools/default-tool-module-loaders.d.ts +9 -0
  147. package/dist/tools/default-tool-module-loaders.d.ts.map +1 -0
  148. package/dist/tools/default-tool-module-loaders.js +11 -0
  149. package/dist/tools/default-tool-module-loaders.js.map +1 -0
  150. package/dist/tools/index.d.ts +2 -1
  151. package/dist/tools/index.d.ts.map +1 -1
  152. package/dist/tools/index.js +2 -1
  153. package/dist/tools/index.js.map +1 -1
  154. package/dist/tools/library-loader.d.ts +1 -1
  155. package/dist/tools/library-loader.d.ts.map +1 -1
  156. package/dist/tools/library-loader.js +7 -2
  157. package/dist/tools/library-loader.js.map +1 -1
  158. package/dist/tools/registrations/accessibility-tools.d.ts +8 -11
  159. package/dist/tools/registrations/accessibility-tools.d.ts.map +1 -1
  160. package/dist/tools/registrations/accessibility-tools.js +108 -120
  161. package/dist/tools/registrations/accessibility-tools.js.map +1 -1
  162. package/dist/tools/registrations/calculator.d.ts +2 -2
  163. package/dist/tools/registrations/calculator.d.ts.map +1 -1
  164. package/dist/tools/registrations/calculator.js +42 -55
  165. package/dist/tools/registrations/calculator.js.map +1 -1
  166. package/dist/tools/registrations/interaction-tools.d.ts.map +1 -1
  167. package/dist/tools/registrations/interaction-tools.js +77 -52
  168. package/dist/tools/registrations/interaction-tools.js.map +1 -1
  169. package/dist/tools/registrations/measurement-tools.d.ts.map +1 -1
  170. package/dist/tools/registrations/measurement-tools.js +63 -38
  171. package/dist/tools/registrations/measurement-tools.js.map +1 -1
  172. package/dist/tools/registrations/subject-specific-tools.d.ts.map +1 -1
  173. package/dist/tools/registrations/subject-specific-tools.js +63 -38
  174. package/dist/tools/registrations/subject-specific-tools.js.map +1 -1
  175. package/dist/tools/registrations/tts.d.ts +1 -1
  176. package/dist/tools/registrations/tts.d.ts.map +1 -1
  177. package/dist/tools/registrations/tts.js +171 -41
  178. package/dist/tools/registrations/tts.js.map +1 -1
  179. package/dist/tools/response-discovery.d.ts +12 -14
  180. package/dist/tools/response-discovery.d.ts.map +1 -1
  181. package/dist/tools/response-discovery.js +23 -80
  182. package/dist/tools/response-discovery.js.map +1 -1
  183. package/dist/tools/tool-tag-map.d.ts +3 -3
  184. package/dist/tools/tool-tag-map.d.ts.map +1 -1
  185. package/dist/tools/tool-tag-map.js +4 -4
  186. package/dist/tools/tool-tag-map.js.map +1 -1
  187. package/dist/tools/types.d.ts +3 -1
  188. package/dist/tools/types.d.ts.map +1 -1
  189. package/package.json +28 -27
  190. package/src/README.md +16 -32
  191. package/src/components/ItemToolBar.svelte +701 -0
  192. package/src/components/PieAssessmentToolkit.svelte +610 -0
  193. package/src/components/SectionToolBar.svelte +58 -0
  194. package/src/components/ToolButton.svelte +11 -11
  195. package/src/components/ToolButtonGroup.svelte +27 -11
  196. package/src/tools/README.md +6 -5
  197. package/src/tools/calculators/README.md +4 -4
  198. package/dist/player/AssessmentPlayer.d.ts +0 -397
  199. package/dist/player/AssessmentPlayer.d.ts.map +0 -1
  200. package/dist/player/AssessmentPlayer.js +0 -974
  201. package/dist/player/AssessmentPlayer.js.map +0 -1
  202. package/dist/player/index.d.ts +0 -17
  203. package/dist/player/index.d.ts.map +0 -1
  204. package/dist/player/index.js +0 -15
  205. package/dist/player/index.js.map +0 -1
  206. package/dist/player/navigation-types.d.ts +0 -63
  207. package/dist/player/navigation-types.d.ts.map +0 -1
  208. package/dist/player/navigation-types.js +0 -7
  209. package/dist/player/navigation-types.js.map +0 -1
  210. package/dist/player/qti-navigation.d.ts +0 -29
  211. package/dist/player/qti-navigation.d.ts.map +0 -1
  212. package/dist/player/qti-navigation.js +0 -160
  213. package/dist/player/qti-navigation.js.map +0 -1
  214. package/dist/reference-layout/index.d.ts +0 -19
  215. package/dist/reference-layout/index.d.ts.map +0 -1
  216. package/dist/reference-layout/index.js +0 -20
  217. package/dist/reference-layout/index.js.map +0 -1
  218. package/dist/services/AnnotationToolbarConfig.d.ts +0 -134
  219. package/dist/services/AnnotationToolbarConfig.d.ts.map +0 -1
  220. package/dist/services/AnnotationToolbarConfig.js +0 -100
  221. package/dist/services/AnnotationToolbarConfig.js.map +0 -1
  222. package/dist/services/AssessmentAuthoringService.d.ts +0 -60
  223. package/dist/services/AssessmentAuthoringService.d.ts.map +0 -1
  224. package/dist/services/AssessmentAuthoringService.js +0 -183
  225. package/dist/services/AssessmentAuthoringService.js.map +0 -1
  226. package/dist/services/PNPMapper.d.ts +0 -81
  227. package/dist/services/PNPMapper.d.ts.map +0 -1
  228. package/dist/services/PNPMapper.js +0 -108
  229. package/dist/services/PNPMapper.js.map +0 -1
  230. package/dist/services/tts/provider-interface.d.ts +0 -131
  231. package/dist/services/tts/provider-interface.d.ts.map +0 -1
  232. package/dist/services/tts/provider-interface.js +0 -10
  233. package/dist/services/tts/provider-interface.js.map +0 -1
  234. package/src/components/PNPProfileTester.example.svelte +0 -188
  235. package/src/components/PNPProfileTester.svelte +0 -397
  236. package/src/components/PNPProvenanceViewer.svelte +0 -260
  237. package/src/components/QuestionToolBar.svelte +0 -483
  238. package/src/player/AssessmentLayout.svelte +0 -68
  239. package/src/player/README.md +0 -154
  240. package/src/reference-layout/README.md +0 -135
  241. package/src/reference-layout/ReferenceLayout.svelte +0 -201
  242. package/src/reference-layout/components/AssessmentContent.svelte +0 -259
  243. package/src/reference-layout/components/AssessmentFooter.svelte +0 -89
  244. package/src/reference-layout/components/AssessmentHeader.svelte +0 -250
  245. package/src/reference-layout/components/AssessmentNavigation.svelte +0 -134
  246. package/src/reference-layout/components/AssessmentToolsBar.svelte +0 -176
  247. package/src/reference-layout/components/ItemPanel.svelte +0 -200
  248. package/src/reference-layout/components/NotesPanel.svelte +0 -179
  249. package/src/reference-layout/components/PassagePanel.svelte +0 -76
@@ -0,0 +1,701 @@
1
+ <svelte:options
2
+ customElement={{
3
+ tag: 'pie-item-toolbar',
4
+ shadow: 'open',
5
+ props: {
6
+ level: { type: 'String', attribute: 'level' },
7
+ scopeId: { type: 'String', attribute: 'scope-id' },
8
+ itemId: { type: 'String', attribute: 'item-id' },
9
+ sectionId: { type: 'String', attribute: 'section-id' },
10
+ catalogId: { type: 'String', attribute: 'catalog-id' },
11
+ tools: { type: 'String', attribute: 'tools' },
12
+ contentKind: { type: 'String', attribute: 'content-kind' },
13
+ position: { type: 'String', attribute: 'position' },
14
+ size: { type: 'String', attribute: 'size' },
15
+ language: { type: 'String', attribute: 'language' },
16
+
17
+ // Local runtime contracts (passed as JS properties, not attributes)
18
+ scopeElement: { type: 'Object', reflect: false },
19
+ toolRegistry: { type: 'Object', reflect: false },
20
+ pnpResolver: { type: 'Object', reflect: false },
21
+ assessment: { type: 'Object', reflect: false },
22
+ itemRef: { type: 'Object', reflect: false },
23
+ item: { type: 'Object', reflect: false },
24
+ hostButtons: { type: 'Object', reflect: false }
25
+ }
26
+ }}
27
+ />
28
+
29
+ <!--
30
+ ItemToolBar - Inline toolbar for item/passage headers
31
+
32
+ Button visibility is always registry-driven:
33
+ - Pass 1: allowed tool IDs (PNP resolver when available, otherwise explicit tools prop)
34
+ - Pass 2: tool-owned isVisibleInContext(context)
35
+ -->
36
+ <script lang="ts">
37
+ import {
38
+ type AssessmentToolkitShellContext,
39
+ type AssessmentToolkitRuntimeContext
40
+ } from '../context/assessment-toolkit-context.js';
41
+ import {
42
+ connectAssessmentToolkitRuntimeContext,
43
+ connectAssessmentToolkitShellContext,
44
+ } from '../context/runtime-context-consumer.js';
45
+ import type { ToolRegistry, ToolToolbarRenderResult, ToolbarContext } from '../services/ToolRegistry.js';
46
+ import {
47
+ isExternalIconUrl,
48
+ isInlineSvgIcon,
49
+ isToolbarLinkItem,
50
+ type ToolbarItem
51
+ } from '../services/toolbar-items.js';
52
+ import type { PNPToolResolver } from '../services/PNPToolResolver.js';
53
+ import { createDefaultToolRegistry } from '../services/createDefaultToolRegistry.js';
54
+ import { DEFAULT_TOOL_MODULE_LOADERS } from '../tools/default-tool-module-loaders.js';
55
+ import {
56
+ normalizeToolsConfig,
57
+ parseToolList,
58
+ resolveToolsForLevel,
59
+ } from '../services/tools-config-normalizer.js';
60
+ import { createScopedToolId, parseScopedToolId } from '../services/tool-instance-id.js';
61
+ import type { AssessmentEntity, AssessmentItemRef, ItemEntity } from '@pie-players/pie-players-shared/types';
62
+ import type { ElementToolContext, ItemToolContext, ToolLevel, ToolContext } from '../services/tool-context.js';
63
+
64
+ const isBrowser = typeof window !== 'undefined';
65
+ const fallbackToolRegistry = createDefaultToolRegistry({
66
+ toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS
67
+ });
68
+
69
+ let activeToolState = $state<Record<string, boolean>>({});
70
+
71
+ // Props
72
+ let {
73
+ level = 'item' as ToolLevel,
74
+ scopeId = '',
75
+ itemId = '',
76
+ sectionId = '',
77
+ catalogId = '',
78
+ tools = 'calculator,textToSpeech,answerEliminator',
79
+ contentKind = 'assessment-item',
80
+ position = 'bottom' as 'top' | 'right' | 'bottom' | 'left' | 'none',
81
+ scopeElement = null,
82
+ toolRegistry = null,
83
+ pnpResolver = null,
84
+ assessment = null,
85
+ itemRef = null,
86
+ item = null,
87
+ hostButtons = [] as ToolbarItem[],
88
+ class: className = '',
89
+ size = 'md' as 'sm' | 'md' | 'lg',
90
+ language = 'en-US'
91
+ }: {
92
+ level?: ToolLevel;
93
+ scopeId?: string;
94
+ itemId?: string;
95
+ sectionId?: string;
96
+ catalogId?: string;
97
+ tools?: string;
98
+ contentKind?: string;
99
+ position?: 'top' | 'right' | 'bottom' | 'left' | 'none';
100
+ scopeElement?: HTMLElement | null;
101
+ toolRegistry?: ToolRegistry | null;
102
+ pnpResolver?: PNPToolResolver | null;
103
+ assessment?: AssessmentEntity | null;
104
+ itemRef?: AssessmentItemRef | null;
105
+ item?: ItemEntity | null;
106
+ hostButtons?: ToolbarItem[];
107
+ class?: string;
108
+ size?: 'sm' | 'md' | 'lg';
109
+ language?: string;
110
+ } = $props();
111
+
112
+ let toolbarRootElement = $state<HTMLDivElement | null>(null);
113
+ let runtimeContext = $state<AssessmentToolkitRuntimeContext | null>(null);
114
+ let shellContext = $state<AssessmentToolkitShellContext | null>(null);
115
+ let moduleLoadVersion = $state(0);
116
+
117
+ $effect(() => {
118
+ if (!toolbarRootElement) return;
119
+ return connectAssessmentToolkitRuntimeContext(toolbarRootElement, (value) => {
120
+ runtimeContext = value;
121
+ });
122
+ });
123
+
124
+ $effect(() => {
125
+ if (!toolbarRootElement) return;
126
+ return connectAssessmentToolkitShellContext(toolbarRootElement, (value) => {
127
+ shellContext = value;
128
+ });
129
+ });
130
+
131
+ const effectiveToolCoordinator = $derived(runtimeContext?.toolCoordinator);
132
+ const effectiveTTSService = $derived(runtimeContext?.ttsService);
133
+ const effectiveElementToolStateStore = $derived(runtimeContext?.elementToolStateStore);
134
+ const effectiveAssessmentId = $derived(runtimeContext?.assessmentId ?? '');
135
+ const effectiveItemId = $derived(itemId || shellContext?.itemId || '');
136
+ const effectiveCanonicalItemId = $derived(shellContext?.canonicalItemId || effectiveItemId);
137
+ const effectiveSectionId = $derived(sectionId || runtimeContext?.sectionId || '');
138
+ const effectiveContentKind = $derived(contentKind || shellContext?.contentKind || (level === 'section' ? 'section' : 'assessment-item'));
139
+ const effectiveLevel = $derived.by((): ToolLevel => {
140
+ if (level && level !== 'item') return level;
141
+ if (effectiveContentKind === 'rubric-block-stimulus') return 'passage';
142
+ return 'item';
143
+ });
144
+ const effectiveScopeId = $derived.by(() => {
145
+ if (scopeId) return scopeId;
146
+ if (effectiveLevel === 'section') return effectiveSectionId || runtimeContext?.sectionId || 'default-section';
147
+ if (effectiveLevel === 'assessment') return runtimeContext?.assessmentId || 'default-assessment';
148
+ return effectiveCanonicalItemId || effectiveItemId || 'default-item';
149
+ });
150
+ const effectiveCatalogId = $derived(catalogId || effectiveScopeId);
151
+ const effectiveItem = $derived((item as ItemEntity | null) || (shellContext?.item as ItemEntity | null) || null);
152
+
153
+ // Effective registry for visibility and metadata ownership.
154
+ const effectiveToolRegistry = $derived(toolRegistry || fallbackToolRegistry);
155
+
156
+ const explicitTools = $derived(parseToolList(tools));
157
+ const normalizedExplicitTools = $derived(
158
+ effectiveToolRegistry.normalizeToolIds(explicitTools).filter(Boolean)
159
+ );
160
+ const effectiveToolsConfig = $derived.by(() => {
161
+ const coordinatorConfig = runtimeContext?.toolkitCoordinator?.config?.tools as any;
162
+ return normalizeToolsConfig(coordinatorConfig || {});
163
+ });
164
+ const placementLevel = $derived.by(() => {
165
+ if (effectiveLevel === 'section') return 'section';
166
+ if (effectiveLevel === 'passage' || effectiveContentKind === 'rubric-block-stimulus') return 'passage';
167
+ return 'item';
168
+ });
169
+ const placementAllowedToolIds = $derived.by(() => {
170
+ const resolved = resolveToolsForLevel(effectiveToolsConfig, placementLevel);
171
+ return effectiveToolRegistry.normalizeToolIds(resolved).filter(Boolean);
172
+ });
173
+
174
+ // Pass 1: determine allowed tools
175
+ const allowedToolIds = $derived.by(() => {
176
+ const configuredTools =
177
+ normalizedExplicitTools.length > 0 ? normalizedExplicitTools : placementAllowedToolIds;
178
+ if (pnpResolver && assessment && itemRef) {
179
+ const allowedByPnp = pnpResolver.getAllowedToolIds(assessment, itemRef);
180
+ if (configuredTools.length === 0) return allowedByPnp;
181
+ return allowedByPnp.filter((toolId) => configuredTools.includes(toolId));
182
+ }
183
+ return configuredTools;
184
+ });
185
+
186
+ const contentReady = $derived.by(() => {
187
+ if (effectiveLevel === 'section' || effectiveLevel === 'assessment') return true;
188
+ const config = (effectiveItem as ItemEntity | null)?.config;
189
+ return !!(effectiveItem && config && typeof config === 'object');
190
+ });
191
+
192
+ const toolContext = $derived.by((): ItemToolContext | null => {
193
+ if (effectiveLevel === 'section') {
194
+ return {
195
+ level: 'section',
196
+ assessment: (assessment || {}) as AssessmentEntity,
197
+ section: {} as any
198
+ } as ToolContext as ItemToolContext;
199
+ }
200
+ if (effectiveLevel === 'assessment') {
201
+ return {
202
+ level: 'assessment',
203
+ assessment: (assessment || {}) as AssessmentEntity
204
+ } as ToolContext as ItemToolContext;
205
+ }
206
+ if (!contentReady || !effectiveItem) {
207
+ return null;
208
+ }
209
+ if (effectiveLevel === 'passage') {
210
+ return {
211
+ level: 'passage',
212
+ assessment: (assessment || {}) as AssessmentEntity,
213
+ itemRef: (itemRef || ({ id: effectiveCanonicalItemId } as AssessmentItemRef)) as AssessmentItemRef,
214
+ passage: effectiveItem as any
215
+ } as ToolContext as ItemToolContext;
216
+ }
217
+ return {
218
+ level: 'item',
219
+ assessment: (assessment || {}) as AssessmentEntity,
220
+ itemRef: (itemRef || ({ id: effectiveCanonicalItemId } as AssessmentItemRef)) as AssessmentItemRef,
221
+ item: effectiveItem as ItemEntity
222
+ } as ToolContext as ItemToolContext;
223
+ });
224
+
225
+ const renderContext = $derived.by((): ToolContext => {
226
+ if (toolContext) return toolContext;
227
+ return {
228
+ level: 'item',
229
+ assessment: (assessment || {}) as AssessmentEntity,
230
+ itemRef: (itemRef || ({ id: effectiveCanonicalItemId } as AssessmentItemRef)) as AssessmentItemRef,
231
+ item: ((effectiveItem as ItemEntity | null) || ({ id: effectiveCanonicalItemId, config: {} } as ItemEntity)) as ItemEntity
232
+ } as ToolContext;
233
+ });
234
+
235
+ const elementContexts = $derived.by((): ElementToolContext[] => {
236
+ if (effectiveLevel !== 'item' && effectiveLevel !== 'passage') return [];
237
+ if (!contentReady || !effectiveItem) return [];
238
+ const modelsRaw = (effectiveItem as any)?.config?.models;
239
+ const models = Array.isArray(modelsRaw)
240
+ ? modelsRaw
241
+ : modelsRaw && typeof modelsRaw === 'object'
242
+ ? Object.values(modelsRaw)
243
+ : [];
244
+ return models
245
+ .filter((model: any) => model && typeof model === 'object' && typeof model.id === 'string')
246
+ .map((model: any) => ({
247
+ level: 'element' as const,
248
+ assessment: (assessment || {}) as AssessmentEntity,
249
+ itemRef: (itemRef || ({ id: effectiveCanonicalItemId } as AssessmentItemRef)) as AssessmentItemRef,
250
+ item: effectiveItem as ItemEntity,
251
+ elementId: model.id as string
252
+ }));
253
+ });
254
+
255
+ // Pass 2: tool-owned context filtering (item + element aggregation)
256
+ const visibleToolIds = $derived.by(() => {
257
+ const levelCompatibleToolIds = allowedToolIds.filter((toolId) => {
258
+ const registration = effectiveToolRegistry.get(toolId);
259
+ return registration ? registration.supportedLevels.includes(effectiveLevel) : false;
260
+ });
261
+ if (effectiveLevel === 'section') {
262
+ // Section toolbars are orchestrator-driven. Tool-level relevance is often
263
+ // item-content dependent. Keep pass-1 as the source of truth here.
264
+ return allowedToolIds;
265
+ }
266
+ if (!contentReady) {
267
+ // Stage 1: orchestrator-level allow-list only.
268
+ return levelCompatibleToolIds;
269
+ }
270
+ if (!toolContext && elementContexts.length === 0) {
271
+ return levelCompatibleToolIds;
272
+ }
273
+ const visible = new Set<string>();
274
+ if (toolContext) {
275
+ effectiveToolRegistry
276
+ .filterVisibleInContext(levelCompatibleToolIds, toolContext)
277
+ .forEach((tool) => visible.add(tool.toolId));
278
+ }
279
+
280
+ for (const context of elementContexts) {
281
+ effectiveToolRegistry
282
+ .filterVisibleInContext(levelCompatibleToolIds, context)
283
+ .forEach((tool) => visible.add(tool.toolId));
284
+ }
285
+
286
+ return Array.from(visible);
287
+ });
288
+
289
+ // Dynamically load whatever tools are currently visible.
290
+ // The registry owns module loader configuration by toolId.
291
+ $effect(() => {
292
+ if (!isBrowser) return;
293
+ let cancelled = false;
294
+ void effectiveToolRegistry
295
+ .ensureToolModulesLoaded(visibleToolIds)
296
+ .then(() => {
297
+ if (!cancelled) moduleLoadVersion += 1;
298
+ })
299
+ .catch((error: unknown) => {
300
+ console.error('[ItemToolBar] Failed to load one or more tool modules:', error);
301
+ });
302
+ return () => {
303
+ cancelled = true;
304
+ };
305
+ });
306
+
307
+ function resolveScopeElement(): HTMLElement | null {
308
+ if (scopeElement) return scopeElement;
309
+ return shellContext?.scopeElement || null;
310
+ }
311
+
312
+ function resolveGlobalElementId(): string | null {
313
+ if (!effectiveElementToolStateStore || !effectiveAssessmentId || !effectiveSectionId || !effectiveCanonicalItemId)
314
+ return null;
315
+ return effectiveElementToolStateStore.getGlobalElementId(
316
+ effectiveAssessmentId,
317
+ effectiveSectionId,
318
+ effectiveCanonicalItemId,
319
+ effectiveCanonicalItemId
320
+ );
321
+ }
322
+
323
+ const toolbarContext = $derived.by((): ToolbarContext => {
324
+ const toolkitCoordinator = runtimeContext?.toolkitCoordinator || null;
325
+ const toInstanceToolId = (toolId: string): string => {
326
+ // Accept either base tool IDs (e.g. "calculator") or already-scoped IDs.
327
+ // This keeps toolbar contracts stable across mixed registration implementations.
328
+ return parseScopedToolId(toolId)
329
+ ? toolId
330
+ : createScopedToolId(toolId, effectiveLevel, effectiveScopeId);
331
+ };
332
+ return {
333
+ scope: {
334
+ level: effectiveLevel,
335
+ scopeId: effectiveScopeId,
336
+ assessmentId: runtimeContext?.assessmentId,
337
+ sectionId: effectiveSectionId,
338
+ itemId: effectiveItemId,
339
+ canonicalItemId: effectiveCanonicalItemId,
340
+ contentKind: effectiveContentKind
341
+ },
342
+ itemId: effectiveScopeId,
343
+ catalogId: effectiveCatalogId,
344
+ language,
345
+ ui: {
346
+ size
347
+ },
348
+ getScopeElement: resolveScopeElement,
349
+ getGlobalElementId: resolveGlobalElementId,
350
+ toolCoordinator: effectiveToolCoordinator || null,
351
+ toolkitCoordinator,
352
+ ttsService: effectiveTTSService || null,
353
+ elementToolStateStore: effectiveElementToolStateStore || null,
354
+ toggleTool: (toolId: string) => {
355
+ if (!effectiveToolCoordinator) return;
356
+ const instanceToolId = toInstanceToolId(toolId);
357
+ // Some tools only self-register after first visibility sync.
358
+ // Seed a placeholder registration so first toggle always works.
359
+ if (!effectiveToolCoordinator.getToolState(instanceToolId)) {
360
+ effectiveToolCoordinator.registerTool(instanceToolId, toolId);
361
+ }
362
+ effectiveToolCoordinator.toggleTool(instanceToolId);
363
+ },
364
+ isToolVisible: (toolId: string) => {
365
+ if (!effectiveToolCoordinator) return false;
366
+ return effectiveToolCoordinator.isToolVisible(toInstanceToolId(toolId));
367
+ },
368
+ subscribeVisibility: effectiveToolCoordinator
369
+ ? (listener: () => void) => effectiveToolCoordinator.subscribe(listener)
370
+ : null,
371
+ ensureTTSReady: toolkitCoordinator ? () => toolkitCoordinator.ensureTTSReady() : null
372
+ };
373
+ });
374
+
375
+ let renderedTools = $derived.by((): ToolToolbarRenderResult[] => {
376
+ if (!isBrowser) return [];
377
+ moduleLoadVersion;
378
+
379
+ const rendered: ToolToolbarRenderResult[] = [];
380
+ for (const toolId of visibleToolIds) {
381
+ const result = effectiveToolRegistry.renderForToolbar(toolId, renderContext, toolbarContext);
382
+ if (result) {
383
+ rendered.push(result);
384
+ }
385
+ }
386
+ return rendered;
387
+ });
388
+ const normalizedHostButtons = $derived.by((): ToolbarItem[] =>
389
+ Array.isArray(hostButtons)
390
+ ? hostButtons.filter((item): item is ToolbarItem => !!item && typeof item.id === 'string')
391
+ : []
392
+ );
393
+ const nativeToolbarItems = $derived.by((): ToolbarItem[] =>
394
+ renderedTools
395
+ .filter((renderedTool) => !!renderedTool.button)
396
+ .map((renderedTool) => {
397
+ const button = renderedTool.button!;
398
+ return {
399
+ id: renderedTool.toolId,
400
+ label: button.label,
401
+ ariaLabel: button.ariaLabel || button.label,
402
+ icon: button.icon,
403
+ tooltip: button.tooltip || button.label,
404
+ disabled: button.disabled,
405
+ active: activeToolState[renderedTool.toolId] ?? button.active ?? false,
406
+ onClick: () => {
407
+ button.onClick();
408
+ syncRenderedToolsState();
409
+ },
410
+ } satisfies ToolbarItem;
411
+ })
412
+ );
413
+ const toolbarItems = $derived.by((): ToolbarItem[] => [
414
+ ...nativeToolbarItems,
415
+ ...normalizedHostButtons
416
+ ]);
417
+ const mountedElementsBeforeButtons = $derived.by(() =>
418
+ renderedTools.flatMap((renderedTool) =>
419
+ (renderedTool.elements || [])
420
+ .filter((entry) => entry.mount === 'before-buttons')
421
+ .map((entry) => entry.element)
422
+ .filter((entry): entry is HTMLElement => Boolean(entry))
423
+ )
424
+ );
425
+ const mountedElementsAfterButtons = $derived.by(() =>
426
+ renderedTools.flatMap((renderedTool) =>
427
+ (renderedTool.elements || [])
428
+ .filter((entry) => entry.mount === 'after-buttons')
429
+ .map((entry) => entry.element)
430
+ .filter((entry): entry is HTMLElement => Boolean(entry))
431
+ )
432
+ );
433
+
434
+ function isToolbarItemActive(item: ToolbarItem): boolean {
435
+ if (item.id in activeToolState) {
436
+ return activeToolState[item.id] === true;
437
+ }
438
+ return item.active === true;
439
+ }
440
+
441
+ function getFallbackIconSvg(iconName: string): string | null {
442
+ const iconMap: Record<string, string> = {
443
+ calculator:
444
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7 2h10a2 2 0 0 1 2 2v16a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2Zm0 2v4h10V4H7Zm0 6v2h2v-2H7Zm4 0v2h2v-2h-2Zm4 0v2h2v-2h-2Zm-8 4v2h2v-2H7Zm4 0v2h2v-2h-2Zm4 0v2h2v-2h-2Zm-8 4v2h2v-2H7Zm4 0v2h2v-2h-2Zm4 0v2h2v-2h-2Z" fill="currentColor"/></svg>',
445
+ 'volume-up':
446
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14 3.23v2.06A7.002 7.002 0 0 1 19 12a7 7 0 0 1-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77Zm-2 17.75V3L7 8H3v8h4l5 5Zm4.5-9a4.5 4.5 0 0 0-2.5-4.03v8.05A4.5 4.5 0 0 0 16.5 12Z" fill="currentColor"/></svg>',
447
+ swatch:
448
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 3a9 9 0 1 0 9 9c0-.55-.45-1-1-1h-2.5a1.5 1.5 0 0 1 0-3H20a1 1 0 0 0 1-1 8.99 8.99 0 0 0-9-4Zm-5.5 9A1.5 1.5 0 1 1 8 13.5 1.5 1.5 0 0 1 6.5 12Zm3-4A1.5 1.5 0 1 1 11 9.5 1.5 1.5 0 0 1 9.5 8Zm5 0A1.5 1.5 0 1 1 16 9.5 1.5 1.5 0 0 1 14.5 8Z" fill="currentColor"/></svg>',
449
+ 'chart-bar':
450
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4.75 5a.76.76 0 0 1 .75.75v11c0 .438.313.75.75.75h13a.76.76 0 0 1 .696 1.039.74.74 0 0 1-.696.461h-13C5 19 4 18 4 16.75v-11A.74.74 0 0 1 4.75 5ZM8 8.25a.74.74 0 0 1 .75-.75h6.5a.76.76 0 0 1 .696 1.039.74.74 0 0 1-.696.461h-6.5A.722.722 0 0 1 8 8.25Zm.75 2.25h4.5a.76.76 0 0 1 .696 1.039.74.74 0 0 1-.696.461h-4.5a.723.723 0 0 1-.75-.75.74.74 0 0 1 .75-.75Zm0 3h8.5a.76.76 0 0 1 .696 1.039.74.74 0 0 1-.696.461h-8.5a.723.723 0 0 1-.75-.75.74.74 0 0 1 .75-.75Z" fill="currentColor"/></svg>',
451
+ beaker:
452
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5 21c-.85 0-1.454-.38-1.813-1.137-.358-.759-.27-1.463.263-2.113L9 11V5H8a.968.968 0 0 1-.713-.287A.968.968 0 0 1 7 4c0-.283.096-.52.287-.712A.968.968 0 0 1 8 3h8c.283 0 .52.096.712.288.192.191.288.429.288.712s-.096.52-.288.713A.968.968 0 0 1 16 5h-1v6l5.55 6.75c.533.65.62 1.354.262 2.113C20.454 20.62 19.85 21 19 21H5Zm2-3h10l-3.4-4h-3.2L7 18Zm-2 1h14l-6-7.3V5h-2v6.7L5 19Z" fill="currentColor"/></svg>',
453
+ protractor:
454
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="m6.75 21-.25-2.2 2.85-7.85a3.95 3.95 0 0 0 1.75.95l-2.75 7.55L6.75 21Zm10.5 0-1.6-1.55-2.75-7.55a3.948 3.948 0 0 0 1.75-.95l2.85 7.85-.25 2.2ZM12 11a2.893 2.893 0 0 1-2.125-.875A2.893 2.893 0 0 1 9 8c0-.65.188-1.23.563-1.737A2.935 2.935 0 0 1 11 5.2V3h2v2.2c.583.2 1.063.554 1.438 1.063C14.812 6.77 15 7.35 15 8c0 .833-.292 1.542-.875 2.125A2.893 2.893 0 0 1 12 11Zm0-2c.283 0 .52-.096.713-.287A.967.967 0 0 0 13 8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8c0 .283.096.52.287.713.192.191.43.287.713.287Z" fill="currentColor"/></svg>',
455
+ 'bars-3':
456
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6.85 15c.517 0 .98-.15 1.388-.45.408-.3.695-.692.862-1.175l.375-1.15c.267-.8.2-1.537-.2-2.213C8.875 9.337 8.3 9 7.55 9H4.025l.475 3.925c.083.583.346 1.075.787 1.475.442.4.963.6 1.563.6Zm10.3 0c.6 0 1.12-.2 1.563-.6.441-.4.704-.892.787-1.475L19.975 9h-3.5c-.75 0-1.325.342-1.725 1.025-.4.683-.467 1.425-.2 2.225l.35 1.125c.167.483.454.875.862 1.175.409.3.871.45 1.388.45Zm-10.3 2c-1.1 0-2.063-.363-2.887-1.088a4.198 4.198 0 0 1-1.438-2.737L2 9H1V7h6.55c.733 0 1.404.18 2.013.537A3.906 3.906 0 0 1 11 9h2.025c.35-.617.83-1.104 1.438-1.463A3.892 3.892 0 0 1 16.474 7H23v2h-1l-.525 4.175a4.198 4.198 0 0 1-1.438 2.737A4.238 4.238 0 0 1 17.15 17c-.95 0-1.804-.27-2.562-.813A4.234 4.234 0 0 1 13 14.026l-.375-1.125a21.35 21.35 0 0 1-.1-.363 4.926 4.926 0 0 1-.1-.537h-.85c-.033.2-.067.363-.1.488a21.35 21.35 0 0 1-.1.362L11 14a4.3 4.3 0 0 1-1.588 2.175A4.258 4.258 0 0 1 6.85 17Z" fill="currentColor"/></svg>',
457
+ ruler:
458
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="m8.8 10.95 2.15-2.175-1.4-1.425-1.1 1.1-1.4-1.4 1.075-1.1L7 4.825 4.825 7 8.8 10.95Zm8.2 8.225L19.175 17l-1.125-1.125-1.1 1.075-1.4-1.4 1.075-1.1-1.425-1.4-2.15 2.15L17 19.175ZM7.25 21H3v-4.25l4.375-4.375L2 7l5-5 5.4 5.4 3.775-3.8c.2-.2.425-.35.675-.45a2.068 2.068 0 0 1 1.55 0c.25.1.475.25.675.45L20.4 4.95c.2.2.35.425.45.675.1.25.15.508.15.775a1.975 1.975 0 0 1-.6 1.425l-3.775 3.8L22 17l-5 5-5.375-5.375L7.25 21ZM5 19h1.4l9.8-9.775L14.775 7.8 5 17.6V19Z" fill="currentColor"/></svg>'
459
+ };
460
+ return iconMap[iconName] || null;
461
+ }
462
+
463
+ function syncRenderedToolsState() {
464
+ const nextActiveState: Record<string, boolean> = {};
465
+ for (const renderedTool of renderedTools) {
466
+ renderedTool.sync?.();
467
+ if (renderedTool.button) {
468
+ nextActiveState[renderedTool.toolId] = renderedTool.button.active ?? false;
469
+ }
470
+ }
471
+ activeToolState = nextActiveState;
472
+ }
473
+
474
+ $effect(() => {
475
+ const unsubs: Array<() => void> = [];
476
+ syncRenderedToolsState();
477
+ for (const renderedTool of renderedTools) {
478
+ if (renderedTool.subscribeActive) {
479
+ const unsubscribe = renderedTool.subscribeActive((active) => {
480
+ activeToolState = {
481
+ ...activeToolState,
482
+ [renderedTool.toolId]: active
483
+ };
484
+ renderedTool.sync?.();
485
+ });
486
+ unsubs.push(unsubscribe);
487
+ }
488
+ }
489
+
490
+ return () => {
491
+ unsubs.forEach((unsub) => unsub());
492
+ };
493
+ });
494
+
495
+ $effect(() => {
496
+ if (!toolbarContext.subscribeVisibility) return;
497
+ return toolbarContext.subscribeVisibility(() => {
498
+ syncRenderedToolsState();
499
+ });
500
+ });
501
+
502
+ function mountElement(node: HTMLSpanElement, element: HTMLElement | null) {
503
+ let mountedElement: HTMLElement | null = null;
504
+ const updateMountedElement = (nextElement: HTMLElement | null) => {
505
+ if (mountedElement === nextElement) return;
506
+ if (mountedElement && mountedElement.parentNode === node) {
507
+ node.removeChild(mountedElement);
508
+ }
509
+ mountedElement = nextElement;
510
+ if (mountedElement) {
511
+ if (mountedElement.parentNode && mountedElement.parentNode !== node) {
512
+ mountedElement.parentNode.removeChild(mountedElement);
513
+ }
514
+ node.appendChild(mountedElement);
515
+ }
516
+ };
517
+ updateMountedElement(element);
518
+ return {
519
+ update(nextElement: HTMLElement | null) {
520
+ updateMountedElement(nextElement);
521
+ },
522
+ destroy() {
523
+ updateMountedElement(null);
524
+ }
525
+ };
526
+ }
527
+ </script>
528
+
529
+ {#if isBrowser}
530
+ <div
531
+ class="item-toolbar {className} item-toolbar--{size}"
532
+ class:item-toolbar--top={position === 'top'}
533
+ class:item-toolbar--right={position === 'right'}
534
+ class:item-toolbar--bottom={position === 'bottom'}
535
+ class:item-toolbar--left={position === 'left'}
536
+ data-content-kind={effectiveContentKind}
537
+ data-level={effectiveLevel}
538
+ bind:this={toolbarRootElement}
539
+ >
540
+ {#each mountedElementsBeforeButtons as mountedElement (`before-${mountedElement.tagName}-${mountedElement.getAttribute('tool-id') || ''}`)}
541
+ <span class="item-toolbar__element-host" use:mountElement={mountedElement}></span>
542
+ {/each}
543
+
544
+ {#each toolbarItems as item (item.id)}
545
+ {#if isToolbarLinkItem(item)}
546
+ <a
547
+ class="item-toolbar__button"
548
+ class:item-toolbar__button--active={isToolbarItemActive(item)}
549
+ href={item.disabled ? undefined : item.href}
550
+ target={item.target}
551
+ rel={item.rel}
552
+ aria-label={item.ariaLabel || item.label}
553
+ title={item.tooltip || item.label}
554
+ aria-disabled={item.disabled ? 'true' : undefined}
555
+ onclick={(event) => {
556
+ if (item.disabled) {
557
+ event.preventDefault();
558
+ }
559
+ }}
560
+ >
561
+ {#if item.icon}
562
+ {#if isInlineSvgIcon(item.icon)}
563
+ {@html item.icon}
564
+ {:else if isExternalIconUrl(item.icon)}
565
+ <img class="item-toolbar__icon-image" src={item.icon} alt="" />
566
+ {:else}
567
+ {@const fallbackIcon = getFallbackIconSvg(item.icon)}
568
+ {#if fallbackIcon}
569
+ {@html fallbackIcon}
570
+ {:else}
571
+ <i class={`icon icon-${item.icon}`} aria-hidden="true"></i>
572
+ {/if}
573
+ {/if}
574
+ {/if}
575
+ </a>
576
+ {:else}
577
+ <button
578
+ type="button"
579
+ class="item-toolbar__button"
580
+ class:item-toolbar__button--active={isToolbarItemActive(item)}
581
+ onclick={item.onClick}
582
+ aria-label={item.ariaLabel || item.label}
583
+ aria-pressed={isToolbarItemActive(item)}
584
+ title={item.tooltip || item.label}
585
+ disabled={item.disabled}
586
+ >
587
+ {#if item.icon}
588
+ {#if isInlineSvgIcon(item.icon)}
589
+ {@html item.icon}
590
+ {:else if isExternalIconUrl(item.icon)}
591
+ <img class="item-toolbar__icon-image" src={item.icon} alt="" />
592
+ {:else}
593
+ {@const fallbackIcon = getFallbackIconSvg(item.icon)}
594
+ {#if fallbackIcon}
595
+ {@html fallbackIcon}
596
+ {:else}
597
+ <i class={`icon icon-${item.icon}`} aria-hidden="true"></i>
598
+ {/if}
599
+ {/if}
600
+ {/if}
601
+ </button>
602
+ {/if}
603
+ {/each}
604
+
605
+ {#each mountedElementsAfterButtons as mountedElement (`after-${mountedElement.tagName}-${mountedElement.getAttribute('tool-id') || ''}`)}
606
+ <span class="item-toolbar__element-host" use:mountElement={mountedElement}></span>
607
+ {/each}
608
+ </div>
609
+ {/if}
610
+
611
+ <style>
612
+ .item-toolbar {
613
+ display: flex;
614
+ align-items: center;
615
+ gap: 0.5rem;
616
+ }
617
+
618
+ .item-toolbar--top,
619
+ .item-toolbar--bottom {
620
+ flex-direction: row;
621
+ flex-wrap: wrap;
622
+ }
623
+
624
+ .item-toolbar--left,
625
+ .item-toolbar--right {
626
+ flex-direction: column;
627
+ align-items: stretch;
628
+ }
629
+
630
+ .item-toolbar__button {
631
+ display: flex;
632
+ align-items: center;
633
+ justify-content: center;
634
+ width: 2rem;
635
+ height: 2rem;
636
+ padding: 0.25rem;
637
+ border: 1px solid var(--pie-border, #ccc);
638
+ border-radius: 0.25rem;
639
+ background-color: var(--pie-background, white);
640
+ color: var(--pie-text, #333);
641
+ cursor: pointer;
642
+ transition: all 0.15s ease;
643
+ text-decoration: none;
644
+ }
645
+
646
+ .item-toolbar__element-host {
647
+ display: contents;
648
+ }
649
+
650
+ .item-toolbar--sm .item-toolbar__button {
651
+ width: 2.75rem;
652
+ height: 2.75rem;
653
+ }
654
+
655
+ .item-toolbar--lg .item-toolbar__button {
656
+ width: 2.5rem;
657
+ height: 2.5rem;
658
+ }
659
+
660
+ .item-toolbar__button:hover:not(:disabled) {
661
+ background-color: var(--pie-secondary-background, #f5f5f5);
662
+ transform: translateY(-1px);
663
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
664
+ }
665
+
666
+ .item-toolbar__button:active:not(:disabled) {
667
+ transform: translateY(0);
668
+ box-shadow: none;
669
+ }
670
+
671
+ .item-toolbar__button--active {
672
+ background-color: var(--pie-primary, #1976d2);
673
+ color: white;
674
+ border-color: var(--pie-primary, #1976d2);
675
+ }
676
+
677
+ .item-toolbar__button--active:hover:not(:disabled) {
678
+ background-color: var(--pie-primary-dark, #1565c0);
679
+ }
680
+
681
+ .item-toolbar__button:focus-visible {
682
+ outline: 2px solid var(--pie-primary, #1976d2);
683
+ outline-offset: 2px;
684
+ }
685
+
686
+ .item-toolbar__button:disabled {
687
+ opacity: 0.5;
688
+ cursor: not-allowed;
689
+ }
690
+
691
+ .item-toolbar__button :global(svg) {
692
+ width: 100%;
693
+ height: 100%;
694
+ }
695
+
696
+ .item-toolbar__icon-image {
697
+ width: 100%;
698
+ height: 100%;
699
+ object-fit: contain;
700
+ }
701
+ </style>