@pie-players/pie-assessment-toolkit 0.3.74 → 1.0.0-beta.0

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