@pie-players/pie-assessment-toolkit 0.3.73 → 0.3.75

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 (274) hide show
  1. package/README.md +469 -318
  2. package/dist/attempt/AssessmentSession.d.ts +1 -5
  3. package/dist/attempt/AssessmentSession.js +0 -29
  4. package/dist/attempt/TestSession.d.ts +1 -20
  5. package/dist/attempt/TestSession.js +8 -99
  6. package/dist/components/ItemScope.custom-element.d.ts +1 -0
  7. package/dist/components/ItemScope.custom-element.js +1 -0
  8. package/dist/components/ItemToolBar.custom-element.d.ts +1 -0
  9. package/dist/components/ItemToolBar.custom-element.js +1 -1
  10. package/dist/components/PieAssessmentToolkit.custom-element.d.ts +1 -0
  11. package/dist/components/PieAssessmentToolkit.custom-element.js +13 -13
  12. package/dist/components/SectionToolBar.custom-element.d.ts +1 -0
  13. package/dist/components/SectionToolBar.custom-element.js +1 -1
  14. package/dist/components/chunks/ItemToolBar-7tt7v2xp.js +4 -0
  15. package/dist/components/chunks/ItemToolBar-97hx60z4.js +2 -0
  16. package/dist/components/chunks/ItemToolBar-bxwyhr5p.js +2 -0
  17. package/dist/components/chunks/ItemToolBar-jx38w99s.js +2 -0
  18. package/dist/components/chunks/ItemToolBar-rd2bb4p6.js +40 -0
  19. package/dist/components/chunks/ItemToolBar-vdkyn9f3.js +2 -0
  20. package/dist/components/chunks/sre-engine-jd8cc08x.js +1 -0
  21. package/dist/components/item-scope-element.d.ts +2 -0
  22. package/dist/components/item-scope-element.js +2 -0
  23. package/dist/context/assessment-toolkit-context.d.ts +32 -6
  24. package/dist/index.d.ts +23 -48
  25. package/dist/index.js +12 -37
  26. package/dist/policy/core/PolicySource.d.ts +1 -1
  27. package/dist/policy/core/ToolPolicyEngine.d.ts +85 -34
  28. package/dist/policy/core/ToolPolicyEngine.js +148 -46
  29. package/dist/policy/core/compose-decision.d.ts +28 -7
  30. package/dist/policy/core/compose-decision.js +75 -54
  31. package/dist/policy/core/decision-types.d.ts +65 -34
  32. package/dist/policy/core/feature-decision.d.ts +16 -9
  33. package/dist/policy/core/feature-decision.js +14 -8
  34. package/dist/policy/core/pnp-policy-inputs.d.ts +24 -33
  35. package/dist/policy/core/pnp-policy-inputs.js +28 -56
  36. package/dist/policy/core/provenance.d.ts +9 -4
  37. package/dist/policy/core/provenance.js +1 -1
  38. package/dist/policy/engine.d.ts +7 -23
  39. package/dist/policy/engine.js +5 -20
  40. package/dist/policy/sources/PnpPolicySource.d.ts +44 -61
  41. package/dist/policy/sources/PnpPolicySource.js +117 -118
  42. package/dist/runtime/SectionControllerBinding.d.ts +279 -0
  43. package/dist/runtime/SectionControllerBinding.js +450 -0
  44. package/dist/runtime/SectionRuntimeEngine.d.ts +26 -289
  45. package/dist/runtime/SectionRuntimeEngine.js +30 -372
  46. package/dist/runtime/adapter/SectionEngineAdapter.d.ts +19 -80
  47. package/dist/runtime/adapter/SectionEngineAdapter.js +19 -92
  48. package/dist/runtime/adapter/dom-event-bridge.d.ts +15 -30
  49. package/dist/runtime/adapter/dom-event-bridge.js +17 -44
  50. package/dist/runtime/adapter/subscriber-fanout.d.ts +1 -2
  51. package/dist/runtime/adapter/subscriber-fanout.js +1 -2
  52. package/dist/runtime/catalog-context.d.ts +19 -0
  53. package/dist/runtime/catalog-context.js +28 -0
  54. package/dist/runtime/composition-emit-scheduler.d.ts +9 -15
  55. package/dist/runtime/composition-emit-scheduler.js +9 -15
  56. package/dist/runtime/content-language.d.ts +32 -0
  57. package/dist/runtime/content-language.js +46 -0
  58. package/dist/runtime/content-region.d.ts +17 -0
  59. package/dist/runtime/content-region.js +29 -0
  60. package/dist/runtime/content-registration.d.ts +13 -0
  61. package/dist/runtime/content-registration.js +25 -0
  62. package/dist/runtime/core/SectionEngineCore.d.ts +2 -2
  63. package/dist/runtime/core/SectionEngineCore.js +2 -2
  64. package/dist/runtime/core/cohort.d.ts +1 -1
  65. package/dist/runtime/core/cohort.js +1 -1
  66. package/dist/runtime/core/engine-input.d.ts +18 -30
  67. package/dist/runtime/core/engine-input.js +11 -12
  68. package/dist/runtime/core/engine-output.d.ts +16 -40
  69. package/dist/runtime/core/engine-output.js +9 -21
  70. package/dist/runtime/core/engine-readiness.d.ts +5 -37
  71. package/dist/runtime/core/engine-readiness.js +8 -41
  72. package/dist/runtime/core/engine-resolver.d.ts +46 -74
  73. package/dist/runtime/core/engine-resolver.js +18 -43
  74. package/dist/runtime/core/engine-state.d.ts +28 -36
  75. package/dist/runtime/core/engine-state.js +18 -21
  76. package/dist/runtime/core/engine-transition.d.ts +14 -19
  77. package/dist/runtime/core/engine-transition.js +71 -51
  78. package/dist/runtime/engine.d.ts +16 -19
  79. package/dist/runtime/engine.js +12 -19
  80. package/dist/runtime/page-warnings.d.ts +13 -0
  81. package/dist/runtime/page-warnings.js +22 -0
  82. package/dist/runtime/policy-input-forwarding.d.ts +25 -0
  83. package/dist/runtime/policy-input-forwarding.js +25 -0
  84. package/dist/runtime/registration-events.d.ts +14 -0
  85. package/dist/runtime/runtime-event-claim.d.ts +8 -0
  86. package/dist/runtime/runtime-event-claim.js +15 -0
  87. package/dist/runtime/shell-event-bridge.d.ts +45 -0
  88. package/dist/runtime/shell-event-bridge.js +127 -0
  89. package/dist/runtime/shell-registration.d.ts +73 -0
  90. package/dist/runtime/shell-registration.js +95 -0
  91. package/dist/runtime/shell-scope.d.ts +68 -0
  92. package/dist/runtime/shell-scope.js +188 -0
  93. package/dist/runtime/tool-host-contract.d.ts +28 -10
  94. package/dist/runtime/tool-host-contract.js +31 -10
  95. package/dist/runtime/unclaimed-registration-watch.d.ts +7 -0
  96. package/dist/runtime/unclaimed-registration-watch.js +23 -0
  97. package/dist/services/ElementToolStateStore.d.ts +14 -10
  98. package/dist/services/ElementToolStateStore.js +37 -13
  99. package/dist/services/HighlightCoordinator.d.ts +22 -28
  100. package/dist/services/HighlightCoordinator.js +431 -327
  101. package/dist/services/RangeSerializer.d.ts +10 -1
  102. package/dist/services/RangeSerializer.js +87 -14
  103. package/dist/services/TTSService.d.ts +130 -69
  104. package/dist/services/TTSService.js +713 -570
  105. package/dist/services/ToolCoordinator.d.ts +43 -66
  106. package/dist/services/ToolCoordinator.js +164 -208
  107. package/dist/services/ToolRegistry.d.ts +78 -54
  108. package/dist/services/ToolRegistry.js +107 -105
  109. package/dist/services/ToolkitCoordinator.d.ts +261 -175
  110. package/dist/services/ToolkitCoordinator.js +667 -433
  111. package/dist/services/audio-handoff.d.ts +6 -7
  112. package/dist/services/audio-handoff.js +20 -14
  113. package/dist/services/catalog-owner.d.ts +11 -1
  114. package/dist/services/catalog-owner.js +25 -0
  115. package/dist/services/create-tools-config.d.ts +2 -2
  116. package/dist/services/framework-error-bus.d.ts +3 -6
  117. package/dist/services/framework-error-bus.js +3 -6
  118. package/dist/services/framework-error.d.ts +30 -8
  119. package/dist/services/framework-error.js +9 -7
  120. package/dist/services/interfaces.d.ts +158 -129
  121. package/dist/services/section-controller-types.d.ts +15 -0
  122. package/dist/services/section-session-assignment.d.ts +21 -0
  123. package/dist/services/section-session-assignment.js +82 -0
  124. package/dist/services/spoken-audio-cards.d.ts +0 -2
  125. package/dist/services/spoken-audio-cards.js +1 -3
  126. package/dist/services/tool-config-defaults.d.ts +3 -3
  127. package/dist/services/tool-config-defaults.js +3 -3
  128. package/dist/services/tool-config-validation.d.ts +14 -4
  129. package/dist/services/tool-config-validation.js +63 -71
  130. package/dist/services/tool-context.d.ts +0 -8
  131. package/dist/services/tool-context.js +29 -25
  132. package/dist/services/tool-icons.d.ts +4 -3
  133. package/dist/services/tool-icons.js +4 -3
  134. package/dist/services/tool-instance-id.d.ts +0 -1
  135. package/dist/services/tool-instance-id.js +0 -6
  136. package/dist/services/tool-providers/TTSToolProvider.d.ts +41 -80
  137. package/dist/services/tool-providers/TTSToolProvider.js +81 -40
  138. package/dist/services/tool-providers/ToolProviderApi.d.ts +10 -4
  139. package/dist/services/tool-providers/ToolProviderRegistry.d.ts +32 -28
  140. package/dist/services/tool-providers/ToolProviderRegistry.js +121 -107
  141. package/dist/services/tool-providers/index.d.ts +0 -9
  142. package/dist/services/tool-providers/index.js +2 -4
  143. package/dist/services/tool-request.d.ts +27 -12
  144. package/dist/services/tool-request.js +24 -14
  145. package/dist/services/toolbar-decided-tools.d.ts +15 -0
  146. package/dist/services/toolbar-decided-tools.js +28 -0
  147. package/dist/services/tools-config-normalizer.d.ts +37 -23
  148. package/dist/services/tools-config-normalizer.js +2 -10
  149. package/dist/services/tts/browser-provider.d.ts +38 -2
  150. package/dist/services/tts/browser-provider.js +276 -140
  151. package/dist/services/tts/catalog-span-alignment.d.ts +2 -17
  152. package/dist/services/tts/catalog-span-alignment.js +17 -142
  153. package/dist/services/tts/flat-tree.d.ts +71 -0
  154. package/dist/services/tts/flat-tree.js +269 -0
  155. package/dist/services/tts/generated-speech/assemble-plan.d.ts +3 -1
  156. package/dist/services/tts/generated-speech/assemble-plan.js +3 -1
  157. package/dist/services/tts/generated-speech/dom/build-dom-plan.d.ts +2 -0
  158. package/dist/services/tts/generated-speech/dom/build-dom-plan.js +1 -1
  159. package/dist/services/tts/generated-speech/math-speech-cache.d.ts +3 -1
  160. package/dist/services/tts/generated-speech/math-speech-cache.js +5 -1
  161. package/dist/services/tts/generated-speech/types.d.ts +0 -12
  162. package/dist/services/tts/highlight-pipeline/boundary-normalizer.js +12 -62
  163. package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +38 -5
  164. package/dist/services/tts/highlight-pipeline/normalize-speech-chunks.js +3 -6
  165. package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js +6 -4
  166. package/dist/services/tts/highlight-pipeline/readable-region.js +3 -2
  167. package/dist/services/tts/highlight-pipeline/types.d.ts +5 -0
  168. package/dist/services/tts/highlight-pipeline/visible-map-range.d.ts +12 -0
  169. package/dist/services/tts/highlight-pipeline/visible-map-range.js +73 -7
  170. package/dist/services/tts/math-alignment/index.d.ts +0 -5
  171. package/dist/services/tts/math-alignment/index.js +5 -25
  172. package/dist/services/tts/math-alignment/speech-tokenizer.d.ts +9 -1
  173. package/dist/services/tts/math-alignment/speech-tokenizer.js +38 -36
  174. package/dist/services/tts/math-alignment/types.d.ts +4 -2
  175. package/dist/services/tts/math-aware-text-processing.d.ts +5 -1
  176. package/dist/services/tts/math-aware-text-processing.js +74 -18
  177. package/dist/services/tts/math-control-names.d.ts +21 -0
  178. package/dist/services/tts/math-control-names.js +142 -0
  179. package/dist/services/tts/math-speech.d.ts +16 -1
  180. package/dist/services/tts/math-speech.js +55 -19
  181. package/dist/services/tts/provider-options.d.ts +41 -0
  182. package/dist/services/tts/provider-options.js +1 -0
  183. package/dist/services/tts/sre-engine.d.ts +7 -0
  184. package/dist/services/tts/sre-engine.js +11 -0
  185. package/dist/services/tts/sre-locales.d.ts +14 -0
  186. package/dist/services/tts/sre-locales.js +57 -0
  187. package/dist/services/tts/ssml/spoken-text.d.ts +2 -26
  188. package/dist/services/tts/ssml/spoken-text.js +29 -0
  189. package/dist/services/tts/start-failure.d.ts +11 -0
  190. package/dist/services/tts/start-failure.js +19 -0
  191. package/dist/services/tts/text-processing.d.ts +9 -1
  192. package/dist/services/tts/text-processing.js +46 -34
  193. package/dist/services/tts/text-segmentation.d.ts +7 -0
  194. package/dist/services/tts/text-segmentation.js +47 -0
  195. package/dist/services/tts-runtime-config.d.ts +44 -18
  196. package/dist/services/tts-runtime-config.js +22 -93
  197. package/dist/tools/content-capability-resolution.d.ts +7 -9
  198. package/dist/tools/content-capability-resolution.js +8 -45
  199. package/dist/tools/registration.d.ts +58 -0
  200. package/dist/tools/registration.js +54 -0
  201. package/dist/tools/registrations/toolbar-registration-helpers.js +2 -2
  202. package/dist/tools/tool-surface-host.d.ts +6 -10
  203. package/dist/tools/tool-surface-host.js +44 -42
  204. package/dist/tools/tool-tag-map.d.ts +5 -15
  205. package/dist/tools/tool-tag-map.js +6 -20
  206. package/dist/utils/structural-equality.d.ts +11 -0
  207. package/dist/utils/structural-equality.js +47 -0
  208. package/package.json +24 -59
  209. package/dist/attempt/adapters/activity-to-test-attempt-session.d.ts +0 -49
  210. package/dist/attempt/adapters/activity-to-test-attempt-session.js +0 -138
  211. package/dist/components/chunks/ItemToolBar-byq80z42.js +0 -40
  212. package/dist/components/chunks/ItemToolBar-d36g3aw4.js +0 -49
  213. package/dist/context/runtime-context-consumer.d.ts +0 -13
  214. package/dist/context/runtime-context-consumer.js +0 -51
  215. package/dist/item-loader.d.ts +0 -44
  216. package/dist/item-loader.js +0 -61
  217. package/dist/policy/core/engine-context.d.ts +0 -29
  218. package/dist/policy/core/engine-context.js +0 -27
  219. package/dist/policy/core/policy-source-tag.d.ts +0 -31
  220. package/dist/policy/core/policy-source-tag.js +0 -28
  221. package/dist/policy/internal.d.ts +0 -25
  222. package/dist/policy/internal.js +0 -25
  223. package/dist/runtime/adapter/coordinator-bridge.d.ts +0 -92
  224. package/dist/runtime/adapter/coordinator-bridge.js +0 -126
  225. package/dist/runtime/adapter/framework-error-bridge.d.ts +0 -36
  226. package/dist/runtime/adapter/framework-error-bridge.js +0 -46
  227. package/dist/runtime/adapter/instrumentation-bridge.d.ts +0 -31
  228. package/dist/runtime/adapter/instrumentation-bridge.js +0 -38
  229. package/dist/runtime/core/engine-stage-derivation.d.ts +0 -18
  230. package/dist/runtime/core/engine-stage-derivation.js +0 -29
  231. package/dist/runtime/engine-context.d.ts +0 -39
  232. package/dist/runtime/engine-context.js +0 -31
  233. package/dist/runtime/internal.d.ts +0 -43
  234. package/dist/runtime/internal.js +0 -45
  235. package/dist/runtime/section-runtime-engine-host-context.d.ts +0 -80
  236. package/dist/runtime/section-runtime-engine-host-context.js +0 -95
  237. package/dist/runtime/stage-emit-gate.d.ts +0 -92
  238. package/dist/runtime/stage-emit-gate.js +0 -82
  239. package/dist/services/I18nService.d.ts +0 -47
  240. package/dist/services/I18nService.js +0 -84
  241. package/dist/services/ThemeProvider.d.ts +0 -95
  242. package/dist/services/ThemeProvider.js +0 -334
  243. package/dist/services/ToolConfigResolver.d.ts +0 -161
  244. package/dist/services/ToolConfigResolver.js +0 -229
  245. package/dist/services/catalog-media.d.ts +0 -56
  246. package/dist/services/catalog-media.js +0 -190
  247. package/dist/services/createDefaultToolRegistry.d.ts +0 -53
  248. package/dist/services/createDefaultToolRegistry.js +0 -45
  249. package/dist/services/defaultPersonalNeedsProfile.d.ts +0 -21
  250. package/dist/services/defaultPersonalNeedsProfile.js +0 -26
  251. package/dist/services/pnp-standard-features.d.ts +0 -238
  252. package/dist/services/pnp-standard-features.js +0 -338
  253. package/dist/services/tool-providers/CortexToolProvider.d.ts +0 -13
  254. package/dist/services/tool-providers/CortexToolProvider.js +0 -32
  255. package/dist/services/tool-providers/DesmosToolProvider.d.ts +0 -14
  256. package/dist/services/tool-providers/DesmosToolProvider.js +0 -32
  257. package/dist/services/tool-providers/GeoGebraToolProvider.d.ts +0 -16
  258. package/dist/services/tool-providers/GeoGebraToolProvider.js +0 -32
  259. package/dist/services/tool-providers/LazyCalculatorToolProvider.d.ts +0 -35
  260. package/dist/services/tool-providers/LazyCalculatorToolProvider.js +0 -95
  261. package/dist/services/tts/visual-line-ranges.d.ts +0 -18
  262. package/dist/services/tts/visual-line-ranges.js +0 -226
  263. package/dist/tools/client.d.ts +0 -12
  264. package/dist/tools/client.js +0 -14
  265. package/dist/tools/index.d.ts +0 -8
  266. package/dist/tools/index.js +0 -10
  267. package/dist/tools/internal.d.ts +0 -43
  268. package/dist/tools/internal.js +0 -41
  269. package/dist/tools/response-discovery.d.ts +0 -61
  270. package/dist/tools/response-discovery.js +0 -113
  271. package/dist/tools/types.d.ts +0 -337
  272. package/dist/tools/types.js +0 -12
  273. package/dist/tools/variant-resolver.d.ts +0 -41
  274. package/dist/tools/variant-resolver.js +0 -207
@@ -1,18 +1,18 @@
1
1
  /**
2
2
  * ToolCoordinator
3
3
  *
4
- * Manages z-index layering and visibility for floating tools.
5
- * Prevents conflicts between calculator, ruler, protractor, etc.
4
+ * Holds the visibility state of floating tools and stacks their elements.
6
5
  *
7
- * Features:
8
- * - Centralized z-index management with defined layers
9
- * - Tool visibility state tracking
10
- * - Bring-to-front on interaction
11
- * - Framework-agnostic (works with any DOM element)
6
+ * Visibility is state: the coordinator records it and notifies subscribers, and
7
+ * whoever renders a tool shows or hides it from that state. Stacking is the
8
+ * coordinator's alone: an element bound to a tool (`registerTool` with an
9
+ * element, or `updateToolElement`) takes a z-index in the tool's layer and comes
10
+ * to the front of that layer when shown or pressed.
12
11
  *
13
12
  * Part of PIE Assessment Toolkit.
14
13
  */
15
14
  import { createLogger } from "../utils/logger.js";
15
+ import { parseScopedToolId } from "./tool-instance-id.js";
16
16
  const log = createLogger("ToolCoordinator");
17
17
  /**
18
18
  * Z-index layers for assessment components
@@ -25,29 +25,23 @@ export var ZIndexLayer;
25
25
  ZIndexLayer[ZIndexLayer["CONTROL"] = 3000] = "CONTROL";
26
26
  ZIndexLayer[ZIndexLayer["HIGHLIGHT"] = 4000] = "HIGHLIGHT";
27
27
  })(ZIndexLayer || (ZIndexLayer = {}));
28
+ function matchesFilter(id, filter) {
29
+ if (!filter?.baseId)
30
+ return true;
31
+ return (parseScopedToolId(id)?.baseToolId ?? id) === filter.baseId;
32
+ }
33
+ function toToolState(entry) {
34
+ return {
35
+ id: entry.id,
36
+ name: entry.name,
37
+ isVisible: entry.isVisible,
38
+ element: entry.element,
39
+ layer: entry.layer,
40
+ };
41
+ }
28
42
  export class ToolCoordinator {
29
- config;
30
43
  tools = new Map();
31
- layerCounters = new Map();
32
44
  listeners = new Set();
33
- /**
34
- * Activation (on/off) state keyed by tool id, kept independent of the
35
- * element registration lifecycle. A tool's DOM element can be unregistered
36
- * and re-registered as the item re-renders (e.g. a model change re-mounts
37
- * the toolbar overlay); when that happens the on/off state must survive so
38
- * that only an explicit toggle — the toolbar button — turns a tool off.
39
- * Cleared on genuine teardown via {@link releaseTool}.
40
- */
41
- visibilityState = new Map();
42
- constructor(config = {}) {
43
- this.config = config;
44
- // Initialize layer counters
45
- this.layerCounters.set(ZIndexLayer.BASE, 0);
46
- this.layerCounters.set(ZIndexLayer.TOOL, 0);
47
- this.layerCounters.set(ZIndexLayer.MODAL, 0);
48
- this.layerCounters.set(ZIndexLayer.CONTROL, 0);
49
- this.layerCounters.set(ZIndexLayer.HIGHLIGHT, 0);
50
- }
51
45
  /**
52
46
  * Subscribe to tool state changes
53
47
  * Returns unsubscribe function
@@ -58,99 +52,89 @@ export class ToolCoordinator {
58
52
  this.listeners.delete(listener);
59
53
  };
60
54
  }
61
- /**
62
- * Notify all listeners of state change
63
- */
64
55
  notifyListeners() {
65
- this.listeners.forEach((listener) => listener());
56
+ for (const listener of this.listeners) {
57
+ try {
58
+ listener();
59
+ }
60
+ catch (error) {
61
+ console.warn("[ToolCoordinator] listener failed:", error);
62
+ }
63
+ }
66
64
  }
67
65
  /**
68
- * Register a tool with the coordinator
66
+ * Register a tool with the coordinator. Registering a known id keeps its
67
+ * on/off state and takes the name, the element when one is given, and the
68
+ * layer when none was declared before.
69
69
  *
70
70
  * @param id Unique tool identifier
71
71
  * @param name Display name
72
- * @param element DOM element for the tool (optional)
73
- * @param layer Z-index layer (defaults to MODAL)
72
+ * @param element DOM element to stack (optional)
73
+ * @param layer Z-index layer. Without one the tool stacks on TOOL until a
74
+ * registration names one.
74
75
  */
75
- registerTool(id, name, element, layer = ZIndexLayer.MODAL) {
76
+ registerTool(id, name, element, layer) {
76
77
  log("registerTool called:", { id, name, hasElement: !!element, layer });
77
- if (this.tools.has(id)) {
78
- log(`Tool ${id} is already registered`);
79
- return;
78
+ let entry = this.tools.get(id);
79
+ if (entry) {
80
+ entry.name = name;
81
+ const wasRegistered = entry.registered;
82
+ entry.registered = true;
83
+ if (layer !== undefined && !entry.layerDeclared) {
84
+ entry.layer = layer;
85
+ entry.layerDeclared = true;
86
+ this.stackOnTop(entry);
87
+ }
88
+ else if (!wasRegistered) {
89
+ this.stackOnTop(entry);
90
+ }
80
91
  }
81
- // Restore prior on/off state. A re-registration (e.g. after an item
82
- // re-render unmounts and re-mounts the tool element) must preserve the
83
- // tool's activation state so only an explicit toggle can turn it off.
84
- const isVisible = this.visibilityState.get(id) ?? false;
85
- // If no element provided, create a placeholder registration
86
- if (!element) {
87
- this.tools.set(id, {
92
+ else {
93
+ entry = {
88
94
  id,
89
95
  name,
96
+ registered: true,
90
97
  element: null,
91
- layer,
92
- isVisible,
93
- baseZIndex: layer,
94
- });
95
- log("Tool registered without element:", id);
96
- return;
98
+ layer: layer ?? ZIndexLayer.TOOL,
99
+ layerDeclared: layer !== undefined,
100
+ isVisible: false,
101
+ baseZIndex: 0,
102
+ };
103
+ this.tools.set(id, entry);
104
+ this.stackOnTop(entry);
97
105
  }
98
- // Calculate base z-index for this layer
99
- const baseZIndex = layer + this.getNextLayerOffset(layer);
100
- // Apply z-index
101
- element.style.zIndex = String(baseZIndex);
102
- // Reflect restored visibility onto the freshly registered element.
103
- element.style.display = isVisible ? "" : "none";
104
- // Create and store event handler to enable proper cleanup
105
- const mouseDownHandler = () => this.bringToFront(element);
106
- element.addEventListener("mousedown", mouseDownHandler);
107
- // Register tool with handler reference
108
- this.tools.set(id, {
109
- id,
110
- name,
111
- element,
112
- layer,
113
- isVisible,
114
- baseZIndex,
115
- mouseDownHandler,
116
- });
117
- if (isVisible) {
118
- this.bringToFront(element);
119
- }
120
- log("Tool registered with element:", id);
106
+ if (element)
107
+ this.updateToolElement(id, element);
121
108
  }
122
109
  /**
123
- * Unregister a tool
124
- *
125
- * Detaches the element binding (listeners, registration) but intentionally
126
- * preserves the tool's activation state in {@link visibilityState}, so a
127
- * subsequent re-registration of the same id (e.g. after an item re-render)
128
- * restores whether the tool was on or off. Use {@link releaseTool} to also
129
- * discard the activation state on genuine teardown.
110
+ * Unregister a tool's element binding, keeping its on/off state so a
111
+ * re-registration of the same id (an item re-render) restores it. Use
112
+ * {@link releaseTool} on genuine teardown.
130
113
  *
131
114
  * @param id Tool identifier
132
115
  */
133
116
  unregisterTool(id) {
134
- const tool = this.tools.get(id);
135
- if (!tool)
117
+ const entry = this.tools.get(id);
118
+ if (!entry?.registered)
136
119
  return;
137
- // Remove event listeners using stored handler reference
138
- if (tool.element && tool.mouseDownHandler) {
139
- tool.element.removeEventListener("mousedown", tool.mouseDownHandler);
140
- }
141
- this.tools.delete(id);
120
+ this.detachElement(entry);
121
+ entry.registered = false;
142
122
  }
143
123
  /**
144
- * Fully release a tool: unregister its element binding AND discard its
145
- * preserved activation state. Call this on genuine teardown (e.g. leaving
146
- * the item/section that owns the tool) rather than {@link unregisterTool},
147
- * which keeps the on/off state alive across element re-registration.
124
+ * Fully release a tool: unregister its element binding and discard its
125
+ * on/off state. Call this on genuine teardown (leaving the item or section
126
+ * that owns the tool).
148
127
  *
149
128
  * @param id Tool identifier
150
129
  */
151
130
  releaseTool(id) {
152
- this.unregisterTool(id);
153
- this.visibilityState.delete(id);
131
+ const entry = this.tools.get(id);
132
+ if (!entry)
133
+ return;
134
+ this.detachElement(entry);
135
+ this.tools.delete(id);
136
+ if (entry.isVisible)
137
+ this.notifyListeners();
154
138
  }
155
139
  /**
156
140
  * Show a tool
@@ -158,18 +142,14 @@ export class ToolCoordinator {
158
142
  * @param id Tool identifier
159
143
  */
160
144
  showTool(id) {
161
- const tool = this.tools.get(id);
162
- if (!tool) {
145
+ const entry = this.tools.get(id);
146
+ if (!entry) {
163
147
  log(`Tool ${id} not found`);
164
148
  return;
165
149
  }
166
- if (tool.element) {
167
- tool.element.style.display = "";
168
- this.bringToFront(tool.element);
169
- }
170
- tool.isVisible = true;
171
- this.visibilityState.set(id, true);
172
- this.notifyListeners();
150
+ if (entry.element)
151
+ this.bringToFront(entry.element);
152
+ this.setVisible(entry, true);
173
153
  }
174
154
  /**
175
155
  * Hide a tool
@@ -177,17 +157,12 @@ export class ToolCoordinator {
177
157
  * @param id Tool identifier
178
158
  */
179
159
  hideTool(id) {
180
- const tool = this.tools.get(id);
181
- if (!tool) {
160
+ const entry = this.tools.get(id);
161
+ if (!entry) {
182
162
  log(`Tool ${id} not found`);
183
163
  return;
184
164
  }
185
- if (tool.element) {
186
- tool.element.style.display = "none";
187
- }
188
- tool.isVisible = false;
189
- this.visibilityState.set(id, false);
190
- this.notifyListeners();
165
+ this.setVisible(entry, false);
191
166
  }
192
167
  /**
193
168
  * Toggle tool visibility
@@ -195,14 +170,12 @@ export class ToolCoordinator {
195
170
  * @param id Tool identifier
196
171
  */
197
172
  toggleTool(id) {
198
- log("toggleTool called for:", id);
199
- const tool = this.tools.get(id);
200
- if (!tool) {
201
- log(`Tool ${id} not found. Registered tools:`, Array.from(this.tools.keys()));
173
+ const entry = this.tools.get(id);
174
+ if (!entry) {
175
+ log(`Tool ${id} not found`);
202
176
  return;
203
177
  }
204
- log("Tool found, current visibility:", tool.isVisible);
205
- if (tool.isVisible) {
178
+ if (entry.isVisible) {
206
179
  this.hideTool(id);
207
180
  }
208
181
  else {
@@ -213,39 +186,29 @@ export class ToolCoordinator {
213
186
  * Check if tool is visible
214
187
  *
215
188
  * @param id Tool identifier
216
- * @returns true if tool is visible
217
189
  */
218
190
  isToolVisible(id) {
219
- const tool = this.tools.get(id);
220
- if (tool)
221
- return tool.isVisible;
222
- // No live element registration (e.g. mid re-render, between unmount and
223
- // re-mount): fall back to the preserved activation state so the tool
224
- // doesn't read as "off" during the gap.
225
- return this.visibilityState.get(id) ?? false;
191
+ return this.tools.get(id)?.isVisible ?? false;
226
192
  }
227
193
  /**
228
- * Bring element to front of its layer
194
+ * Bring a bound element to the front of its tool's layer. An element no tool
195
+ * is bound to is left alone.
229
196
  *
230
197
  * @param element DOM element to bring forward
231
198
  */
232
199
  bringToFront(element) {
233
- // Find tool registration
234
- const tool = Array.from(this.tools.values()).find((t) => t.element === element);
235
- if (!tool)
200
+ const entry = Array.from(this.tools.values()).find((t) => t.element === element);
201
+ if (!entry)
236
202
  return;
237
- // Calculate new z-index (highest in layer + 1)
238
- const maxZIndexInLayer = this.getMaxZIndexInLayer(tool.layer);
239
- const newZIndex = Math.max(maxZIndexInLayer + 1, tool.layer + 1);
240
- // Update z-index
241
- element.style.zIndex = String(newZIndex);
242
- tool.baseZIndex = newZIndex;
203
+ this.stackOnTop(entry);
243
204
  }
244
205
  /**
245
- * Get all registered tool IDs
206
+ * Ids of the tools an element registration currently holds.
246
207
  */
247
208
  getRegisteredTools() {
248
- return Array.from(this.tools.keys());
209
+ return Array.from(this.tools.values())
210
+ .filter((entry) => entry.registered)
211
+ .map((entry) => entry.id);
249
212
  }
250
213
  /**
251
214
  * Get tool element by ID
@@ -253,108 +216,101 @@ export class ToolCoordinator {
253
216
  getToolElement(id) {
254
217
  return this.tools.get(id)?.element ?? null;
255
218
  }
256
- /**
257
- * Get next offset within a layer
258
- */
259
- getNextLayerOffset(layer) {
260
- const current = this.layerCounters.get(layer) ?? 0;
261
- this.layerCounters.set(layer, current + 1);
262
- return current;
219
+ setVisible(entry, visible) {
220
+ if (entry.isVisible === visible)
221
+ return;
222
+ entry.isVisible = visible;
223
+ this.notifyListeners();
263
224
  }
264
- /**
265
- * Get maximum z-index currently in use in a layer
266
- */
267
- getMaxZIndexInLayer(layer) {
268
- let max = layer;
269
- for (const tool of this.tools.values()) {
270
- if (tool.layer === layer) {
271
- max = Math.max(max, tool.baseZIndex);
272
- }
225
+ detachElement(entry) {
226
+ if (entry.element && entry.mouseDownHandler) {
227
+ entry.element.removeEventListener("mousedown", entry.mouseDownHandler);
273
228
  }
274
- return max;
229
+ entry.element = null;
230
+ entry.mouseDownHandler = undefined;
275
231
  }
276
232
  /**
277
- * Reset all tools to their base z-indices
233
+ * Put `entry` in front of its layer. The layer is renumbered from its base on
234
+ * every move, so its z-indices stay within it however often tools are raised.
278
235
  */
279
- resetZIndices() {
280
- for (const tool of this.tools.values()) {
281
- if (tool.element) {
282
- tool.element.style.zIndex = String(tool.baseZIndex);
283
- }
236
+ stackOnTop(entry) {
237
+ const others = Array.from(this.tools.values())
238
+ .filter((other) => other.registered && other.layer === entry.layer && other !== entry)
239
+ .sort((a, b) => a.baseZIndex - b.baseZIndex);
240
+ const top = entry.layer + others.length + 1;
241
+ if (entry.baseZIndex === top &&
242
+ others.every((other) => other.baseZIndex < top)) {
243
+ return;
284
244
  }
245
+ [...others, entry].forEach((stacked, index) => {
246
+ stacked.baseZIndex = entry.layer + 1 + index;
247
+ if (stacked.element) {
248
+ stacked.element.style.zIndex = String(stacked.baseZIndex);
249
+ }
250
+ });
285
251
  }
286
252
  /**
287
- * Update tool element reference
253
+ * Bind the element a tool stacks by. It takes the tool's z-index and comes to
254
+ * the front of the layer when pressed, and when bound while the tool is shown.
255
+ *
256
+ * The outermost element stacks: binding an element inside the bound one keeps
257
+ * the binding, so a tool rendered inside a toolbar's floating window stacks
258
+ * by that window whichever binds first.
288
259
  *
289
260
  * @param id Tool identifier
290
261
  * @param element New DOM element
291
262
  */
292
263
  updateToolElement(id, element) {
293
- const tool = this.tools.get(id);
294
- if (!tool) {
264
+ const entry = this.tools.get(id);
265
+ if (!entry?.registered) {
295
266
  log(`Tool ${id} not found`);
296
267
  return;
297
268
  }
298
- // Remove old event listener using stored handler reference
299
- if (tool.element && tool.mouseDownHandler) {
300
- tool.element.removeEventListener("mousedown", tool.mouseDownHandler);
301
- }
302
- // Create new handler and add listener
269
+ if (entry.element === element)
270
+ return;
271
+ if (entry.element?.isConnected && entry.element.contains(element))
272
+ return;
273
+ this.detachElement(entry);
303
274
  const mouseDownHandler = () => this.bringToFront(element);
304
275
  element.addEventListener("mousedown", mouseDownHandler);
305
- // Update element and handler reference
306
- tool.element = element;
307
- tool.mouseDownHandler = mouseDownHandler;
308
- // Apply z-index to new element
309
- element.style.zIndex = String(tool.baseZIndex);
310
- if (tool.isVisible) {
311
- element.style.display = "";
312
- }
313
- else {
314
- element.style.display = "none";
315
- }
276
+ entry.element = element;
277
+ entry.mouseDownHandler = mouseDownHandler;
278
+ element.style.zIndex = String(entry.baseZIndex);
279
+ if (entry.isVisible)
280
+ this.bringToFront(element);
316
281
  }
317
282
  /**
318
- * Hide all tools
283
+ * Hide every visible tool, or every instance of one tool with
284
+ * `{ baseId }`, notifying subscribers once.
319
285
  */
320
- hideAllTools() {
321
- for (const id of this.tools.keys()) {
322
- this.hideTool(id);
286
+ hideAllTools(filter) {
287
+ let changed = false;
288
+ for (const entry of this.tools.values()) {
289
+ if (!entry.isVisible || !matchesFilter(entry.id, filter))
290
+ continue;
291
+ entry.isVisible = false;
292
+ changed = true;
323
293
  }
324
- this.notifyListeners();
294
+ if (changed)
295
+ this.notifyListeners();
325
296
  }
326
297
  /**
327
- * Get tool state (interface method)
298
+ * State of a tool an element registration holds.
328
299
  *
329
300
  * @param id Tool identifier
330
- * @returns Tool state or undefined
331
301
  */
332
302
  getToolState(id) {
333
- const tool = this.tools.get(id);
334
- if (!tool)
335
- return undefined;
336
- return {
337
- id: tool.id,
338
- name: tool.name,
339
- isVisible: tool.isVisible,
340
- element: tool.element ?? null,
341
- layer: tool.layer,
342
- };
303
+ const entry = this.tools.get(id);
304
+ return entry?.registered ? toToolState(entry) : undefined;
343
305
  }
344
306
  /**
345
- * Get all visible tools (interface method)
346
- *
347
- * @returns Array of visible tool states
307
+ * Every visible tool, or every visible instance of one tool with
308
+ * `{ baseId }`. A tool shown while its element re-renders counts, as
309
+ * {@link isToolVisible} counts it.
348
310
  */
349
- getVisibleTools() {
311
+ getVisibleTools(filter) {
350
312
  return Array.from(this.tools.values())
351
- .filter((tool) => tool.isVisible)
352
- .map((tool) => ({
353
- id: tool.id,
354
- name: tool.name,
355
- isVisible: tool.isVisible,
356
- element: tool.element ?? null,
357
- layer: tool.layer,
358
- }));
313
+ .filter((entry) => entry.isVisible && matchesFilter(entry.id, filter))
314
+ .map(toToolState);
359
315
  }
360
316
  }