@pie-players/pie-assessment-toolkit 0.3.74 → 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 (228) hide show
  1. package/README.md +349 -313
  2. package/dist/attempt/TestSession.d.ts +1 -20
  3. package/dist/attempt/TestSession.js +8 -99
  4. package/dist/components/ItemScope.custom-element.d.ts +1 -0
  5. package/dist/components/ItemScope.custom-element.js +1 -0
  6. package/dist/components/ItemToolBar.custom-element.js +1 -1
  7. package/dist/components/PieAssessmentToolkit.custom-element.js +14 -14
  8. package/dist/components/SectionToolBar.custom-element.js +1 -1
  9. package/dist/components/chunks/ItemToolBar-7tt7v2xp.js +4 -0
  10. package/dist/components/chunks/{ItemToolBar-gmrryv2v.js → ItemToolBar-97hx60z4.js} +1 -1
  11. package/dist/components/chunks/ItemToolBar-bxwyhr5p.js +2 -0
  12. package/dist/components/chunks/ItemToolBar-jx38w99s.js +2 -0
  13. package/dist/components/chunks/ItemToolBar-rd2bb4p6.js +40 -0
  14. package/dist/components/chunks/ItemToolBar-vdkyn9f3.js +2 -0
  15. package/dist/components/chunks/sre-engine-jd8cc08x.js +1 -0
  16. package/dist/components/item-scope-element.d.ts +2 -0
  17. package/dist/components/item-scope-element.js +2 -0
  18. package/dist/context/assessment-toolkit-context.d.ts +27 -6
  19. package/dist/index.d.ts +22 -47
  20. package/dist/index.js +12 -36
  21. package/dist/policy/core/PolicySource.d.ts +1 -1
  22. package/dist/policy/core/ToolPolicyEngine.d.ts +77 -32
  23. package/dist/policy/core/ToolPolicyEngine.js +136 -46
  24. package/dist/policy/core/compose-decision.d.ts +28 -7
  25. package/dist/policy/core/compose-decision.js +75 -54
  26. package/dist/policy/core/decision-types.d.ts +65 -34
  27. package/dist/policy/core/feature-decision.d.ts +16 -9
  28. package/dist/policy/core/feature-decision.js +14 -8
  29. package/dist/policy/core/pnp-policy-inputs.d.ts +24 -33
  30. package/dist/policy/core/pnp-policy-inputs.js +28 -56
  31. package/dist/policy/core/provenance.d.ts +9 -4
  32. package/dist/policy/core/provenance.js +1 -1
  33. package/dist/policy/engine.d.ts +7 -23
  34. package/dist/policy/engine.js +5 -20
  35. package/dist/policy/sources/PnpPolicySource.d.ts +44 -61
  36. package/dist/policy/sources/PnpPolicySource.js +117 -118
  37. package/dist/runtime/SectionControllerBinding.d.ts +279 -0
  38. package/dist/runtime/SectionControllerBinding.js +450 -0
  39. package/dist/runtime/SectionRuntimeEngine.d.ts +26 -289
  40. package/dist/runtime/SectionRuntimeEngine.js +30 -372
  41. package/dist/runtime/adapter/SectionEngineAdapter.d.ts +19 -80
  42. package/dist/runtime/adapter/SectionEngineAdapter.js +19 -92
  43. package/dist/runtime/adapter/dom-event-bridge.d.ts +15 -30
  44. package/dist/runtime/adapter/dom-event-bridge.js +17 -44
  45. package/dist/runtime/adapter/subscriber-fanout.d.ts +1 -2
  46. package/dist/runtime/adapter/subscriber-fanout.js +1 -2
  47. package/dist/runtime/catalog-context.d.ts +19 -0
  48. package/dist/runtime/catalog-context.js +28 -0
  49. package/dist/runtime/composition-emit-scheduler.d.ts +9 -15
  50. package/dist/runtime/composition-emit-scheduler.js +9 -15
  51. package/dist/runtime/content-language.d.ts +32 -0
  52. package/dist/runtime/content-language.js +46 -0
  53. package/dist/runtime/content-region.d.ts +17 -0
  54. package/dist/runtime/content-region.js +29 -0
  55. package/dist/runtime/content-registration.d.ts +13 -0
  56. package/dist/runtime/content-registration.js +25 -0
  57. package/dist/runtime/core/SectionEngineCore.d.ts +2 -2
  58. package/dist/runtime/core/SectionEngineCore.js +2 -2
  59. package/dist/runtime/core/cohort.d.ts +1 -1
  60. package/dist/runtime/core/cohort.js +1 -1
  61. package/dist/runtime/core/engine-input.d.ts +18 -30
  62. package/dist/runtime/core/engine-input.js +11 -12
  63. package/dist/runtime/core/engine-output.d.ts +16 -40
  64. package/dist/runtime/core/engine-output.js +9 -21
  65. package/dist/runtime/core/engine-readiness.d.ts +5 -37
  66. package/dist/runtime/core/engine-readiness.js +8 -41
  67. package/dist/runtime/core/engine-resolver.d.ts +46 -74
  68. package/dist/runtime/core/engine-resolver.js +18 -43
  69. package/dist/runtime/core/engine-state.d.ts +28 -36
  70. package/dist/runtime/core/engine-state.js +18 -21
  71. package/dist/runtime/core/engine-transition.d.ts +14 -19
  72. package/dist/runtime/core/engine-transition.js +71 -51
  73. package/dist/runtime/engine.d.ts +16 -19
  74. package/dist/runtime/engine.js +12 -19
  75. package/dist/runtime/page-warnings.d.ts +13 -0
  76. package/dist/runtime/page-warnings.js +22 -0
  77. package/dist/runtime/policy-input-forwarding.d.ts +25 -0
  78. package/dist/runtime/policy-input-forwarding.js +25 -0
  79. package/dist/runtime/registration-events.d.ts +11 -0
  80. package/dist/runtime/runtime-event-claim.d.ts +8 -0
  81. package/dist/runtime/runtime-event-claim.js +15 -0
  82. package/dist/runtime/shell-event-bridge.d.ts +45 -0
  83. package/dist/runtime/shell-event-bridge.js +127 -0
  84. package/dist/runtime/shell-registration.d.ts +73 -0
  85. package/dist/runtime/shell-registration.js +95 -0
  86. package/dist/runtime/shell-scope.d.ts +68 -0
  87. package/dist/runtime/shell-scope.js +188 -0
  88. package/dist/runtime/tool-host-contract.d.ts +8 -10
  89. package/dist/runtime/tool-host-contract.js +11 -11
  90. package/dist/runtime/unclaimed-registration-watch.d.ts +7 -0
  91. package/dist/runtime/unclaimed-registration-watch.js +23 -0
  92. package/dist/services/ElementToolStateStore.d.ts +14 -10
  93. package/dist/services/ElementToolStateStore.js +37 -13
  94. package/dist/services/HighlightCoordinator.d.ts +21 -21
  95. package/dist/services/HighlightCoordinator.js +431 -325
  96. package/dist/services/RangeSerializer.d.ts +10 -1
  97. package/dist/services/RangeSerializer.js +87 -14
  98. package/dist/services/TTSService.d.ts +117 -70
  99. package/dist/services/TTSService.js +668 -578
  100. package/dist/services/ToolCoordinator.d.ts +43 -60
  101. package/dist/services/ToolCoordinator.js +164 -206
  102. package/dist/services/ToolRegistry.d.ts +74 -57
  103. package/dist/services/ToolRegistry.js +104 -114
  104. package/dist/services/ToolkitCoordinator.d.ts +183 -189
  105. package/dist/services/ToolkitCoordinator.js +477 -431
  106. package/dist/services/audio-handoff.d.ts +6 -7
  107. package/dist/services/audio-handoff.js +20 -14
  108. package/dist/services/catalog-owner.d.ts +0 -1
  109. package/dist/services/framework-error-bus.d.ts +3 -6
  110. package/dist/services/framework-error-bus.js +3 -6
  111. package/dist/services/framework-error.d.ts +22 -7
  112. package/dist/services/framework-error.js +9 -7
  113. package/dist/services/interfaces.d.ts +158 -129
  114. package/dist/services/section-session-assignment.d.ts +21 -0
  115. package/dist/services/section-session-assignment.js +82 -0
  116. package/dist/services/spoken-audio-cards.d.ts +0 -2
  117. package/dist/services/spoken-audio-cards.js +0 -2
  118. package/dist/services/tool-config-defaults.d.ts +3 -3
  119. package/dist/services/tool-config-defaults.js +3 -3
  120. package/dist/services/tool-config-validation.d.ts +4 -5
  121. package/dist/services/tool-config-validation.js +36 -61
  122. package/dist/services/tool-context.d.ts +0 -8
  123. package/dist/services/tool-context.js +0 -12
  124. package/dist/services/tool-icons.d.ts +4 -3
  125. package/dist/services/tool-icons.js +4 -3
  126. package/dist/services/tool-instance-id.d.ts +0 -1
  127. package/dist/services/tool-instance-id.js +0 -6
  128. package/dist/services/tool-providers/TTSToolProvider.d.ts +18 -19
  129. package/dist/services/tool-providers/TTSToolProvider.js +37 -34
  130. package/dist/services/tool-providers/ToolProviderApi.d.ts +10 -4
  131. package/dist/services/tool-providers/ToolProviderRegistry.d.ts +30 -26
  132. package/dist/services/tool-providers/ToolProviderRegistry.js +119 -93
  133. package/dist/services/tool-request.d.ts +27 -12
  134. package/dist/services/tool-request.js +24 -14
  135. package/dist/services/toolbar-decided-tools.d.ts +15 -0
  136. package/dist/services/toolbar-decided-tools.js +28 -0
  137. package/dist/services/tools-config-normalizer.d.ts +17 -22
  138. package/dist/services/tools-config-normalizer.js +2 -10
  139. package/dist/services/tts/browser-provider.d.ts +38 -2
  140. package/dist/services/tts/browser-provider.js +276 -140
  141. package/dist/services/tts/flat-tree.d.ts +71 -0
  142. package/dist/services/tts/flat-tree.js +269 -0
  143. package/dist/services/tts/generated-speech/assemble-plan.d.ts +3 -1
  144. package/dist/services/tts/generated-speech/assemble-plan.js +3 -1
  145. package/dist/services/tts/generated-speech/dom/build-dom-plan.d.ts +2 -0
  146. package/dist/services/tts/generated-speech/dom/build-dom-plan.js +1 -1
  147. package/dist/services/tts/generated-speech/math-speech-cache.d.ts +3 -1
  148. package/dist/services/tts/generated-speech/math-speech-cache.js +5 -1
  149. package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +12 -4
  150. package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js +6 -4
  151. package/dist/services/tts/highlight-pipeline/readable-region.js +3 -2
  152. package/dist/services/tts/highlight-pipeline/types.d.ts +5 -0
  153. package/dist/services/tts/highlight-pipeline/visible-map-range.d.ts +12 -0
  154. package/dist/services/tts/highlight-pipeline/visible-map-range.js +73 -7
  155. package/dist/services/tts/math-aware-text-processing.d.ts +5 -1
  156. package/dist/services/tts/math-aware-text-processing.js +32 -18
  157. package/dist/services/tts/math-control-names.d.ts +21 -0
  158. package/dist/services/tts/math-control-names.js +142 -0
  159. package/dist/services/tts/math-speech.d.ts +11 -0
  160. package/dist/services/tts/math-speech.js +51 -18
  161. package/dist/services/tts/provider-options.d.ts +41 -0
  162. package/dist/services/tts/provider-options.js +1 -0
  163. package/dist/services/tts/ssml/spoken-text.d.ts +2 -26
  164. package/dist/services/tts/ssml/spoken-text.js +29 -0
  165. package/dist/services/tts/start-failure.d.ts +11 -0
  166. package/dist/services/tts/start-failure.js +19 -0
  167. package/dist/services/tts/text-processing.d.ts +9 -0
  168. package/dist/services/tts/text-processing.js +46 -33
  169. package/dist/services/tts/text-segmentation.d.ts +7 -0
  170. package/dist/services/tts/text-segmentation.js +47 -0
  171. package/dist/services/tts-runtime-config.d.ts +25 -17
  172. package/dist/services/tts-runtime-config.js +16 -95
  173. package/dist/tools/content-capability-resolution.d.ts +7 -9
  174. package/dist/tools/content-capability-resolution.js +8 -45
  175. package/dist/tools/registration.d.ts +58 -0
  176. package/dist/tools/registration.js +54 -0
  177. package/dist/tools/registrations/toolbar-registration-helpers.js +2 -2
  178. package/dist/tools/tool-surface-host.d.ts +6 -10
  179. package/dist/tools/tool-surface-host.js +44 -42
  180. package/dist/tools/tool-tag-map.d.ts +5 -15
  181. package/dist/tools/tool-tag-map.js +6 -20
  182. package/dist/utils/structural-equality.d.ts +11 -0
  183. package/dist/utils/structural-equality.js +47 -0
  184. package/package.json +13 -29
  185. package/dist/attempt/adapters/activity-to-test-attempt-session.d.ts +0 -49
  186. package/dist/attempt/adapters/activity-to-test-attempt-session.js +0 -138
  187. package/dist/components/chunks/ItemToolBar-b9dz5jvd.js +0 -40
  188. package/dist/components/chunks/ItemToolBar-h4xap0t0.js +0 -4
  189. package/dist/components/chunks/ItemToolBar-wr2myaah.js +0 -2
  190. package/dist/components/chunks/sre-engine-wf9s43gg.js +0 -1
  191. package/dist/context/runtime-context-consumer.d.ts +0 -13
  192. package/dist/context/runtime-context-consumer.js +0 -18
  193. package/dist/item-loader.d.ts +0 -44
  194. package/dist/item-loader.js +0 -61
  195. package/dist/policy/core/engine-context.d.ts +0 -29
  196. package/dist/policy/core/engine-context.js +0 -27
  197. package/dist/policy/core/policy-source-tag.d.ts +0 -31
  198. package/dist/policy/core/policy-source-tag.js +0 -28
  199. package/dist/policy/internal.d.ts +0 -25
  200. package/dist/policy/internal.js +0 -25
  201. package/dist/runtime/adapter/coordinator-bridge.d.ts +0 -92
  202. package/dist/runtime/adapter/coordinator-bridge.js +0 -126
  203. package/dist/runtime/adapter/framework-error-bridge.d.ts +0 -36
  204. package/dist/runtime/adapter/framework-error-bridge.js +0 -46
  205. package/dist/runtime/adapter/instrumentation-bridge.d.ts +0 -31
  206. package/dist/runtime/adapter/instrumentation-bridge.js +0 -38
  207. package/dist/runtime/core/engine-stage-derivation.d.ts +0 -18
  208. package/dist/runtime/core/engine-stage-derivation.js +0 -29
  209. package/dist/runtime/engine-context.d.ts +0 -39
  210. package/dist/runtime/engine-context.js +0 -31
  211. package/dist/runtime/internal.d.ts +0 -43
  212. package/dist/runtime/internal.js +0 -45
  213. package/dist/runtime/section-runtime-engine-host-context.d.ts +0 -74
  214. package/dist/runtime/section-runtime-engine-host-context.js +0 -57
  215. package/dist/runtime/stage-emit-gate.d.ts +0 -92
  216. package/dist/runtime/stage-emit-gate.js +0 -82
  217. package/dist/services/I18nService.d.ts +0 -47
  218. package/dist/services/I18nService.js +0 -84
  219. package/dist/services/ThemeProvider.d.ts +0 -95
  220. package/dist/services/ThemeProvider.js +0 -334
  221. package/dist/services/defaultPersonalNeedsProfile.d.ts +0 -21
  222. package/dist/services/defaultPersonalNeedsProfile.js +0 -26
  223. package/dist/services/pnp-standard-features.d.ts +0 -238
  224. package/dist/services/pnp-standard-features.js +0 -338
  225. package/dist/tools/client.d.ts +0 -10
  226. package/dist/tools/client.js +0 -10
  227. package/dist/tools/internal.d.ts +0 -44
  228. package/dist/tools/internal.js +0 -39
@@ -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,27 +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
43
  tools = new Map();
30
- layerCounters = new Map();
31
44
  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
45
  /**
50
46
  * Subscribe to tool state changes
51
47
  * Returns unsubscribe function
@@ -56,99 +52,89 @@ export class ToolCoordinator {
56
52
  this.listeners.delete(listener);
57
53
  };
58
54
  }
59
- /**
60
- * Notify all listeners of state change
61
- */
62
55
  notifyListeners() {
63
- 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
+ }
64
64
  }
65
65
  /**
66
- * 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.
67
69
  *
68
70
  * @param id Unique tool identifier
69
71
  * @param name Display name
70
- * @param element DOM element for the tool (optional)
71
- * @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.
72
75
  */
73
- registerTool(id, name, element, layer = ZIndexLayer.MODAL) {
76
+ registerTool(id, name, element, layer) {
74
77
  log("registerTool called:", { id, name, hasElement: !!element, layer });
75
- if (this.tools.has(id)) {
76
- log(`Tool ${id} is already registered`);
77
- 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
+ }
78
91
  }
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, {
92
+ else {
93
+ entry = {
86
94
  id,
87
95
  name,
96
+ registered: true,
88
97
  element: null,
89
- layer,
90
- isVisible,
91
- baseZIndex: layer,
92
- });
93
- log("Tool registered without element:", id);
94
- 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);
95
105
  }
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, {
107
- id,
108
- name,
109
- element,
110
- layer,
111
- isVisible,
112
- baseZIndex,
113
- mouseDownHandler,
114
- });
115
- if (isVisible) {
116
- this.bringToFront(element);
117
- }
118
- log("Tool registered with element:", id);
106
+ if (element)
107
+ this.updateToolElement(id, element);
119
108
  }
120
109
  /**
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.
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.
128
113
  *
129
114
  * @param id Tool identifier
130
115
  */
131
116
  unregisterTool(id) {
132
- const tool = this.tools.get(id);
133
- if (!tool)
117
+ const entry = this.tools.get(id);
118
+ if (!entry?.registered)
134
119
  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);
120
+ this.detachElement(entry);
121
+ entry.registered = false;
140
122
  }
141
123
  /**
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.
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).
146
127
  *
147
128
  * @param id Tool identifier
148
129
  */
149
130
  releaseTool(id) {
150
- this.unregisterTool(id);
151
- 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();
152
138
  }
153
139
  /**
154
140
  * Show a tool
@@ -156,18 +142,14 @@ export class ToolCoordinator {
156
142
  * @param id Tool identifier
157
143
  */
158
144
  showTool(id) {
159
- const tool = this.tools.get(id);
160
- if (!tool) {
145
+ const entry = this.tools.get(id);
146
+ if (!entry) {
161
147
  log(`Tool ${id} not found`);
162
148
  return;
163
149
  }
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();
150
+ if (entry.element)
151
+ this.bringToFront(entry.element);
152
+ this.setVisible(entry, true);
171
153
  }
172
154
  /**
173
155
  * Hide a tool
@@ -175,17 +157,12 @@ export class ToolCoordinator {
175
157
  * @param id Tool identifier
176
158
  */
177
159
  hideTool(id) {
178
- const tool = this.tools.get(id);
179
- if (!tool) {
160
+ const entry = this.tools.get(id);
161
+ if (!entry) {
180
162
  log(`Tool ${id} not found`);
181
163
  return;
182
164
  }
183
- if (tool.element) {
184
- tool.element.style.display = "none";
185
- }
186
- tool.isVisible = false;
187
- this.visibilityState.set(id, false);
188
- this.notifyListeners();
165
+ this.setVisible(entry, false);
189
166
  }
190
167
  /**
191
168
  * Toggle tool visibility
@@ -193,14 +170,12 @@ export class ToolCoordinator {
193
170
  * @param id Tool identifier
194
171
  */
195
172
  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()));
173
+ const entry = this.tools.get(id);
174
+ if (!entry) {
175
+ log(`Tool ${id} not found`);
200
176
  return;
201
177
  }
202
- log("Tool found, current visibility:", tool.isVisible);
203
- if (tool.isVisible) {
178
+ if (entry.isVisible) {
204
179
  this.hideTool(id);
205
180
  }
206
181
  else {
@@ -211,39 +186,29 @@ export class ToolCoordinator {
211
186
  * Check if tool is visible
212
187
  *
213
188
  * @param id Tool identifier
214
- * @returns true if tool is visible
215
189
  */
216
190
  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;
191
+ return this.tools.get(id)?.isVisible ?? false;
224
192
  }
225
193
  /**
226
- * 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.
227
196
  *
228
197
  * @param element DOM element to bring forward
229
198
  */
230
199
  bringToFront(element) {
231
- // Find tool registration
232
- const tool = Array.from(this.tools.values()).find((t) => t.element === element);
233
- if (!tool)
200
+ const entry = Array.from(this.tools.values()).find((t) => t.element === element);
201
+ if (!entry)
234
202
  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;
203
+ this.stackOnTop(entry);
241
204
  }
242
205
  /**
243
- * Get all registered tool IDs
206
+ * Ids of the tools an element registration currently holds.
244
207
  */
245
208
  getRegisteredTools() {
246
- return Array.from(this.tools.keys());
209
+ return Array.from(this.tools.values())
210
+ .filter((entry) => entry.registered)
211
+ .map((entry) => entry.id);
247
212
  }
248
213
  /**
249
214
  * Get tool element by ID
@@ -251,108 +216,101 @@ export class ToolCoordinator {
251
216
  getToolElement(id) {
252
217
  return this.tools.get(id)?.element ?? null;
253
218
  }
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;
219
+ setVisible(entry, visible) {
220
+ if (entry.isVisible === visible)
221
+ return;
222
+ entry.isVisible = visible;
223
+ this.notifyListeners();
261
224
  }
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
- }
225
+ detachElement(entry) {
226
+ if (entry.element && entry.mouseDownHandler) {
227
+ entry.element.removeEventListener("mousedown", entry.mouseDownHandler);
271
228
  }
272
- return max;
229
+ entry.element = null;
230
+ entry.mouseDownHandler = undefined;
273
231
  }
274
232
  /**
275
- * 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.
276
235
  */
277
- resetZIndices() {
278
- for (const tool of this.tools.values()) {
279
- if (tool.element) {
280
- tool.element.style.zIndex = String(tool.baseZIndex);
281
- }
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;
282
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
+ });
283
251
  }
284
252
  /**
285
- * 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.
286
259
  *
287
260
  * @param id Tool identifier
288
261
  * @param element New DOM element
289
262
  */
290
263
  updateToolElement(id, element) {
291
- const tool = this.tools.get(id);
292
- if (!tool) {
264
+ const entry = this.tools.get(id);
265
+ if (!entry?.registered) {
293
266
  log(`Tool ${id} not found`);
294
267
  return;
295
268
  }
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
269
+ if (entry.element === element)
270
+ return;
271
+ if (entry.element?.isConnected && entry.element.contains(element))
272
+ return;
273
+ this.detachElement(entry);
301
274
  const mouseDownHandler = () => this.bringToFront(element);
302
275
  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
- }
276
+ entry.element = element;
277
+ entry.mouseDownHandler = mouseDownHandler;
278
+ element.style.zIndex = String(entry.baseZIndex);
279
+ if (entry.isVisible)
280
+ this.bringToFront(element);
314
281
  }
315
282
  /**
316
- * Hide all tools
283
+ * Hide every visible tool, or every instance of one tool with
284
+ * `{ baseId }`, notifying subscribers once.
317
285
  */
318
- hideAllTools() {
319
- for (const id of this.tools.keys()) {
320
- 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;
321
293
  }
322
- this.notifyListeners();
294
+ if (changed)
295
+ this.notifyListeners();
323
296
  }
324
297
  /**
325
- * Get tool state (interface method)
298
+ * State of a tool an element registration holds.
326
299
  *
327
300
  * @param id Tool identifier
328
- * @returns Tool state or undefined
329
301
  */
330
302
  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
- };
303
+ const entry = this.tools.get(id);
304
+ return entry?.registered ? toToolState(entry) : undefined;
341
305
  }
342
306
  /**
343
- * Get all visible tools (interface method)
344
- *
345
- * @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.
346
310
  */
347
- getVisibleTools() {
311
+ getVisibleTools(filter) {
348
312
  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
- }));
313
+ .filter((entry) => entry.isVisible && matchesFilter(entry.id, filter))
314
+ .map(toToolState);
357
315
  }
358
316
  }