@pie-players/pie-assessment-toolkit 0.3.67 → 0.3.69

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 (73) hide show
  1. package/README.md +81 -9
  2. package/dist/attempt/AssessmentSession.d.ts +7 -27
  3. package/dist/components/ItemToolBar.custom-element.js +1 -1
  4. package/dist/components/PieAssessmentToolkit.custom-element.js +14 -14
  5. package/dist/components/SectionToolBar.custom-element.js +1 -1
  6. package/dist/components/chunks/ItemToolBar-pe5szfyx.js +46 -0
  7. package/dist/components/chunks/ItemToolBar-rd7te9r0.js +51 -0
  8. package/dist/components/item-toolbar-element.js +1 -0
  9. package/dist/components/pie-assessment-toolkit-element.js +1 -0
  10. package/dist/components/section-toolbar-element.js +1 -0
  11. package/dist/context/assessment-toolkit-context.d.ts +28 -0
  12. package/dist/index.d.ts +7 -3
  13. package/dist/index.js +4 -2
  14. package/dist/policy/core/compose-decision.js +14 -0
  15. package/dist/policy/core/decision-types.d.ts +1 -1
  16. package/dist/policy/sources/PnpPolicySource.d.ts +8 -0
  17. package/dist/policy/sources/PnpPolicySource.js +19 -10
  18. package/dist/runtime/SectionRuntimeEngine.d.ts +56 -0
  19. package/dist/runtime/SectionRuntimeEngine.js +66 -1
  20. package/dist/runtime/core/engine-resolver.d.ts +25 -1
  21. package/dist/runtime/registration-events.d.ts +52 -0
  22. package/dist/runtime/registration-events.js +2 -0
  23. package/dist/services/AccessibilityCatalogResolver.js +21 -5
  24. package/dist/services/I18nService.d.ts +28 -100
  25. package/dist/services/I18nService.js +43 -233
  26. package/dist/services/TTSService.d.ts +12 -0
  27. package/dist/services/TTSService.js +18 -17
  28. package/dist/services/ToolRegistry.d.ts +120 -2
  29. package/dist/services/ToolRegistry.js +64 -0
  30. package/dist/services/ToolkitCoordinator.d.ts +39 -37
  31. package/dist/services/ToolkitCoordinator.js +40 -0
  32. package/dist/services/audio-handoff.d.ts +39 -0
  33. package/dist/services/audio-handoff.js +58 -0
  34. package/dist/services/catalog-media.d.ts +35 -4
  35. package/dist/services/catalog-media.js +92 -3
  36. package/dist/services/framework-error.d.ts +15 -1
  37. package/dist/services/interfaces.d.ts +28 -0
  38. package/dist/services/pnp-standard-features.d.ts +1 -1
  39. package/dist/services/section-controller-types.d.ts +218 -7
  40. package/dist/services/selection-action.d.ts +49 -0
  41. package/dist/services/selection-action.js +10 -0
  42. package/dist/services/spoken-audio-cards.js +5 -1
  43. package/dist/services/tool-context.d.ts +6 -5
  44. package/dist/services/tool-context.js +205 -155
  45. package/dist/services/tool-icons.d.ts +18 -0
  46. package/dist/services/tool-icons.js +31 -0
  47. package/dist/services/tool-providers/CortexToolProvider.d.ts +18 -0
  48. package/dist/services/tool-providers/CortexToolProvider.js +32 -0
  49. package/dist/services/tool-providers/DesmosToolProvider.d.ts +13 -101
  50. package/dist/services/tool-providers/DesmosToolProvider.js +14 -145
  51. package/dist/services/tool-providers/GeoGebraToolProvider.d.ts +21 -0
  52. package/dist/services/tool-providers/GeoGebraToolProvider.js +32 -0
  53. package/dist/services/tool-providers/LazyCalculatorToolProvider.d.ts +35 -0
  54. package/dist/services/tool-providers/LazyCalculatorToolProvider.js +95 -0
  55. package/dist/services/tool-providers/index.d.ts +4 -0
  56. package/dist/services/tool-providers/index.js +2 -0
  57. package/dist/services/tool-request.d.ts +106 -0
  58. package/dist/services/tool-request.js +127 -0
  59. package/dist/services/toolbar-items.d.ts +6 -0
  60. package/dist/tools/client.d.ts +0 -2
  61. package/dist/tools/client.js +0 -4
  62. package/dist/tools/internal.d.ts +7 -1
  63. package/dist/tools/internal.js +8 -1
  64. package/dist/tools/tool-surface-host.d.ts +57 -0
  65. package/dist/tools/tool-surface-host.js +610 -0
  66. package/dist/tools/types.d.ts +1 -66
  67. package/package.json +22 -12
  68. package/dist/components/chunks/ItemToolBar-8jgdz50p.js +0 -51
  69. package/dist/components/chunks/ItemToolBar-cvs646j3.js +0 -36
  70. package/dist/tools/calculators/desmos-provider.d.ts +0 -46
  71. package/dist/tools/calculators/desmos-provider.js +0 -393
  72. package/dist/tools/library-loader.d.ts +0 -62
  73. package/dist/tools/library-loader.js +0 -261
@@ -1,136 +1,26 @@
1
- /**
2
- * Desmos Calculator Tool Provider
3
- *
4
- * Provides Desmos calculators (basic, scientific, graphing)
5
- * with authentication and proxy support.
6
- *
7
- * SECURITY BEST PRACTICE:
8
- * - Development: Pass apiKey directly for local testing
9
- * - Production: Use proxyEndpoint or authFetcher to keep API key server-side
10
- *
11
- * Part of PIE Assessment Toolkit.
12
- */
13
- /**
14
- * Desmos Calculator Tool Provider
15
- *
16
- * Wraps DesmosCalculatorProvider with the ToolProviderApi interface
17
- * for use in the ToolProviderRegistry.
18
- *
19
- * @example
20
- * ```typescript
21
- * const provider = new DesmosToolProvider();
22
- *
23
- * await provider.initialize({
24
- * apiKey: 'your-api-key', // Development only
25
- * proxyEndpoint: '/api/desmos/token', // Production
26
- * });
27
- *
28
- * const calculatorProvider = await provider.createInstance();
29
- * ```
30
- */
31
- export class DesmosToolProvider {
1
+ /** Desmos calculator adapter for the generic tool-provider registry. */
2
+ import { LazyCalculatorToolProvider } from "./LazyCalculatorToolProvider.js";
3
+ export class DesmosToolProvider extends LazyCalculatorToolProvider {
32
4
  providerId = "desmos-calculator";
33
5
  providerName = "Desmos Calculator";
34
- category = "calculator";
35
6
  version = "1.12";
36
7
  requiresAuth = true;
37
- desmosProvider = null;
38
- config = null;
39
- async emitTelemetry(eventName, payload) {
40
- try {
41
- await this.config?.onTelemetry?.(eventName, payload);
42
- }
43
- catch (error) {
44
- console.warn("[DesmosToolProvider] telemetry callback failed:", error);
45
- }
46
- }
47
- /**
48
- * Initialize Desmos calculator provider
49
- *
50
- * Loads the Desmos API library and authenticates with provided credentials.
51
- *
52
- * @param config Configuration with API key or proxy endpoint
53
- * @throws Error if initialization fails
54
- */
55
- async initialize(config) {
56
- if (this.desmosProvider) {
57
- console.warn("[DesmosToolProvider] Already initialized, skipping reinitialization");
58
- return;
59
- }
60
- this.config = config;
61
- const moduleLoadStartedAt = Date.now();
62
- await this.emitTelemetry("pie-tool-library-load-start", {
63
- toolId: "calculator",
64
- operation: "desmos-provider-module-import",
8
+ getDefinition() {
9
+ return {
65
10
  backend: "desmos",
66
- });
67
- const desmosModule = await (async () => {
68
- try {
69
- const loaded = (await import("@pie-players/pie-calculator-desmos"));
70
- await this.emitTelemetry("pie-tool-library-load-success", {
71
- toolId: "calculator",
72
- operation: "desmos-provider-module-import",
73
- backend: "desmos",
74
- duration: Date.now() - moduleLoadStartedAt,
75
- });
76
- return loaded;
77
- }
78
- catch (error) {
79
- await this.emitTelemetry("pie-tool-library-load-error", {
80
- toolId: "calculator",
81
- operation: "desmos-provider-module-import",
82
- backend: "desmos",
83
- duration: Date.now() - moduleLoadStartedAt,
84
- errorType: "ToolLibraryLoadError",
85
- message: error instanceof Error ? error.message : String(error),
86
- });
87
- throw error;
88
- }
89
- })();
90
- this.desmosProvider = new desmosModule.DesmosCalculatorProvider();
91
- // Initialize with API key or proxy
92
- try {
93
- await this.desmosProvider.initialize({
94
- apiKey: config.apiKey,
95
- proxyEndpoint: config.proxyEndpoint,
96
- onTelemetry: config.onTelemetry,
97
- });
98
- console.log(`[DesmosToolProvider] Initialized successfully ${config.proxyEndpoint
99
- ? "(using proxy)"
100
- : config.apiKey
101
- ? "(direct API key)"
102
- : "(no auth)"}`);
103
- }
104
- catch (error) {
105
- console.error("[DesmosToolProvider] Initialization failed:", error);
106
- throw new Error("Failed to initialize Desmos calculator provider. Check API key or proxy endpoint.");
107
- }
108
- }
109
- /**
110
- * Create a calculator provider instance
111
- *
112
- * Returns the initialized Desmos calculator provider.
113
- *
114
- * @param config Optional instance-specific configuration (currently unused)
115
- * @returns Desmos calculator provider
116
- * @throws Error if provider not initialized
117
- */
118
- async createInstance(config) {
119
- if (!this.desmosProvider) {
120
- throw new Error("[DesmosToolProvider] Provider not initialized. Call initialize() first.");
121
- }
122
- return this.desmosProvider;
11
+ moduleImportOperation: "desmos-provider-module-import",
12
+ loadProvider: async () => {
13
+ const module = await import("@pie-players/pie-calculator-desmos");
14
+ return module.DesmosCalculatorProvider;
15
+ },
16
+ initializationErrorMessage: "Failed to initialize Desmos calculator provider. Check the application key, preloaded API, runtime endpoint, or network access.",
17
+ };
123
18
  }
124
- /**
125
- * Get provider capabilities
126
- *
127
- * @returns Desmos calculator capabilities
128
- */
129
19
  getCapabilities() {
130
20
  return {
131
- supportsOffline: false, // Requires Desmos CDN
21
+ supportsOffline: false,
132
22
  requiresAuth: true,
133
- maxInstances: null, // Unlimited calculator instances
23
+ maxInstances: null,
134
24
  features: {
135
25
  basic: true,
136
26
  scientific: true,
@@ -139,25 +29,4 @@ export class DesmosToolProvider {
139
29
  },
140
30
  };
141
31
  }
142
- /**
143
- * Check if provider is ready
144
- *
145
- * @returns true if provider is initialized
146
- */
147
- isReady() {
148
- return this.desmosProvider !== null;
149
- }
150
- /**
151
- * Clean up provider resources
152
- *
153
- * Destroys the Desmos calculator provider and releases resources.
154
- */
155
- destroy() {
156
- if (this.desmosProvider) {
157
- this.desmosProvider.destroy();
158
- this.desmosProvider = null;
159
- }
160
- this.config = null;
161
- console.log("[DesmosToolProvider] Destroyed");
162
- }
163
32
  }
@@ -0,0 +1,21 @@
1
+ /** GeoGebra calculator adapter for the generic tool-provider registry. */
2
+ import type { CalculatorToolProviderInitConfig } from "./LazyCalculatorToolProvider.js";
3
+ import { LazyCalculatorToolProvider } from "./LazyCalculatorToolProvider.js";
4
+ import type { ToolProviderCapabilities } from "./ToolProviderApi.js";
5
+ export interface GeoGebraToolProviderConfig extends CalculatorToolProviderInitConfig {
6
+ scriptUrl?: string;
7
+ appletTimeoutMs?: number;
8
+ }
9
+ export declare class GeoGebraToolProvider extends LazyCalculatorToolProvider<GeoGebraToolProviderConfig> {
10
+ readonly providerId = "geogebra-calculator";
11
+ readonly providerName = "GeoGebra Calculator";
12
+ readonly version = "6";
13
+ readonly requiresAuth = false;
14
+ protected getDefinition(): {
15
+ backend: string;
16
+ moduleImportOperation: string;
17
+ loadProvider: () => Promise<typeof import("@pie-players/pie-calculator-geogebra").GeoGebraCalculatorProvider>;
18
+ initializationErrorMessage: string;
19
+ };
20
+ getCapabilities(): ToolProviderCapabilities;
21
+ }
@@ -0,0 +1,32 @@
1
+ /** GeoGebra calculator adapter for the generic tool-provider registry. */
2
+ import { LazyCalculatorToolProvider } from "./LazyCalculatorToolProvider.js";
3
+ export class GeoGebraToolProvider extends LazyCalculatorToolProvider {
4
+ providerId = "geogebra-calculator";
5
+ providerName = "GeoGebra Calculator";
6
+ version = "6";
7
+ requiresAuth = false;
8
+ getDefinition() {
9
+ return {
10
+ backend: "geogebra",
11
+ moduleImportOperation: "geogebra-provider-module-import",
12
+ loadProvider: async () => {
13
+ const module = await import("@pie-players/pie-calculator-geogebra");
14
+ return module.GeoGebraCalculatorProvider;
15
+ },
16
+ initializationErrorMessage: "Failed to initialize GeoGebra calculator provider. Confirm that the deployment may load GeoGebra and that its script URL is reachable.",
17
+ };
18
+ }
19
+ getCapabilities() {
20
+ return {
21
+ supportsOffline: false,
22
+ requiresAuth: false,
23
+ maxInstances: null,
24
+ features: {
25
+ basic: true,
26
+ scientific: true,
27
+ graphing: true,
28
+ fourFunction: false,
29
+ },
30
+ };
31
+ }
32
+ }
@@ -0,0 +1,35 @@
1
+ import type { CalculatorProvider, CalculatorProviderInit } from "@pie-players/pie-calculator";
2
+ import type { ToolProviderApi, ToolProviderCapabilities } from "./ToolProviderApi.js";
3
+ export type CalculatorToolProviderInitConfig = Pick<CalculatorProviderInit, "onTelemetry">;
4
+ type InitializableCalculatorProvider<TConfig> = CalculatorProvider & {
5
+ initialize(config: TConfig): Promise<void>;
6
+ };
7
+ interface LazyCalculatorProviderDefinition<TConfig> {
8
+ backend: string;
9
+ moduleImportOperation: string;
10
+ loadProvider: () => Promise<new () => InitializableCalculatorProvider<TConfig>>;
11
+ initializationErrorMessage: string;
12
+ }
13
+ /**
14
+ * Shared lazy-module and lifecycle implementation for calculator tool adapters.
15
+ * Concrete adapters own only their metadata, capabilities and provider import.
16
+ */
17
+ export declare abstract class LazyCalculatorToolProvider<TConfig extends CalculatorToolProviderInitConfig> implements ToolProviderApi<TConfig, CalculatorProvider> {
18
+ abstract readonly providerId: string;
19
+ abstract readonly providerName: string;
20
+ readonly category: "calculator";
21
+ abstract readonly version: string;
22
+ abstract readonly requiresAuth: boolean;
23
+ protected abstract getDefinition(): LazyCalculatorProviderDefinition<TConfig>;
24
+ abstract getCapabilities(): ToolProviderCapabilities;
25
+ private calculatorProvider;
26
+ private initializationPromise;
27
+ private lifecycleGeneration;
28
+ private emitTelemetry;
29
+ initialize(config?: TConfig): Promise<void>;
30
+ private initializeProvider;
31
+ createInstance(): Promise<CalculatorProvider>;
32
+ isReady(): boolean;
33
+ destroy(): void;
34
+ }
35
+ export {};
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Shared lazy-module and lifecycle implementation for calculator tool adapters.
3
+ * Concrete adapters own only their metadata, capabilities and provider import.
4
+ */
5
+ export class LazyCalculatorToolProvider {
6
+ category = "calculator";
7
+ calculatorProvider = null;
8
+ initializationPromise = null;
9
+ lifecycleGeneration = 0;
10
+ async emitTelemetry(config, eventName, payload) {
11
+ try {
12
+ await config.onTelemetry?.(eventName, payload);
13
+ }
14
+ catch (error) {
15
+ console.warn(`[${this.providerName}] telemetry callback failed:`, error);
16
+ }
17
+ }
18
+ async initialize(config = {}) {
19
+ if (this.calculatorProvider)
20
+ return;
21
+ if (this.initializationPromise)
22
+ return this.initializationPromise;
23
+ const generation = ++this.lifecycleGeneration;
24
+ const initializationPromise = this.initializeProvider(config, generation);
25
+ this.initializationPromise = initializationPromise;
26
+ try {
27
+ await initializationPromise;
28
+ }
29
+ finally {
30
+ if (this.initializationPromise === initializationPromise) {
31
+ this.initializationPromise = null;
32
+ }
33
+ }
34
+ }
35
+ async initializeProvider(config, generation) {
36
+ const definition = this.getDefinition();
37
+ const moduleLoadStartedAt = Date.now();
38
+ await this.emitTelemetry(config, "pie-tool-library-load-start", {
39
+ toolId: "calculator",
40
+ operation: definition.moduleImportOperation,
41
+ backend: definition.backend,
42
+ });
43
+ let ProviderConstructor;
44
+ try {
45
+ ProviderConstructor = await definition.loadProvider();
46
+ await this.emitTelemetry(config, "pie-tool-library-load-success", {
47
+ toolId: "calculator",
48
+ operation: definition.moduleImportOperation,
49
+ backend: definition.backend,
50
+ duration: Date.now() - moduleLoadStartedAt,
51
+ });
52
+ }
53
+ catch (error) {
54
+ await this.emitTelemetry(config, "pie-tool-library-load-error", {
55
+ toolId: "calculator",
56
+ operation: definition.moduleImportOperation,
57
+ backend: definition.backend,
58
+ duration: Date.now() - moduleLoadStartedAt,
59
+ errorType: "ToolLibraryLoadError",
60
+ message: error instanceof Error ? error.message : String(error),
61
+ });
62
+ throw new Error(definition.initializationErrorMessage, { cause: error });
63
+ }
64
+ if (generation !== this.lifecycleGeneration) {
65
+ throw new Error(`${this.providerName} initialization was cancelled`);
66
+ }
67
+ const candidate = new ProviderConstructor();
68
+ try {
69
+ await candidate.initialize(config);
70
+ if (generation !== this.lifecycleGeneration) {
71
+ throw new Error(`${this.providerName} initialization was cancelled`);
72
+ }
73
+ this.calculatorProvider = candidate;
74
+ }
75
+ catch (error) {
76
+ candidate.destroy();
77
+ throw new Error(definition.initializationErrorMessage, { cause: error });
78
+ }
79
+ }
80
+ async createInstance() {
81
+ if (!this.calculatorProvider) {
82
+ throw new Error(`[${this.providerName}] Provider not initialized. Call initialize() first.`);
83
+ }
84
+ return this.calculatorProvider;
85
+ }
86
+ isReady() {
87
+ return this.calculatorProvider !== null;
88
+ }
89
+ destroy() {
90
+ this.lifecycleGeneration += 1;
91
+ this.initializationPromise = null;
92
+ this.calculatorProvider?.destroy();
93
+ this.calculatorProvider = null;
94
+ }
95
+ }
@@ -11,5 +11,9 @@ export { ToolProviderRegistry } from "./ToolProviderRegistry.js";
11
11
  export type { ToolProviderConfig } from "./ToolProviderRegistry.js";
12
12
  export { DesmosToolProvider } from "./DesmosToolProvider.js";
13
13
  export type { DesmosToolProviderConfig } from "./DesmosToolProvider.js";
14
+ export { CortexToolProvider } from "./CortexToolProvider.js";
15
+ export type { CortexToolProviderConfig } from "./CortexToolProvider.js";
16
+ export { GeoGebraToolProvider } from "./GeoGebraToolProvider.js";
17
+ export type { GeoGebraToolProviderConfig } from "./GeoGebraToolProvider.js";
14
18
  export { TTSToolProvider } from "./TTSToolProvider.js";
15
19
  export type { TTSToolProviderConfig, TTSBackend, } from "./TTSToolProvider.js";
@@ -10,4 +10,6 @@
10
10
  export { ToolProviderRegistry } from "./ToolProviderRegistry.js";
11
11
  // Concrete providers
12
12
  export { DesmosToolProvider } from "./DesmosToolProvider.js";
13
+ export { CortexToolProvider } from "./CortexToolProvider.js";
14
+ export { GeoGebraToolProvider } from "./GeoGebraToolProvider.js";
13
15
  export { TTSToolProvider } from "./TTSToolProvider.js";
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Tool open requests.
3
+ *
4
+ * A surface that acts on the learner's current selection has to hand that selection
5
+ * to a tool it does not mount. The annotation strip is the case PIE ships: it is a
6
+ * section-scoped singleton in its own shadow root, and the tool it opens is mounted
7
+ * by a toolbar under a scoped instance id the strip cannot construct.
8
+ *
9
+ * Resolution is a claim, not a broadcast. Each toolbar registers as the target for its
10
+ * placement level, and a request reaches exactly one: the first target that currently
11
+ * hosts the tool, preferring section scope. A broadcast would open a panel in every
12
+ * toolbar whose scope contains the selection, which in a section player is the item
13
+ * card's toolbar and the section's both.
14
+ *
15
+ * `params` reaches the tool through the same seam a host-registered context resolver
16
+ * feeds, so receiving a request costs a tool nothing: whatever already reads
17
+ * `getToolRenderParams` sees it.
18
+ *
19
+ * Core names no capability. The requester supplies the tool id, which is why the
20
+ * pairing of a selection action to a dictionary lives in the composition layer.
21
+ */
22
+ import type { ToolPlacementLevel } from "./tools-config-normalizer.js";
23
+ /** Level a request resolves against when the requester names none. */
24
+ export declare const DEFAULT_TOOL_REQUEST_LEVEL: ToolPlacementLevel;
25
+ export interface ToolOpenRequest {
26
+ /** Unscoped tool id, as the registration declares it. */
27
+ toolId: string;
28
+ /**
29
+ * Merged over the host-resolved render params for this tool rather than
30
+ * replacing them, so a request carrying a term to look up leaves the endpoint
31
+ * the host configured in place.
32
+ */
33
+ params?: Record<string, unknown>;
34
+ /**
35
+ * Placement level of the toolbar that should open the tool.
36
+ *
37
+ * Naming one is a constraint and is honoured strictly: a requester that asks for
38
+ * `"item"` gets an item toolbar or nothing. Leaving it out asks for whichever
39
+ * toolbar hosts the tool, preferring `"section"` — the level at which a whole
40
+ * section shares one instance. A host that places a tool only at item scope would
41
+ * otherwise have the affordance silently disappear, and configuring a level here
42
+ * to match a placement made elsewhere is a step it has no reason to expect.
43
+ *
44
+ * At `"item"` and `"passage"` a section holds one target per card, and the first
45
+ * registered one that hosts the tool claims the request. A requester that needs a
46
+ * particular card's instance cannot express that here, and the gateway PIE ships
47
+ * does not need to: the strip is a section-scoped singleton acting on passage
48
+ * selections, so the selection belongs to no card, and what opens is a floating
49
+ * shell rather than anything rendered inside one.
50
+ */
51
+ level?: ToolPlacementLevel;
52
+ }
53
+ export interface ToolRequestTarget {
54
+ /** The placement level this toolbar renders. */
55
+ level: ToolPlacementLevel;
56
+ /** Whether this toolbar currently renders the tool, per its own policy pass. */
57
+ hostsTool: (toolId: string) => boolean;
58
+ /**
59
+ * Show the tool with `params` already applied.
60
+ *
61
+ * Show rather than toggle: a learner who selects a second word and asks for the
62
+ * dictionary again is asking for the dictionary, and a toggle would close it.
63
+ */
64
+ open: (toolId: string, params?: Record<string, unknown>) => void;
65
+ }
66
+ /**
67
+ * Registry of the toolbars a request can reach.
68
+ *
69
+ * Insertion order is the tie-break within a level, so a target registered while an
70
+ * earlier one is still mounted does not displace it.
71
+ */
72
+ export declare class ToolRequestRegistry {
73
+ private readonly targets;
74
+ private readonly changeListeners;
75
+ registerTarget(target: ToolRequestTarget): () => void;
76
+ /**
77
+ * Whether a request for this tool would reach a toolbar.
78
+ *
79
+ * A surface asks before offering the affordance: a button that silently does
80
+ * nothing is worse than an absent one, and availability moves with policy —
81
+ * hence {@link onTargetsChange}.
82
+ */
83
+ canRequest(toolId: string, level?: ToolPlacementLevel): boolean;
84
+ /** Returns whether a target claimed the request. */
85
+ request(request: ToolOpenRequest): boolean;
86
+ /**
87
+ * Fires when a toolbar registers or unregisters.
88
+ *
89
+ * Not when a registered toolbar's own visible set changes: `hostsTool` is read
90
+ * live, so a caller re-asking `canRequest` gets the current answer. A surface
91
+ * that needs to notice a policy change should also follow the policy signal it
92
+ * already has.
93
+ */
94
+ onTargetsChange(listener: () => void): () => void;
95
+ /**
96
+ * An explicit level is a constraint; the default is a preference.
97
+ *
98
+ * Falling back off `"section"` is what lets a host place a tool at item scope only
99
+ * and still have a section-scoped gateway reach it. Requesting a level explicitly
100
+ * does not fall back, because a requester that named one meant it.
101
+ */
102
+ private findTarget;
103
+ private findTargetAtLevel;
104
+ private hostsTool;
105
+ private notifyChange;
106
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Tool open requests.
3
+ *
4
+ * A surface that acts on the learner's current selection has to hand that selection
5
+ * to a tool it does not mount. The annotation strip is the case PIE ships: it is a
6
+ * section-scoped singleton in its own shadow root, and the tool it opens is mounted
7
+ * by a toolbar under a scoped instance id the strip cannot construct.
8
+ *
9
+ * Resolution is a claim, not a broadcast. Each toolbar registers as the target for its
10
+ * placement level, and a request reaches exactly one: the first target that currently
11
+ * hosts the tool, preferring section scope. A broadcast would open a panel in every
12
+ * toolbar whose scope contains the selection, which in a section player is the item
13
+ * card's toolbar and the section's both.
14
+ *
15
+ * `params` reaches the tool through the same seam a host-registered context resolver
16
+ * feeds, so receiving a request costs a tool nothing: whatever already reads
17
+ * `getToolRenderParams` sees it.
18
+ *
19
+ * Core names no capability. The requester supplies the tool id, which is why the
20
+ * pairing of a selection action to a dictionary lives in the composition layer.
21
+ */
22
+ /** Level a request resolves against when the requester names none. */
23
+ export const DEFAULT_TOOL_REQUEST_LEVEL = "section";
24
+ /**
25
+ * Registry of the toolbars a request can reach.
26
+ *
27
+ * Insertion order is the tie-break within a level, so a target registered while an
28
+ * earlier one is still mounted does not displace it.
29
+ */
30
+ export class ToolRequestRegistry {
31
+ targets = new Set();
32
+ changeListeners = new Set();
33
+ registerTarget(target) {
34
+ this.targets.add(target);
35
+ this.notifyChange();
36
+ return () => {
37
+ if (!this.targets.delete(target))
38
+ return;
39
+ this.notifyChange();
40
+ };
41
+ }
42
+ /**
43
+ * Whether a request for this tool would reach a toolbar.
44
+ *
45
+ * A surface asks before offering the affordance: a button that silently does
46
+ * nothing is worse than an absent one, and availability moves with policy —
47
+ * hence {@link onTargetsChange}.
48
+ */
49
+ canRequest(toolId, level) {
50
+ return this.findTarget(toolId, level) !== null;
51
+ }
52
+ /** Returns whether a target claimed the request. */
53
+ request(request) {
54
+ const target = this.findTarget(request.toolId, request.level);
55
+ if (!target)
56
+ return false;
57
+ try {
58
+ target.open(request.toolId, request.params);
59
+ }
60
+ catch (error) {
61
+ console.error(`[ToolRequestRegistry] Target failed to open "${request.toolId}":`, error);
62
+ return false;
63
+ }
64
+ return true;
65
+ }
66
+ /**
67
+ * Fires when a toolbar registers or unregisters.
68
+ *
69
+ * Not when a registered toolbar's own visible set changes: `hostsTool` is read
70
+ * live, so a caller re-asking `canRequest` gets the current answer. A surface
71
+ * that needs to notice a policy change should also follow the policy signal it
72
+ * already has.
73
+ */
74
+ onTargetsChange(listener) {
75
+ this.changeListeners.add(listener);
76
+ return () => {
77
+ this.changeListeners.delete(listener);
78
+ };
79
+ }
80
+ /**
81
+ * An explicit level is a constraint; the default is a preference.
82
+ *
83
+ * Falling back off `"section"` is what lets a host place a tool at item scope only
84
+ * and still have a section-scoped gateway reach it. Requesting a level explicitly
85
+ * does not fall back, because a requester that named one meant it.
86
+ */
87
+ findTarget(toolId, level) {
88
+ const preferred = this.findTargetAtLevel(toolId, level ?? DEFAULT_TOOL_REQUEST_LEVEL);
89
+ if (preferred || level !== undefined)
90
+ return preferred;
91
+ for (const target of this.targets) {
92
+ if (target.level === DEFAULT_TOOL_REQUEST_LEVEL)
93
+ continue;
94
+ if (this.hostsTool(target, toolId))
95
+ return target;
96
+ }
97
+ return null;
98
+ }
99
+ findTargetAtLevel(toolId, level) {
100
+ for (const target of this.targets) {
101
+ if (target.level !== level)
102
+ continue;
103
+ if (this.hostsTool(target, toolId))
104
+ return target;
105
+ }
106
+ return null;
107
+ }
108
+ hostsTool(target, toolId) {
109
+ try {
110
+ return target.hostsTool(toolId) === true;
111
+ }
112
+ catch (error) {
113
+ console.warn(`[ToolRequestRegistry] Target at level "${target.level}" failed the host check for "${toolId}":`, error);
114
+ return false;
115
+ }
116
+ }
117
+ notifyChange() {
118
+ for (const listener of this.changeListeners) {
119
+ try {
120
+ listener();
121
+ }
122
+ catch (error) {
123
+ console.warn("[ToolRequestRegistry] Target change listener threw:", error);
124
+ }
125
+ }
126
+ }
127
+ }
@@ -3,6 +3,12 @@ export interface ToolbarItemBase {
3
3
  label: string;
4
4
  ariaLabel?: string;
5
5
  icon?: string;
6
+ /**
7
+ * FontAwesome icon name, opting this item into `<nds-icon-button>` rendering
8
+ * where the host enables NDS icons. Set by a tool registration's button
9
+ * definition, or by a host contributing its own button.
10
+ */
11
+ faIconName?: string;
6
12
  tooltip?: string;
7
13
  active?: boolean;
8
14
  disabled?: boolean;
@@ -8,7 +8,5 @@
8
8
  * '@pie-players/pie-assessment-toolkit' to ensure
9
9
  * they don't accidentally pull in server-side dependencies.
10
10
  */
11
- export { DesmosCalculatorProvider } from "./calculators/desmos-provider.js";
12
- export { COMMON_LIBRARIES, LibraryLoaderImpl, libraryLoader, } from "./library-loader.js";
13
11
  export { ResponseDiscoveryServiceImpl, responseDiscovery, } from "./response-discovery.js";
14
12
  export * from "./types.js";
@@ -8,10 +8,6 @@
8
8
  * '@pie-players/pie-assessment-toolkit' to ensure
9
9
  * they don't accidentally pull in server-side dependencies.
10
10
  */
11
- // Calculator providers (client-safe, have SSR guards)
12
- export { DesmosCalculatorProvider } from "./calculators/desmos-provider.js";
13
- // Library loader (client-safe, has SSR guards)
14
- export { COMMON_LIBRARIES, LibraryLoaderImpl, libraryLoader, } from "./library-loader.js";
15
11
  // Response discovery (client-safe, browser-only)
16
12
  export { ResponseDiscoveryServiceImpl, responseDiscovery, } from "./response-discovery.js";
17
13
  // Core types (client-safe)