@pie-players/pie-default-tool-loaders 0.3.75 → 1.0.0-beta.1

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.
package/README.md CHANGED
@@ -1,16 +1,15 @@
1
1
  # PIE Default Tool Loaders
2
2
 
3
- Composition layer for the packaged PIE assessment tools: which capabilities exist
4
- in a deployment, and how a program tiers them.
3
+ The packaged PIE assessment tools: which capabilities exist in a deployment, how
4
+ they load, and which of them a program grants to everyone. This README is for
5
+ host integrators choosing a tool set and a calculator, and for contributors
6
+ adding a packaged capability.
5
7
 
6
- It owns the concrete `pie-tool-*` dependencies and the calculator engines
7
- (`pie-calculator-cortex`, `-desmos` and `-geogebra`) so toolkit core can stay
8
- dependency-light and cycle-safe, and it is the layer above core — core knows
9
- `featureId`, placement levels, activation kinds and precedence rules, and knows
10
- no capability ids.
11
-
12
- It owns five things, all answers to "which capabilities does this deployment have
13
- and how does the program tier them":
8
+ The package owns the concrete `pie-tool-*` dependencies and the calculator
9
+ engines (`pie-calculator-cortex`, `-desmos` and `-geogebra`), so toolkit core
10
+ stays dependency-light and cycle-safe. Core knows the concepts — support ids,
11
+ placement levels, activation kinds and precedence rules — and no concrete
12
+ capability id. A support id is the tool id a profile grants.
14
13
 
15
14
  | Export | What it decides |
16
15
  | --- | --- |
@@ -18,49 +17,56 @@ and how does the program tier them":
18
17
  | `PACKAGED_TOOL_TAG_MAP` | Which custom element each one renders as |
19
18
  | `SECTION_PLAYER_PREFERRED_TOOL_PLACEMENT` | Where they appear and in what order |
20
19
  | `createUniversalPersonalNeedsProfile`, `createEmptyPersonalNeedsProfile` | Which of them the program grants to everyone, or to no one |
20
+ | `createPersonalNeedsProfileFromAfa` | Which of them a student's AfA PNP 3.0 record grants, prohibits and turns on at start, and with which settings |
21
21
  | `DEFAULT_TOOL_MODULE_LOADERS` | When each one's bundle loads |
22
+ | `CONTENT_ALTERNATE_REGISTRATIONS` | Which of them carry an authored alternate that renders as a region; the print player mounts these |
22
23
 
23
- The individual registrations are exported too, so a host can compose its own set
24
- rather than take the packaged one whole.
25
-
26
- These exports are projections of one internal **Packaged Capability
27
- Composition**. Each packaged capability is authored once with its registration,
28
- element delivery, module loader, preferred placement and explicit
29
- universal-support policy. The package build rejects contradictory PIE-owned data
30
- — for example, a region capability with a toolbar tag, or a content-dependent
31
- capability marked universal — so release tests find a missing facet
32
- instead of a learner finding a dead affordance. The browser does not repeat that
33
- strict gate at import time: a PIE authoring defect must block publication, not an
34
- otherwise usable assessment.
35
-
36
- That invariant boundary does not make host input stricter: lazy module loading
37
- is opt-in, registration/tag/factory overrides take precedence, an empty
38
- `toolIds` array selects the full packaged set, and unknown selected ids are
39
- ignored while known ids register.
24
+ The individual registrations are exported too, so a host can compose its own
25
+ set ([Custom sets](#custom-sets)).
40
26
 
41
27
  ## Usage
42
28
 
43
29
  ```ts
44
- import {
45
- createPackagedToolRegistry,
46
- DEFAULT_TOOL_MODULE_LOADERS,
47
- } from "@pie-players/pie-default-tool-loaders";
30
+ import { createPackagedToolRegistry } from "@pie-players/pie-default-tool-loaders";
48
31
 
49
32
  // The packaged capability set, lazily loaded.
50
- const registry = createPackagedToolRegistry({
51
- toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
52
- });
33
+ const registry = createPackagedToolRegistry();
34
+
35
+ // The same registrations for a host that defines the tool elements itself.
36
+ const preloaded = createPackagedToolRegistry({ toolModuleLoaders: {} });
53
37
  ```
54
38
 
55
39
  One loader set serves every host shape. `DEFAULT_TOOL_MODULE_LOADERS` holds a
56
- loader per packaged capability, and a registry loads a module only when a tool
57
- first renders, so an item player, a section player and an assessment player
58
- pass the same set and load only the tools they place.
59
-
60
- The calculator is one element, `<pie-tool-calculator>`, for every provider.
61
- Desmos is the provider when none is configured; the toolkit picks another from
62
- `tools.providers.calculator` when the element mounts, and an open calculator
63
- remounts on the new provider when a tool-config update replaces it.
40
+ loader per packaged capability and is what `createPackagedToolRegistry`
41
+ installs unless `toolModuleLoaders` replaces it. A registry loads a module only
42
+ when a tool first renders, so an item player, a section player and an
43
+ assessment player share the set and load only the tools they place.
44
+
45
+ `createPackagedToolRegistry` is lenient with host input. Registration, tag-map
46
+ and component-factory overrides take precedence over the packaged data. An
47
+ empty `toolIds` array selects the full packaged set, and unknown ids in it are
48
+ ignored while the known ones register.
49
+
50
+ ## Calculator providers
51
+
52
+ The calculator is one element, `<pie-tool-calculator>`, for every provider. The
53
+ toolkit picks the provider from `tools.providers.calculator` when the element
54
+ mounts, and an open calculator remounts on the new provider when a tool-config
55
+ update replaces it.
56
+
57
+ Desmos is the provider when none is configured, and it needs an API key: pass
58
+ it through `provider.runtime.authFetcher`, a `provider.init.proxyEndpoint` that
59
+ returns it, or `provider.init.apiKey` in development
60
+ ([Calculator with host auth](../../docs/tools-and-accomodations/tool_provider_system.md#calculator-with-host-auth)).
61
+ Without one the calculator fails to load. For a keyless, offline-capable
62
+ calculator with no runtime CDN, use `provider.id: "calculator-cortex"`. Its
63
+ `settings` take the `CortexCalculatorSettings` type that
64
+ `@pie-players/pie-calculator-cortex` exports: `angleMode`,
65
+ `calculationPrecision`, `displayPrecision`, `historyLimit`,
66
+ `evaluationTimeLimitMs`, `allowedFunctions`, `allowClipboard`, `messages`,
67
+ `direction` and `graph`. Clipboard blocking follows `restrictedMode`;
68
+ `allowClipboard` alone blocks nothing. The [Cortex README](../calculator-cortex/README.md)
69
+ covers localization, message overrides and the keypad.
64
70
 
65
71
  ```ts
66
72
  const tools = {
@@ -76,10 +82,7 @@ const tools = {
76
82
  };
77
83
  ```
78
84
 
79
- For an offline-capable calculator with no API key or runtime CDN, use
80
- `provider.id: "calculator-cortex"`. Its `settings` accept angle mode, precision,
81
- history, evaluation limit, allowed functions, clipboard policy, and graph
82
- viewport options documented by `@pie-players/pie-calculator-cortex`.
85
+ ## Custom sets
83
86
 
84
87
  A host composing its own set starts from the toolkit's empty `ToolRegistry`,
85
88
  registers into it, and installs the tag map its registrations create elements
@@ -105,19 +108,21 @@ A later `setToolModuleLoaders` call adds or replaces entries. A replacement
105
108
  registered after a tool's module has loaded is ignored with a warning, since the
106
109
  elements it defines are already in place.
107
110
 
108
- ## Content-dependent capabilities require an explicit packaging decision
111
+ ## Content-dependent capabilities
109
112
 
110
- A content-dependent capability is never granted by
111
- `createUniversalPersonalNeedsProfile()`. It may ship in `PACKAGED_TOOL_REGISTRATIONS` only
112
- when it has a useful presentation path without a grant and declares
113
- `resolvesWithoutGrant`. Audio transcript is the shipped example: an authored
114
- `visibility: "always"` transcript reaches every player, while its accommodation
115
- path remains policy-gated and `transcript` stays out of the universal profile.
113
+ A content-dependent capability declares `requiresAuthoredContent`: it renders
114
+ only where the content carries what it needs, and
115
+ `createUniversalPersonalNeedsProfile()` never grants one. It ships in
116
+ `PACKAGED_TOOL_REGISTRATIONS` only when it has a useful presentation path
117
+ without a grant and declares `resolvesWithoutGrant`. Audio transcript is the
118
+ shipped example: an authored `visibility: "always"` transcript reaches every
119
+ player, while its accommodation path stays policy-gated and `transcript` stays
120
+ out of the universal profile.
116
121
 
117
- A content-dependent capability with no presentation path remains an explicit
118
- deployment choice. Signing is the shipped example: install
119
- `@pie-players/pie-tool-sign-language` and register it, which is the same two
120
- lines a host writes for a capability of its own.
122
+ A content-dependent capability with no such path is an explicit deployment
123
+ choice. Signing is the shipped example: install
124
+ `@pie-players/pie-tool-sign-language` and register it, the same two lines a
125
+ host writes for a capability of its own.
121
126
 
122
127
  ```ts
123
128
  import { signLanguageRegistration } from "@pie-players/pie-tool-sign-language";
@@ -129,25 +134,59 @@ registry.register(signLanguageRegistration);
129
134
 
130
135
  `createUniversalPersonalNeedsProfile()` builds a fresh `PersonalNeedsProfile`
131
136
  granting the tool ids of the capabilities the packaged set treats as universal
132
- features. A support id is the tool id it grants.
137
+ features. Tool policy reads the profile from the assessment entity, so set it
138
+ there: on the player's `assessment` property, or through
139
+ `coordinator.updateAssessment(...)` on a coordinator you pass. A
140
+ `section.personalNeedsProfile` is ignored with a console warning.
133
141
 
134
142
  ```ts
135
143
  import { createUniversalPersonalNeedsProfile } from "@pie-players/pie-default-tool-loaders";
136
144
 
137
- const section = {
138
- ...authoredSection,
145
+ sectionPlayer.assessment = {
146
+ ...authoredAssessment,
139
147
  personalNeedsProfile: createUniversalPersonalNeedsProfile(),
140
148
  };
141
149
  ```
142
150
 
143
151
  The toolkit ships no populated default, and `createEmptyPersonalNeedsProfile()`
144
- builds a profile granting nothing. Which capabilities a
145
- deployment grants by default is a property of the program, not of a capability:
146
- TTS is a universal feature in one program and a documented accommodation in
147
- another. So the universal profile is data to adopt, extend or replace alongside the district
148
- and test-administration configuration, and it deliberately excludes any
149
- capability that declares a content dependency.
150
-
151
- A section carrying no profile is left alone. `pnpEnforcement` auto-detection
152
- engages on real host policy material, so supplying no profile means placement
153
- alone decides which tools appear.
152
+ builds a profile granting nothing. Which capabilities a deployment grants by
153
+ default is a property of the program: TTS is a universal feature in one program
154
+ and a documented accommodation in another. The universal profile is data to
155
+ adopt, extend or replace alongside the district and test-administration
156
+ configuration, and it excludes every capability that declares a content
157
+ dependency.
158
+
159
+ An assessment carrying no profile is left alone. `pnpEnforcement`
160
+ auto-detection engages only on host policy material, so with no profile,
161
+ placement alone decides which tools appear.
162
+
163
+ ## AfA profiles
164
+
165
+ `createPersonalNeedsProfileFromAfa(record)` builds a fresh profile from a
166
+ student's AfA PNP 3.0 record, held as JSON keyed by the spec's terms. Features
167
+ become support ids in the packaged set's names, the activation sets fill
168
+ `activateAtInit` and `prohibitedSupports`, and the settings the packaged tools
169
+ read become `toolParameters`: read-aloud speed, pitch and volume, the calculator
170
+ type and the sign language. The toolkit's PNP configuration guide gives each AfA
171
+ feature's disposition.
172
+
173
+ ```ts
174
+ import { createPersonalNeedsProfileFromAfa } from "@pie-players/pie-default-tool-loaders";
175
+
176
+ sectionPlayer.assessment = {
177
+ ...authoredAssessment,
178
+ personalNeedsProfile: createPersonalNeedsProfileFromAfa(afaRecord),
179
+ };
180
+ ```
181
+
182
+ ## Packaged capability composition
183
+
184
+ The exports are projections of one internal composition. Each packaged
185
+ capability is authored once with its registration, element delivery, module
186
+ loader, preferred placement and explicit universal-support flag.
187
+
188
+ The package build rejects contradictory PIE-owned data, such as a region
189
+ capability with a toolbar tag or a content-dependent capability marked
190
+ universal, so release tests catch a missing facet before it ships. The browser
191
+ skips that strict check at import time: a PIE authoring defect blocks
192
+ publication and never an otherwise usable assessment.
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The AfA PNP 3.0 adapter: an AfA record, as JSON keyed by the spec's
3
+ * kebab-case terms, becomes the toolkit's personal-needs profile.
4
+ *
5
+ * Each feature the record names is a support id, the term camelCased, except
6
+ * where a packaged tool carries the capability under an id of its own
7
+ * (`TOOL_REGISTRY.md`, Support ids). `spoken` resolves by its `reading-type`:
8
+ * `computer-read-aloud` is `textToSpeech`, and `screen-reader`, AfA's default,
9
+ * is `screenReader` (ADR 0004). `extensions` and the resource-matching
10
+ * preferences name no support and are skipped.
11
+ *
12
+ * A feature at the record's root or in `activate-as-option-set` is granted, one
13
+ * in `activate-at-initialization-set` is granted and on at start, and one in
14
+ * `prohibit-set` is prohibited, which withdraws it from both. The settings of
15
+ * a feature a packaged tool reads become that tool's `toolParameters`.
16
+ */
17
+ import type { PersonalNeedsProfile } from "@pie-players/pie-players-shared/types";
18
+ /** `spoken`'s settings, in AfA's units. */
19
+ export interface AfaSpoken {
20
+ "reading-type"?: "screen-reader" | "computer-read-aloud";
21
+ /** Words per minute; AfA's default is 180. */
22
+ "speech-rate"?: number;
23
+ /** 0–1; AfA's default is 0.5. */
24
+ pitch?: number;
25
+ /** 0–1; AfA's default is 0.5. */
26
+ volume?: number;
27
+ [setting: string]: unknown;
28
+ }
29
+ /** A set's features: empty elements keyed by feature, or the feature names. */
30
+ export type AfaFeatureSet = Readonly<Record<string, unknown>> | readonly string[];
31
+ /** An AfA PNP 3.0 record in JSON. A feature is present unless absent, `null` or `false`. */
32
+ export interface AfaPnpRecord {
33
+ spoken?: AfaSpoken;
34
+ "calculator-on-screen"?: {
35
+ "calculator-type"?: string;
36
+ [setting: string]: unknown;
37
+ };
38
+ "sign-language"?: {
39
+ language?: string;
40
+ [setting: string]: unknown;
41
+ };
42
+ "activate-at-initialization-set"?: AfaFeatureSet;
43
+ "activate-as-option-set"?: AfaFeatureSet;
44
+ "prohibit-set"?: AfaFeatureSet;
45
+ [feature: string]: unknown;
46
+ }
47
+ /** A fresh profile carrying `record`'s grants, prohibitions and settings, ids sorted. */
48
+ export declare function createPersonalNeedsProfileFromAfa(record: AfaPnpRecord): PersonalNeedsProfile;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * The AfA PNP 3.0 adapter: an AfA record, as JSON keyed by the spec's
3
+ * kebab-case terms, becomes the toolkit's personal-needs profile.
4
+ *
5
+ * Each feature the record names is a support id, the term camelCased, except
6
+ * where a packaged tool carries the capability under an id of its own
7
+ * (`TOOL_REGISTRY.md`, Support ids). `spoken` resolves by its `reading-type`:
8
+ * `computer-read-aloud` is `textToSpeech`, and `screen-reader`, AfA's default,
9
+ * is `screenReader` (ADR 0004). `extensions` and the resource-matching
10
+ * preferences name no support and are skipped.
11
+ *
12
+ * A feature at the record's root or in `activate-as-option-set` is granted, one
13
+ * in `activate-at-initialization-set` is granted and on at start, and one in
14
+ * `prohibit-set` is prohibited, which withdraws it from both. The settings of
15
+ * a feature a packaged tool reads become that tool's `toolParameters`.
16
+ */
17
+ const RENAMED_FEATURES = {
18
+ "calculator-on-screen": "calculator",
19
+ "answer-masking": "answerEliminator",
20
+ "dictionary-on-screen": "dictionary",
21
+ };
22
+ /**
23
+ * Keys that name no support: the sets, `extensions`, and the preferences that
24
+ * select among adapted resources by metadata PIE content does not carry. The
25
+ * interface language is the host's `locale`.
26
+ */
27
+ const NON_SUPPORT_KEYS = new Set([
28
+ "activate-at-initialization-set",
29
+ "activate-as-option-set",
30
+ "prohibit-set",
31
+ "extensions",
32
+ "access-mode-required",
33
+ "adaptation-type-required",
34
+ "at-interoperable",
35
+ "educational-complexity-of-adaptation",
36
+ "hazard-avoidance",
37
+ "input-requirements",
38
+ "language-of-adaptation",
39
+ "language-of-interface",
40
+ "adaptation-detail-required",
41
+ "adaptation-media-required",
42
+ "educational-level-of-adaptation",
43
+ ]);
44
+ /** AfA's `standard` calculator has no PIE type, so it keeps the tool's default. */
45
+ const CALCULATOR_TYPES = new Set(["basic", "scientific", "graphing"]);
46
+ /** AfA's default `speech-rate`, which PIE's normal speed 1 stands for. */
47
+ const AFA_NORMAL_SPEECH_RATE = 180;
48
+ const isPresent = (value) => value !== undefined && value !== null && value !== false;
49
+ const finiteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
50
+ const camelCase = (term) => term.replace(/-([a-z0-9])/g, (_, next) => next.toUpperCase());
51
+ function featureNames(set) {
52
+ if (!set)
53
+ return [];
54
+ const names = Array.isArray(set)
55
+ ? set.filter((name) => typeof name === "string")
56
+ : Object.entries(set)
57
+ .filter(([, value]) => isPresent(value))
58
+ .map(([name]) => name);
59
+ return names.filter((name) => !NON_SUPPORT_KEYS.has(name));
60
+ }
61
+ function spokenParameters(spoken) {
62
+ const speechRate = finiteNumber(spoken["speech-rate"]);
63
+ const pitch = finiteNumber(spoken.pitch);
64
+ const volume = finiteNumber(spoken.volume);
65
+ return {
66
+ ...(speechRate !== undefined && speechRate > 0
67
+ ? { rate: speechRate / AFA_NORMAL_SPEECH_RATE }
68
+ : {}),
69
+ ...(pitch !== undefined ? { pitch: pitch * 2 } : {}),
70
+ ...(volume !== undefined ? { volume } : {}),
71
+ };
72
+ }
73
+ /** A fresh profile carrying `record`'s grants, prohibitions and settings, ids sorted. */
74
+ export function createPersonalNeedsProfileFromAfa(record) {
75
+ const spoken = typeof record.spoken === "object" && record.spoken !== null
76
+ ? record.spoken
77
+ : {};
78
+ const readAloud = spoken["reading-type"] === "computer-read-aloud";
79
+ const supportId = (feature) => feature === "spoken"
80
+ ? readAloud
81
+ ? "textToSpeech"
82
+ : "screenReader"
83
+ : (RENAMED_FEATURES[feature] ?? camelCase(feature));
84
+ // PIE cannot withhold a screen reader, so a prohibited `spoken` withholds
85
+ // the spoken presentation PIE makes, read-aloud.
86
+ const prohibited = new Set(featureNames(record["prohibit-set"]).map((feature) => feature === "spoken" ? "textToSpeech" : supportId(feature)));
87
+ const admitted = (ids) => [...new Set(ids)].filter((id) => !prohibited.has(id)).sort();
88
+ const activateAtInit = admitted(featureNames(record["activate-at-initialization-set"]).map(supportId));
89
+ const supports = admitted([
90
+ ...featureNames(record).map(supportId),
91
+ ...featureNames(record["activate-as-option-set"]).map(supportId),
92
+ ...activateAtInit,
93
+ ]);
94
+ const toolParameters = {};
95
+ if (readAloud) {
96
+ const speech = spokenParameters(spoken);
97
+ if (Object.keys(speech).length > 0) {
98
+ toolParameters.textToSpeech = speech;
99
+ }
100
+ }
101
+ const calculatorType = record["calculator-on-screen"]?.["calculator-type"];
102
+ if (calculatorType && CALCULATOR_TYPES.has(calculatorType)) {
103
+ toolParameters.calculator = {
104
+ type: calculatorType,
105
+ };
106
+ }
107
+ const signLang = record["sign-language"]?.language;
108
+ if (typeof signLang === "string" && signLang.length > 0) {
109
+ toolParameters.signLanguage = { signLang };
110
+ }
111
+ return {
112
+ supports,
113
+ prohibitedSupports: [...prohibited].sort(),
114
+ ...(activateAtInit.length > 0 ? { activateAtInit } : {}),
115
+ ...(Object.keys(toolParameters).length > 0 ? { toolParameters } : {}),
116
+ };
117
+ }
@@ -1,12 +1,10 @@
1
1
  /** Fully bundled open-source calculator adapter for the tool-provider registry. */
2
2
  import type { CalculatorToolProviderInitConfig, LazyCalculatorProviderDefinition } from "./LazyCalculatorToolProvider.js";
3
3
  import { LazyCalculatorToolProvider } from "./LazyCalculatorToolProvider.js";
4
- import type { ToolProviderCapabilities } from "@pie-players/pie-assessment-toolkit/tools/registration";
5
4
  export type CortexToolProviderConfig = CalculatorToolProviderInitConfig;
6
5
  export declare class CortexToolProvider extends LazyCalculatorToolProvider<CortexToolProviderConfig> {
7
6
  readonly providerName = "PIE Open-Source Calculator";
8
7
  readonly version = "1";
9
8
  readonly requiresAuth = false;
10
9
  protected getDefinition(): LazyCalculatorProviderDefinition<CortexToolProviderConfig>;
11
- getCapabilities(): ToolProviderCapabilities;
12
10
  }
@@ -15,17 +15,4 @@ export class CortexToolProvider extends LazyCalculatorToolProvider {
15
15
  initializationErrorMessage: "Failed to initialize the PIE open-source calculator provider. Confirm that this browser supports module workers and that the bundled worker assets are available.",
16
16
  };
17
17
  }
18
- getCapabilities() {
19
- return {
20
- supportsOffline: true,
21
- requiresAuth: false,
22
- maxInstances: null,
23
- features: {
24
- basic: true,
25
- scientific: true,
26
- graphing: true,
27
- fourFunction: true,
28
- },
29
- };
30
- }
31
18
  }
@@ -1,7 +1,6 @@
1
1
  /** Desmos calculator adapter for the generic tool-provider registry. */
2
2
  import type { CalculatorProviderInit } from "@pie-players/pie-calculator";
3
3
  import { LazyCalculatorToolProvider, type LazyCalculatorProviderDefinition } from "./LazyCalculatorToolProvider.js";
4
- import type { ToolProviderCapabilities } from "@pie-players/pie-assessment-toolkit/tools/registration";
5
4
  /** Provider initialization is the provider-neutral calculator contract. */
6
5
  export type DesmosToolProviderConfig = CalculatorProviderInit;
7
6
  export declare class DesmosToolProvider extends LazyCalculatorToolProvider<DesmosToolProviderConfig> {
@@ -9,5 +8,4 @@ export declare class DesmosToolProvider extends LazyCalculatorToolProvider<Desmo
9
8
  readonly version = "1.12";
10
9
  readonly requiresAuth = true;
11
10
  protected getDefinition(): LazyCalculatorProviderDefinition<DesmosToolProviderConfig>;
12
- getCapabilities(): ToolProviderCapabilities;
13
11
  }
@@ -15,17 +15,4 @@ export class DesmosToolProvider extends LazyCalculatorToolProvider {
15
15
  initializationErrorMessage: "Failed to initialize Desmos calculator provider. Check the application key, preloaded API, runtime endpoint, or network access.",
16
16
  };
17
17
  }
18
- getCapabilities() {
19
- return {
20
- supportsOffline: false,
21
- requiresAuth: true,
22
- maxInstances: null,
23
- features: {
24
- basic: true,
25
- scientific: true,
26
- graphing: true,
27
- fourFunction: true,
28
- },
29
- };
30
- }
31
18
  }
@@ -1,7 +1,6 @@
1
1
  /** GeoGebra calculator adapter for the generic tool-provider registry. */
2
2
  import type { CalculatorToolProviderInitConfig, LazyCalculatorProviderDefinition } from "./LazyCalculatorToolProvider.js";
3
3
  import { LazyCalculatorToolProvider } from "./LazyCalculatorToolProvider.js";
4
- import type { ToolProviderCapabilities } from "@pie-players/pie-assessment-toolkit/tools/registration";
5
4
  export interface GeoGebraToolProviderConfig extends CalculatorToolProviderInitConfig {
6
5
  scriptUrl?: string;
7
6
  appletTimeoutMs?: number;
@@ -11,5 +10,4 @@ export declare class GeoGebraToolProvider extends LazyCalculatorToolProvider<Geo
11
10
  readonly version = "6";
12
11
  readonly requiresAuth = false;
13
12
  protected getDefinition(): LazyCalculatorProviderDefinition<GeoGebraToolProviderConfig>;
14
- getCapabilities(): ToolProviderCapabilities;
15
13
  }
@@ -15,17 +15,4 @@ export class GeoGebraToolProvider extends LazyCalculatorToolProvider {
15
15
  initializationErrorMessage: "Failed to initialize GeoGebra calculator provider. Confirm that the deployment may load GeoGebra and that its script URL is reachable.",
16
16
  };
17
17
  }
18
- getCapabilities() {
19
- return {
20
- supportsOffline: false,
21
- requiresAuth: false,
22
- maxInstances: null,
23
- features: {
24
- basic: true,
25
- scientific: true,
26
- graphing: true,
27
- fourFunction: false,
28
- },
29
- };
30
- }
31
18
  }
@@ -1,5 +1,5 @@
1
1
  import type { CalculatorProvider, CalculatorProviderInit } from "@pie-players/pie-calculator";
2
- import type { ToolProviderApi, ToolProviderCapabilities } from "@pie-players/pie-assessment-toolkit/tools/registration";
2
+ import type { ToolProviderApi } from "@pie-players/pie-assessment-toolkit/tools/registration";
3
3
  export type CalculatorToolProviderInitConfig = Pick<CalculatorProviderInit, "onTelemetry">;
4
4
  type InitializableCalculatorProvider<TConfig> = CalculatorProvider & {
5
5
  initialize(config: TConfig): Promise<void>;
@@ -12,7 +12,7 @@ export interface LazyCalculatorProviderDefinition<TConfig> {
12
12
  }
13
13
  /**
14
14
  * Shared lazy-module and lifecycle implementation for calculator tool adapters.
15
- * Concrete adapters own only their metadata, capabilities and provider import.
15
+ * Concrete adapters own only their metadata and provider import.
16
16
  */
17
17
  export declare abstract class LazyCalculatorToolProvider<TConfig extends CalculatorToolProviderInitConfig> implements ToolProviderApi<CalculatorProvider> {
18
18
  abstract readonly providerName: string;
@@ -20,7 +20,6 @@ export declare abstract class LazyCalculatorToolProvider<TConfig extends Calcula
20
20
  abstract readonly version: string;
21
21
  abstract readonly requiresAuth: boolean;
22
22
  protected abstract getDefinition(): LazyCalculatorProviderDefinition<TConfig>;
23
- abstract getCapabilities(): ToolProviderCapabilities;
24
23
  private calculatorProvider;
25
24
  private initializationPromise;
26
25
  private lifecycleGeneration;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Shared lazy-module and lifecycle implementation for calculator tool adapters.
3
- * Concrete adapters own only their metadata, capabilities and provider import.
3
+ * Concrete adapters own only their metadata and provider import.
4
4
  */
5
5
  export class LazyCalculatorToolProvider {
6
6
  category = "calculator";
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  export type { PackagedToolRegistryOptions } from "./packaged-capability-composition.js";
2
2
  export { createEmptyPersonalNeedsProfile, createPackagedToolRegistry, createUniversalPersonalNeedsProfile, DEFAULT_TOOL_MODULE_LOADERS, PACKAGED_TOOL_REGISTRATIONS, PACKAGED_TOOL_TAG_MAP, SECTION_PLAYER_PREFERRED_TOOL_PLACEMENT, } from "./packaged-capability-composition.js";
3
+ export { type AfaFeatureSet, type AfaPnpRecord, type AfaSpoken, createPersonalNeedsProfileFromAfa, } from "./afa-profile.js";
3
4
  export { CONTENT_ALTERNATE_REGISTRATIONS } from "./content-alternates.js";
4
5
  export { annotationToolbarRegistration, lineReaderToolRegistration, themeToolRegistration, } from "./registrations/accessibility-tools.js";
5
- export { AUDIO_TRANSCRIPT_FEATURE_ID, AUDIO_TRANSCRIPT_REGION_CLASS, AUDIO_TRANSCRIPT_REGION_LABEL, audioTranscriptRegistration, resolveAudioTranscript, type ResolvedAudioTranscript, } from "./registrations/audio-transcript.js";
6
+ export { AUDIO_TRANSCRIPT_FEATURE_ID, AUDIO_TRANSCRIPT_REGION_CLASS, AUDIO_TRANSCRIPT_REGION_LABEL, audioTranscriptRegistration, CONTENT_LEAD_SURFACE, resolveAudioTranscript, type ResolvedAudioTranscript, } from "./registrations/audio-transcript.js";
6
7
  export { calculatorToolRegistration } from "./registrations/calculator.js";
7
8
  export { createDictionaryToolRegistration, createPictureDictionaryToolRegistration, dictionaryToolRegistration, type DictionaryVariantOptions, pictureDictionaryToolRegistration, spanishDictionaryToolRegistration, spanishPictureDictionaryToolRegistration, } from "./registrations/dictionary-tools.js";
8
9
  export { answerEliminatorToolRegistration } from "./registrations/interaction-tools.js";
package/dist/index.js CHANGED
@@ -1,11 +1,13 @@
1
1
  export { createEmptyPersonalNeedsProfile, createPackagedToolRegistry, createUniversalPersonalNeedsProfile, DEFAULT_TOOL_MODULE_LOADERS, PACKAGED_TOOL_REGISTRATIONS, PACKAGED_TOOL_TAG_MAP, SECTION_PLAYER_PREFERRED_TOOL_PLACEMENT, } from "./packaged-capability-composition.js";
2
+ // A profile from an AfA PNP 3.0 record, in the packaged set's support ids.
3
+ export { createPersonalNeedsProfileFromAfa, } from "./afa-profile.js";
2
4
  // The authored-alternate subset, for a renderer that shows alternates and no
3
5
  // toolbar — importable without the packaged composition's tool loaders.
4
6
  export { CONTENT_ALTERNATE_REGISTRATIONS } from "./content-alternates.js";
5
7
  // Individual registrations, for a host composing a subset rather than the whole
6
8
  // packaged set.
7
9
  export { annotationToolbarRegistration, lineReaderToolRegistration, themeToolRegistration, } from "./registrations/accessibility-tools.js";
8
- export { AUDIO_TRANSCRIPT_FEATURE_ID, AUDIO_TRANSCRIPT_REGION_CLASS, AUDIO_TRANSCRIPT_REGION_LABEL, audioTranscriptRegistration, resolveAudioTranscript, } from "./registrations/audio-transcript.js";
10
+ export { AUDIO_TRANSCRIPT_FEATURE_ID, AUDIO_TRANSCRIPT_REGION_CLASS, AUDIO_TRANSCRIPT_REGION_LABEL, audioTranscriptRegistration, CONTENT_LEAD_SURFACE, resolveAudioTranscript, } from "./registrations/audio-transcript.js";
9
11
  export { calculatorToolRegistration } from "./registrations/calculator.js";
10
12
  export { createDictionaryToolRegistration, createPictureDictionaryToolRegistration, dictionaryToolRegistration, pictureDictionaryToolRegistration, spanishDictionaryToolRegistration, spanishPictureDictionaryToolRegistration, } from "./registrations/dictionary-tools.js";
11
13
  export { answerEliminatorToolRegistration } from "./registrations/interaction-tools.js";
@@ -16,7 +16,11 @@ export interface PackagedToolRegistryOptions {
16
16
  toolTagMap?: Partial<ToolTagMap>;
17
17
  /** Override the component factory per tool. */
18
18
  toolComponentFactories?: Partial<ToolComponentFactoryMap>;
19
- /** Lazy module loaders keyed by toolId. */
19
+ /**
20
+ * Module loaders keyed by toolId, each loading its tool's element on first
21
+ * render. Defaults to `DEFAULT_TOOL_MODULE_LOADERS`; a map replaces it, so a
22
+ * host that defines its tool elements itself passes `{}`.
23
+ */
20
24
  toolModuleLoaders?: Partial<Record<string, ToolModuleLoader>>;
21
25
  /** Restrict registration to specific packaged tool ids. */
22
26
  toolIds?: string[];
@@ -248,13 +248,9 @@ class PackagedCapabilityComposition {
248
248
  for (const registration of this.selectRegistrations(options.toolIds)) {
249
249
  registry.register(options.overrides?.[registration.toolId] ?? registration);
250
250
  }
251
- // Loader installation remains opt-in. Some hosts preload tool elements or
252
- // provide their own tags; silently adding package loads here would change
253
- // their bundle/runtime behavior.
254
- if (options.toolModuleLoaders &&
255
- Object.keys(options.toolModuleLoaders).length > 0) {
256
- registry.setToolModuleLoaders({ ...options.toolModuleLoaders });
257
- }
251
+ registry.setToolModuleLoaders({
252
+ ...(options.toolModuleLoaders ?? this.moduleLoaders),
253
+ });
258
254
  registry.setComponentOverrides({
259
255
  toolTagMap: {
260
256
  ...this.toolTagMap,
@@ -23,7 +23,6 @@ export const lineReaderToolRegistration = {
23
23
  nameKey: "tools.lineReader.name",
24
24
  descriptionKey: "tools.lineReader.description",
25
25
  icon: "bars-3",
26
- // Line reader appears where there's text to read
27
26
  supportedLevels: ["section", "passage", "rubric", "item"],
28
27
  /**
29
28
  * Pass 2: Line reader is relevant when readable text is present
@@ -56,7 +55,7 @@ export const themeToolRegistration = {
56
55
  * Pass 2: Color scheme is always relevant when allowed
57
56
  */
58
57
  isVisibleInContext(context) {
59
- return true; // Always show if allowed by orchestrator
58
+ return true;
60
59
  },
61
60
  renderToolbar(context, toolbarContext) {
62
61
  return renderOverlayToolbar(this, context, toolbarContext, {
@@ -86,15 +85,12 @@ export const annotationToolbarRegistration = {
86
85
  activation: "selection-gateway",
87
86
  singletonScope: "section",
88
87
  /**
89
- * Section-scoped singleton surface. The gateway used to be mounted by
90
- * `PieSectionPlayerBaseElement.svelte`, which named this tool id in three
91
- * places — the policy check, the module load and the element — so no host
92
- * could contribute a second section-scoped capability. Declaring the surface
93
- * moves that wiring into this registration, and the renderer discovers it
94
- * through `ToolRegistry.getToolsBySurface("section-overlay")`.
88
+ * Section-scoped singleton surface. The renderer discovers it through
89
+ * `ToolRegistry.getToolsBySurface("section-overlay")`, so the section player
90
+ * names no tool id and a host can contribute another section-scoped
91
+ * capability.
95
92
  */
96
93
  surfaces: ["section-overlay"],
97
- // Annotation appears where there's text content
98
94
  supportedLevels: ["passage", "rubric", "item", "element"],
99
95
  /**
100
96
  * Pass 2: Annotation is relevant when readable text is present
@@ -5,13 +5,13 @@
5
5
  * Two kinds of content wear the same shape, and the card says which:
6
6
  *
7
7
  * - `visibility: "always"` is authored presentation. The item family was
8
- * designed to be delivered with its transcript on screen — Star Early
9
- * Literacy sentence items, for instance — so no profile grants it and none
10
- * revokes it.
8
+ * designed to be delivered with its transcript on screen — sentence items in
9
+ * an early-literacy assessment, for instance — so no profile grants it and
10
+ * none revokes it.
11
11
  * - anything else, `"onGrant"` included, is the accommodation. Policy decides
12
- * against the `transcript` support id, and silence means no. For the three
13
- * Star families where a visible transcript turns the item into a reading
14
- * task, that gate is the whole point.
12
+ * against the `transcript` support id, and silence means no. For item
13
+ * families where a visible transcript turns the item into a reading task,
14
+ * that gate is the whole point.
15
15
  *
16
16
  * The toolkit renders the text, not the element. Before this, the transcript rode
17
17
  * on `model.audioTranscript` and `mc-populated-blank` revealed its own copy when
@@ -47,6 +47,7 @@ export declare const AUDIO_TRANSCRIPT_REGION_CLASS = "pie-tool-audio-transcript"
47
47
  export interface ResolvedAudioTranscript {
48
48
  catalogId: string;
49
49
  text: string;
50
+ /** BCP 47, as the region's `lang` takes it, whatever syntax the card used. */
50
51
  language?: string;
51
52
  /** True when the card declares itself authored presentation. */
52
53
  always: boolean;
@@ -5,13 +5,13 @@
5
5
  * Two kinds of content wear the same shape, and the card says which:
6
6
  *
7
7
  * - `visibility: "always"` is authored presentation. The item family was
8
- * designed to be delivered with its transcript on screen — Star Early
9
- * Literacy sentence items, for instance — so no profile grants it and none
10
- * revokes it.
8
+ * designed to be delivered with its transcript on screen — sentence items in
9
+ * an early-literacy assessment, for instance — so no profile grants it and
10
+ * none revokes it.
11
11
  * - anything else, `"onGrant"` included, is the accommodation. Policy decides
12
- * against the `transcript` support id, and silence means no. For the three
13
- * Star families where a visible transcript turns the item into a reading
14
- * task, that gate is the whole point.
12
+ * against the `transcript` support id, and silence means no. For item
13
+ * families where a visible transcript turns the item into a reading task,
14
+ * that gate is the whole point.
15
15
  *
16
16
  * The toolkit renders the text, not the element. Before this, the transcript rode
17
17
  * on `model.audioTranscript` and `mc-populated-blank` revealed its own copy when
@@ -35,6 +35,7 @@
35
35
  * absent from the universal preset — a content-dependent support never belongs in
36
36
  * a wholesale grant.
37
37
  */
38
+ import { toLanguageTag } from "@pie-players/pie-players-shared/i18n/language-tags";
38
39
  /** QTI 3.0 / AfA support id gating an audio transcript. */
39
40
  export const AUDIO_TRANSCRIPT_FEATURE_ID = "transcript";
40
41
  /** Catalog type carrying the transcript text. */
@@ -55,6 +56,10 @@ const isTranscriptCard = (card) => !!card && card.catalog === AUDIO_TRANSCRIPT_C
55
56
  const isAlwaysVisible = (card) => typeof card.visibility === "string" &&
56
57
  card.visibility.trim().toLowerCase() === ALWAYS_VISIBLE;
57
58
  const cardText = (card) => typeof card.content === "string" ? card.content.trim() : "";
59
+ // The Learnosity transform emits POSIX `es_ES`, which `lang` does not accept.
60
+ const cardLanguage = (card) => typeof card.language === "string"
61
+ ? toLanguageTag(card.language) || undefined
62
+ : undefined;
58
63
  /**
59
64
  * The transcript an entity carries, or `null`.
60
65
  *
@@ -83,7 +88,7 @@ export function resolveAudioTranscript(context) {
83
88
  const resolved = {
84
89
  catalogId,
85
90
  text,
86
- language: typeof card.language === "string" ? card.language : undefined,
91
+ language: cardLanguage(card),
87
92
  always: isAlwaysVisible(card),
88
93
  };
89
94
  // An `always` card answers whether or not policy granted anything, so it
@@ -54,7 +54,10 @@ function normalizeCalculatorType(value) {
54
54
  }
55
55
  function getCalculatorRenderParams(toolbarContext) {
56
56
  const params = toolbarContext.getToolRenderParams?.("calculator") ?? {};
57
- const calculatorType = normalizeCalculatorType(params.calculatorType);
57
+ // A host resolver or a request names the type; the policy parameters'
58
+ // `type` is the default beneath both.
59
+ const calculatorType = normalizeCalculatorType(params.calculatorType) ??
60
+ normalizeCalculatorType(toolbarContext.getToolParameters?.("calculator")?.type);
58
61
  const availableTypesRaw = params.availableTypes;
59
62
  const availableTypes = Array.isArray(availableTypesRaw)
60
63
  ? availableTypesRaw
@@ -135,7 +138,6 @@ export const calculatorToolRegistration = {
135
138
  * (MathML, LaTeX, arithmetic markers).
136
139
  */
137
140
  isVisibleInContext(context) {
138
- // Show only when math is present in item content.
139
141
  return hasMathContent(context);
140
142
  },
141
143
  renderToolbar(context, toolbarContext) {
@@ -156,8 +158,7 @@ export const calculatorToolRegistration = {
156
158
  icon: typeof this.icon === "function" ? this.icon(context) : this.icon,
157
159
  // Routes this button through <nds-icon-button> where the host enables NDS
158
160
  // icons. Declared here because which capabilities render in the host's
159
- // design system is a composition decision; the toolbar used to map it from
160
- // the `calculator` toolId, which put a capability name in the generic core.
161
+ // design system is a composition decision.
161
162
  faIconName: "calculator",
162
163
  disabled: false,
163
164
  // The name stays put across open and closed. The toolbar mirrors
@@ -248,10 +249,9 @@ export const calculatorToolRegistration = {
248
249
  const active = toolbarContext.isToolVisible(this.toolId);
249
250
  button.active = active;
250
251
  button.label = displayName;
251
- // Static across the toggle. The previous `Close ${name.toLowerCase()}`
252
- // was a hardcoded English template built by lowercasing a localized
253
- // noun — "Close rekenmachine" under nl-NL — and it is the state
254
- // `aria-pressed` already carries.
252
+ // Static across the toggle: `aria-pressed` carries the state, and a
253
+ // "Close …" template built from a localized noun mixes languages
254
+ // ("Close rekenmachine" under nl-NL).
255
255
  button.ariaLabel = displayName;
256
256
  button.tooltip = displayName;
257
257
  if (lastVisibleState !== active) {
@@ -9,6 +9,6 @@ import type { ToolRegistration } from "@pie-players/pie-assessment-toolkit/tools
9
9
  * Answer Eliminator tool registration
10
10
  *
11
11
  * Allows students to strike through incorrect answer choices.
12
- * Only appears on multiple-choice style questions.
12
+ * Only appears on items with an element that supports answer masking.
13
13
  */
14
14
  export declare const answerEliminatorToolRegistration: ToolRegistration;
@@ -4,12 +4,40 @@
4
4
  * Registers tools for interacting with question content:
5
5
  * - Answer Eliminator (strike through answer choices)
6
6
  */
7
- import { hasChoiceInteraction } from "@pie-players/pie-assessment-toolkit/tools/registration";
7
+ import { elementSupports } from "@pie-players/pie-assessment-toolkit/tools/registration";
8
8
  import { createToolElement, } from "@pie-players/pie-assessment-toolkit/tools/registration";
9
9
  import { createScopedVisibilityBinding, syncButtonAndOverlayVisibility, } from "@pie-players/pie-assessment-toolkit/tools/registration";
10
10
  import { resolveOverlayElement } from "./overlay-element-cache.js";
11
- /** `tools.providers.answerEliminator.strategy`, defaulting to strikethrough. */
11
+ /** The policy parameters' `strategy`, defaulting to strikethrough. */
12
12
  const normalizeEliminationStrategy = (value) => value === "mask" ? "mask" : "strikethrough";
13
+ /** The AfA PNP 3.0 support term an element declares to take the eliminator. */
14
+ const ANSWER_MASKING = "answer-masking";
15
+ /**
16
+ * pie-item contract compatibility: legacy pie-elements, and pie-elements-ng
17
+ * releases from before elements declared their supports, render the markup the
18
+ * eliminator's built-in adapters read for these two. An element that declares
19
+ * its supports is decided by its declaration, these two included.
20
+ */
21
+ const UNDECLARED_ANSWER_MASKING_ELEMENTS = new Set(["multiple-choice", "ebsr"]);
22
+ // Content names one element several ways: `multiple-choice`,
23
+ // `pie-multiple-choice`, `pie-element-multiple-choice`, a runtime `--version-*`
24
+ // tag, or any tag the item's `elements` map binds to `@pie-element/multiple-choice`.
25
+ function isUndeclaredAnswerMaskingElement(tag, packageSpec) {
26
+ const name = tag
27
+ .toLowerCase()
28
+ .replace(/--version-[0-9a-z-]+$/, "")
29
+ .replace(/^pie-(element-)?/, "");
30
+ if (UNDECLARED_ANSWER_MASKING_ELEMENTS.has(name))
31
+ return true;
32
+ const packageName = packageSpec?.match(/^@pie-element\/([^@]+)/)?.[1];
33
+ return !!packageName && UNDECLARED_ANSWER_MASKING_ELEMENTS.has(packageName);
34
+ }
35
+ /** Whether an element of the context supports answer masking. */
36
+ function supportsAnswerMasking(context) {
37
+ return elementSupports(context).some(({ supports, tag, packageSpec }) => supports
38
+ ? supports.includes(ANSWER_MASKING)
39
+ : isUndeclaredAnswerMaskingElement(tag, packageSpec));
40
+ }
13
41
  /** The state-store key of each PIE element in the item, by model id. */
14
42
  function resolveElementStateKeys(context, toolbarContext) {
15
43
  const keys = {};
@@ -29,7 +57,7 @@ function resolveElementStateKeys(context, toolbarContext) {
29
57
  * Answer Eliminator tool registration
30
58
  *
31
59
  * Allows students to strike through incorrect answer choices.
32
- * Only appears on multiple-choice style questions.
60
+ * Only appears on items with an element that supports answer masking.
33
61
  */
34
62
  export const answerEliminatorToolRegistration = {
35
63
  toolId: "answerEliminator",
@@ -38,32 +66,31 @@ export const answerEliminatorToolRegistration = {
38
66
  nameKey: "tools.answerEliminator.name",
39
67
  descriptionKey: "tools.answerEliminator.description",
40
68
  icon: "strikethrough",
41
- // Answer eliminator appears at item level only
42
69
  supportedLevels: ["item"],
43
70
  /**
44
- * Pass 2: Answer eliminator is relevant only for choice-based questions
71
+ * Pass 2: relevant where an element supports answer masking. An element
72
+ * declares it on its class, and the answer eliminator's README says what the
73
+ * element then supplies.
45
74
  */
46
75
  isVisibleInContext(context) {
47
- return hasChoiceInteraction(context);
76
+ return supportsAnswerMasking(context);
48
77
  },
49
78
  /**
50
79
  * Pass 3: the same question, asked as a capability. Elimination controls are
51
- * rendered per choice, so an item with no choice interaction gives this tool
52
- * nothing to act on — `placement-ordering`, `categorize` and
53
- * `drag-in-the-blank` included, whose `choices` hold draggables the
54
- * eliminator cannot reach. Answering the relevance gate alone left the button
55
- * on those items for any learner whose profile grants answer masking.
80
+ * rendered per choice, so an item none of whose elements supports answer
81
+ * masking gives this tool nothing to act on. Answering the relevance gate
82
+ * alone left the button on such items for any learner whose profile grants
83
+ * answer masking.
56
84
  */
57
85
  isApplicableToContent(context) {
58
- return hasChoiceInteraction(context);
86
+ return supportsAnswerMasking(context);
59
87
  },
60
88
  renderToolbar(context, toolbarContext) {
61
89
  const visibility = createScopedVisibilityBinding(this.toolId, toolbarContext);
62
90
  const componentOverrides = toolbarContext.componentOverrides ?? {};
63
91
  const overlay = resolveOverlayElement(toolbarContext, visibility.fullToolId, () => createToolElement(this.toolId, context, toolbarContext, componentOverrides));
64
- overlay.setAttribute("tool-id", visibility.fullToolId);
65
92
  overlay.setAttribute("button-alignment", "inline");
66
- overlay.setAttribute("strategy", normalizeEliminationStrategy(toolbarContext.toolkitCoordinator?.getToolConfig(this.toolId)?.strategy));
93
+ overlay.setAttribute("strategy", normalizeEliminationStrategy(toolbarContext.getToolParameters?.("answerEliminator")?.strategy));
67
94
  const button = {
68
95
  toolId: this.toolId,
69
96
  label: this.name,
@@ -84,7 +111,6 @@ export const answerEliminatorToolRegistration = {
84
111
  overlay,
85
112
  isActive: visibility.isActive,
86
113
  });
87
- overlay.scopeElement = toolbarContext.getScopeElement?.() || null;
88
114
  overlay.elementStateKeys = resolveElementStateKeys(context, toolbarContext);
89
115
  },
90
116
  subscribeActive: visibility.subscribeActive,
@@ -2,18 +2,15 @@
2
2
  * The `renderToolbar` shape shared by every packaged capability that puts one
3
3
  * overlay behind one toolbar button.
4
4
  *
5
- * Seven registrations had this inline. What actually varies between them is the
6
- * two options below and nothing else: whether the overlay paints its own chrome
7
- * or takes a draggable window, and that window's size. Everything else — the visibility
8
- * binding, the button, the component-override lookup, the `tool-id` attribute, the
9
- * sync — was identical, which is why one rule landing in three spellings was
10
- * possible: the shell title was `resolveToolRegistrationName` in two files,
11
- * `displayName` in a third and a raw `t(key)` in a fourth.
5
+ * What varies between capabilities is the two options below: whether the overlay
6
+ * paints its own chrome or takes a draggable window, and that window's size. The
7
+ * visibility binding, the button, the component-override lookup, the `tool-id`
8
+ * attribute and the sync are shared, so the shell title has one spelling.
12
9
  *
13
- * Catalog keys are derived from `toolId`, which held at all seven sites:
14
- * `tools.<toolId>.buttonA11y` and `tools.<toolId>.tooltip`. A capability needing a
15
- * different prefix — the dictionary variants, which compose several capabilities
16
- * off one element — keeps its own renderer.
10
+ * Catalog keys are derived from `toolId`: `tools.<toolId>.buttonA11y` and
11
+ * `tools.<toolId>.tooltip`. A capability needing a different prefix — the
12
+ * dictionary variants, which compose several capabilities off one element —
13
+ * keeps its own renderer.
17
14
  */
18
15
  import { type ToolContext, type ToolRegistration, type ToolToolbarRenderResult, type ToolbarContext } from "@pie-players/pie-assessment-toolkit/tools/registration";
19
16
  /** A window's geometry. Absent for overlays that paint their own chrome. */
@@ -2,18 +2,15 @@
2
2
  * The `renderToolbar` shape shared by every packaged capability that puts one
3
3
  * overlay behind one toolbar button.
4
4
  *
5
- * Seven registrations had this inline. What actually varies between them is the
6
- * two options below and nothing else: whether the overlay paints its own chrome
7
- * or takes a draggable window, and that window's size. Everything else — the visibility
8
- * binding, the button, the component-override lookup, the `tool-id` attribute, the
9
- * sync — was identical, which is why one rule landing in three spellings was
10
- * possible: the shell title was `resolveToolRegistrationName` in two files,
11
- * `displayName` in a third and a raw `t(key)` in a fourth.
5
+ * What varies between capabilities is the two options below: whether the overlay
6
+ * paints its own chrome or takes a draggable window, and that window's size. The
7
+ * visibility binding, the button, the component-override lookup, the `tool-id`
8
+ * attribute and the sync are shared, so the shell title has one spelling.
12
9
  *
13
- * Catalog keys are derived from `toolId`, which held at all seven sites:
14
- * `tools.<toolId>.buttonA11y` and `tools.<toolId>.tooltip`. A capability needing a
15
- * different prefix — the dictionary variants, which compose several capabilities
16
- * off one element — keeps its own renderer.
10
+ * Catalog keys are derived from `toolId`: `tools.<toolId>.buttonA11y` and
11
+ * `tools.<toolId>.tooltip`. A capability needing a different prefix — the
12
+ * dictionary variants, which compose several capabilities off one element —
13
+ * keeps its own renderer.
17
14
  */
18
15
  import { applyOverlaySurface, createScopedVisibilityBinding, createToolElement, resolveToolRegistrationName, syncButtonAndOverlayVisibility, } from "@pie-players/pie-assessment-toolkit/tools/registration";
19
16
  import { resolveOverlayElement } from "./overlay-element-cache.js";
@@ -4,13 +4,12 @@
4
4
  * Registers the TTS tool for reading content aloud.
5
5
  */
6
6
  import type { ToolRegistration } from "@pie-players/pie-assessment-toolkit/tools/registration";
7
- export declare const TOOL_ACTIVE_CHANGE_EVENT = "pie-tool-active-change";
8
7
  /**
9
8
  * Text-to-Speech tool registration
10
9
  *
11
10
  * Supports:
12
11
  * - Reading content aloud using browser TTS or external providers
13
12
  * - Context-aware visibility (shows when readable text is available)
14
- * - All levels except assessment and element
13
+ * - Item and passage levels
15
14
  */
16
15
  export declare const ttsToolRegistration: ToolRegistration;
@@ -3,22 +3,22 @@
3
3
  *
4
4
  * Registers the TTS tool for reading content aloud.
5
5
  */
6
- import { hasReadableText } from "@pie-players/pie-assessment-toolkit/tools/registration";
7
- import { createScopedToolId, createToolElement, } from "@pie-players/pie-assessment-toolkit/tools/registration";
8
- import { buildRuntimeTTSConfig, normalizeTTSLayoutMode, normalizeTTSSpeedControlOptions, resolveTTSHostToolbarLayout, resolveTTSLayoutMode, resolveTTSRuntimeSettings, } from "@pie-players/pie-assessment-toolkit/tools/registration";
6
+ import { hasSpokenContent } from "@pie-players/pie-assessment-toolkit/tools/registration";
7
+ import { createScopedToolId, createToolElement, TOOL_ACTIVE_CHANGE_EVENT, } from "@pie-players/pie-assessment-toolkit/tools/registration";
8
+ import { buildRuntimeTTSConfig, normalizeTTSSpeedControlOptions, resolveTTSHostToolbarLayout, resolveTTSRuntimeSettings, } from "@pie-players/pie-assessment-toolkit/tools/registration";
9
9
  import { TTSToolProvider } from "@pie-players/pie-assessment-toolkit/tools/registration";
10
10
  import { resolveOverlayElement } from "./overlay-element-cache.js";
11
11
  // This package owns the server adapter's import, so the toolkit never names it
12
12
  // and a bundler building the toolkit alone has nothing to resolve.
13
13
  const loadServerTTSProvider = async () => (await import("@pie-players/tts-client-server")).ServerTTSProvider;
14
- export const TOOL_ACTIVE_CHANGE_EVENT = "pie-tool-active-change";
14
+ const numberOrUndefined = (value) => typeof value === "number" ? value : undefined;
15
15
  /**
16
16
  * Text-to-Speech tool registration
17
17
  *
18
18
  * Supports:
19
19
  * - Reading content aloud using browser TTS or external providers
20
20
  * - Context-aware visibility (shows when readable text is available)
21
- * - All levels except assessment and element
21
+ * - Item and passage levels
22
22
  */
23
23
  export const ttsToolRegistration = {
24
24
  toolId: "textToSpeech",
@@ -46,9 +46,6 @@ export const ttsToolRegistration = {
46
46
  const normalizedConfig = {
47
47
  ...config,
48
48
  };
49
- if ("layoutMode" in normalizedConfig) {
50
- normalizedConfig.layoutMode = normalizeTTSLayoutMode(normalizedConfig.layoutMode);
51
- }
52
49
  if ("speedOptions" in normalizedConfig) {
53
50
  normalizedConfig.speedOptions = normalizeTTSSpeedControlOptions(normalizedConfig.speedOptions);
54
51
  }
@@ -59,11 +56,10 @@ export const ttsToolRegistration = {
59
56
  /**
60
57
  * Pass 2: Determine if TTS is relevant in this context
61
58
  *
62
- * TTS is relevant when:
63
- * - Context contains readable text (at least 10 characters)
59
+ * TTS is relevant when the context holds readable text or a catalog idref.
64
60
  */
65
61
  isVisibleInContext(context) {
66
- return hasReadableText(context);
62
+ return hasSpokenContent(context);
67
63
  },
68
64
  renderToolbar(context, toolbarContext) {
69
65
  const resolveRuntimeSettings = () => resolveTTSRuntimeSettings(toolbarContext.toolkitCoordinator?.getToolConfig(this.toolId) ||
@@ -72,19 +68,7 @@ export const ttsToolRegistration = {
72
68
  const runtimeSettings = resolveRuntimeSettings();
73
69
  return normalizeTTSSpeedControlOptions(runtimeSettings.speedOptions);
74
70
  };
75
- const resolveLayoutMode = () => resolveTTSLayoutMode(resolveRuntimeSettings());
76
- const resolveHostLayout = () => resolveTTSHostToolbarLayout(resolveRuntimeSettings());
77
71
  const fullToolId = createScopedToolId(this.toolId, toolbarContext.scope.level, toolbarContext.scope.scopeId);
78
- // Only a language the toolbar or its host named: unnamed, the control resolves
79
- // markup `lang`, and the browser voice follows the browser's language.
80
- const applyContentLanguage = (element) => {
81
- if (toolbarContext.language) {
82
- element.setAttribute("language", toolbarContext.language);
83
- }
84
- else {
85
- element.removeAttribute("language");
86
- }
87
- };
88
72
  const resolveControlSize = () => {
89
73
  const raw = toolbarContext.ui?.size;
90
74
  return raw === "sm" || raw === "lg" ? raw : "md";
@@ -92,22 +76,26 @@ export const ttsToolRegistration = {
92
76
  const element = resolveOverlayElement(toolbarContext, fullToolId, () => createToolElement(this.toolId, context, toolbarContext, toolbarContext.componentOverrides));
93
77
  const applyAttributes = () => {
94
78
  element.setAttribute("catalog-id", toolbarContext.catalogId || toolbarContext.itemId);
95
- applyContentLanguage(element);
96
79
  element.setAttribute("size", resolveControlSize());
97
- element.setAttribute("layout-mode", resolveLayoutMode());
80
+ element.setAttribute("layout-mode", resolveRuntimeSettings().layoutMode);
98
81
  element.speedOptions = resolveElementSpeedOptions();
99
82
  element.showSingleSpeedOption =
100
83
  resolveRuntimeSettings().showSingleSpeedOption === true;
84
+ // The element range-checks these; a non-number means the default.
85
+ const speech = toolbarContext.getToolParameters?.("textToSpeech");
86
+ element.baseRate = numberOrUndefined(speech?.rate);
87
+ element.pitch = numberOrUndefined(speech?.pitch);
88
+ element.volume = numberOrUndefined(speech?.volume);
101
89
  };
102
90
  applyAttributes();
103
- const hostLayout = resolveHostLayout();
91
+ const hostLayout = resolveTTSHostToolbarLayout(resolveRuntimeSettings());
104
92
  return {
105
93
  toolId: this.toolId,
106
94
  button: null,
107
95
  elements: [
108
96
  {
109
97
  element,
110
- mount: hostLayout.mount,
98
+ mount: "before-buttons",
111
99
  layoutHints: {
112
100
  controlsRow: {
113
101
  reserveSpace: hostLayout.controlsRow.reserveSpace,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pie-players/pie-default-tool-loaders",
3
- "version": "0.3.75",
3
+ "version": "1.0.0-beta.1",
4
4
  "type": "module",
5
5
  "description": "Composition layer for the packaged PIE assessment tools: registrations, tag map, placement preset, universal and empty profiles, and lazy module loaders",
6
6
  "repository": {
@@ -33,25 +33,25 @@
33
33
  "test": "bun test"
34
34
  },
35
35
  "dependencies": {
36
- "@pie-players/pie-assessment-toolkit": "0.3.75",
37
- "@pie-players/pie-calculator": "0.3.75",
38
- "@pie-players/pie-calculator-cortex": "0.3.75",
39
- "@pie-players/pie-calculator-desmos": "0.3.75",
40
- "@pie-players/pie-calculator-geogebra": "0.3.75",
41
- "@pie-players/pie-players-shared": "0.3.75",
42
- "@pie-players/pie-tool-annotation-toolbar": "0.3.75",
43
- "@pie-players/pie-tool-answer-eliminator": "0.3.75",
44
- "@pie-players/pie-tool-calculator-shared": "0.3.75",
45
- "@pie-players/pie-tool-dictionary": "0.3.75",
46
- "@pie-players/pie-tool-graph": "0.3.75",
47
- "@pie-players/pie-tool-line-reader": "0.3.75",
48
- "@pie-players/pie-tool-periodic-table": "0.3.75",
49
- "@pie-players/pie-tool-picture-dictionary": "0.3.75",
50
- "@pie-players/pie-tool-protractor": "0.3.75",
51
- "@pie-players/pie-tool-ruler": "0.3.75",
52
- "@pie-players/pie-tool-theme": "0.3.75",
53
- "@pie-players/pie-tool-tts-inline": "0.3.75",
54
- "@pie-players/tts-client-server": "0.3.75"
36
+ "@pie-players/pie-assessment-toolkit": "1.0.0-beta.1",
37
+ "@pie-players/pie-calculator": "1.0.0-beta.1",
38
+ "@pie-players/pie-calculator-cortex": "1.0.0-beta.1",
39
+ "@pie-players/pie-calculator-desmos": "1.0.0-beta.1",
40
+ "@pie-players/pie-calculator-geogebra": "1.0.0-beta.1",
41
+ "@pie-players/pie-players-shared": "1.0.0-beta.1",
42
+ "@pie-players/pie-tool-annotation-toolbar": "1.0.0-beta.1",
43
+ "@pie-players/pie-tool-answer-eliminator": "1.0.0-beta.1",
44
+ "@pie-players/pie-tool-calculator-shared": "1.0.0-beta.1",
45
+ "@pie-players/pie-tool-dictionary": "1.0.0-beta.1",
46
+ "@pie-players/pie-tool-graph": "1.0.0-beta.1",
47
+ "@pie-players/pie-tool-line-reader": "1.0.0-beta.1",
48
+ "@pie-players/pie-tool-periodic-table": "1.0.0-beta.1",
49
+ "@pie-players/pie-tool-picture-dictionary": "1.0.0-beta.1",
50
+ "@pie-players/pie-tool-protractor": "1.0.0-beta.1",
51
+ "@pie-players/pie-tool-ruler": "1.0.0-beta.1",
52
+ "@pie-players/pie-tool-theme": "1.0.0-beta.1",
53
+ "@pie-players/pie-tool-tts-inline": "1.0.0-beta.1",
54
+ "@pie-players/tts-client-server": "1.0.0-beta.1"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@biomejs/biome": "^2.5.15",