@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.
- package/README.md +81 -9
- package/dist/attempt/AssessmentSession.d.ts +7 -27
- package/dist/components/ItemToolBar.custom-element.js +1 -1
- package/dist/components/PieAssessmentToolkit.custom-element.js +14 -14
- package/dist/components/SectionToolBar.custom-element.js +1 -1
- package/dist/components/chunks/ItemToolBar-pe5szfyx.js +46 -0
- package/dist/components/chunks/ItemToolBar-rd7te9r0.js +51 -0
- package/dist/components/item-toolbar-element.js +1 -0
- package/dist/components/pie-assessment-toolkit-element.js +1 -0
- package/dist/components/section-toolbar-element.js +1 -0
- package/dist/context/assessment-toolkit-context.d.ts +28 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +4 -2
- package/dist/policy/core/compose-decision.js +14 -0
- package/dist/policy/core/decision-types.d.ts +1 -1
- package/dist/policy/sources/PnpPolicySource.d.ts +8 -0
- package/dist/policy/sources/PnpPolicySource.js +19 -10
- package/dist/runtime/SectionRuntimeEngine.d.ts +56 -0
- package/dist/runtime/SectionRuntimeEngine.js +66 -1
- package/dist/runtime/core/engine-resolver.d.ts +25 -1
- package/dist/runtime/registration-events.d.ts +52 -0
- package/dist/runtime/registration-events.js +2 -0
- package/dist/services/AccessibilityCatalogResolver.js +21 -5
- package/dist/services/I18nService.d.ts +28 -100
- package/dist/services/I18nService.js +43 -233
- package/dist/services/TTSService.d.ts +12 -0
- package/dist/services/TTSService.js +18 -17
- package/dist/services/ToolRegistry.d.ts +120 -2
- package/dist/services/ToolRegistry.js +64 -0
- package/dist/services/ToolkitCoordinator.d.ts +39 -37
- package/dist/services/ToolkitCoordinator.js +40 -0
- package/dist/services/audio-handoff.d.ts +39 -0
- package/dist/services/audio-handoff.js +58 -0
- package/dist/services/catalog-media.d.ts +35 -4
- package/dist/services/catalog-media.js +92 -3
- package/dist/services/framework-error.d.ts +15 -1
- package/dist/services/interfaces.d.ts +28 -0
- package/dist/services/pnp-standard-features.d.ts +1 -1
- package/dist/services/section-controller-types.d.ts +218 -7
- package/dist/services/selection-action.d.ts +49 -0
- package/dist/services/selection-action.js +10 -0
- package/dist/services/spoken-audio-cards.js +5 -1
- package/dist/services/tool-context.d.ts +6 -5
- package/dist/services/tool-context.js +205 -155
- package/dist/services/tool-icons.d.ts +18 -0
- package/dist/services/tool-icons.js +31 -0
- package/dist/services/tool-providers/CortexToolProvider.d.ts +18 -0
- package/dist/services/tool-providers/CortexToolProvider.js +32 -0
- package/dist/services/tool-providers/DesmosToolProvider.d.ts +13 -101
- package/dist/services/tool-providers/DesmosToolProvider.js +14 -145
- package/dist/services/tool-providers/GeoGebraToolProvider.d.ts +21 -0
- package/dist/services/tool-providers/GeoGebraToolProvider.js +32 -0
- package/dist/services/tool-providers/LazyCalculatorToolProvider.d.ts +35 -0
- package/dist/services/tool-providers/LazyCalculatorToolProvider.js +95 -0
- package/dist/services/tool-providers/index.d.ts +4 -0
- package/dist/services/tool-providers/index.js +2 -0
- package/dist/services/tool-request.d.ts +106 -0
- package/dist/services/tool-request.js +127 -0
- package/dist/services/toolbar-items.d.ts +6 -0
- package/dist/tools/client.d.ts +0 -2
- package/dist/tools/client.js +0 -4
- package/dist/tools/internal.d.ts +7 -1
- package/dist/tools/internal.js +8 -1
- package/dist/tools/tool-surface-host.d.ts +57 -0
- package/dist/tools/tool-surface-host.js +610 -0
- package/dist/tools/types.d.ts +1 -66
- package/package.json +22 -12
- package/dist/components/chunks/ItemToolBar-8jgdz50p.js +0 -51
- package/dist/components/chunks/ItemToolBar-cvs646j3.js +0 -36
- package/dist/tools/calculators/desmos-provider.d.ts +0 -46
- package/dist/tools/calculators/desmos-provider.js +0 -393
- package/dist/tools/library-loader.d.ts +0 -62
- package/dist/tools/library-loader.js +0 -261
|
@@ -1,274 +1,84 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* I18nService
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* The toolkit's view of the shared i18n provider: a delegating wrapper over
|
|
5
|
+
* `SimpleI18n` from `players-shared` that adds toolkit-scoped logging and wires
|
|
6
|
+
* the lazy catalog loaders.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* - Pluralization: ICU-style plural rules
|
|
12
|
-
* - RTL detection: Automatic based on locale
|
|
13
|
-
* - Fallback to English for missing keys
|
|
8
|
+
* It was once a near-verbatim second copy of that class — same fields, same
|
|
9
|
+
* lookup, same plural selection. Duplicate implementations of one contract drift
|
|
10
|
+
* silently, because nothing fails when only one of them is fixed. Keep this a
|
|
11
|
+
* wrapper.
|
|
14
12
|
*
|
|
15
13
|
* Part of PIE Assessment Toolkit.
|
|
16
14
|
*/
|
|
15
|
+
import { createPieI18n } from "@pie-players/pie-players-shared/i18n";
|
|
17
16
|
import { createLogger } from "../utils/logger.js";
|
|
18
17
|
const log = createLogger("I18nService");
|
|
19
18
|
/**
|
|
20
|
-
*
|
|
19
|
+
* Toolkit-scoped i18n service.
|
|
21
20
|
*
|
|
22
|
-
*
|
|
21
|
+
* Serves every locale this repository ships, English resident and the rest
|
|
22
|
+
* lazily loaded.
|
|
23
23
|
*/
|
|
24
24
|
export class I18nService {
|
|
25
|
-
|
|
26
|
-
fallbackLocale = "en";
|
|
27
|
-
direction = "ltr";
|
|
28
|
-
translations = new Map();
|
|
29
|
-
listeners = new Set();
|
|
30
|
-
loadingPromises = new Map();
|
|
31
|
-
config;
|
|
25
|
+
i18n;
|
|
32
26
|
constructor(config = {}) {
|
|
33
|
-
this.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
for (const [locale, bundle] of Object.entries(config.bundledTranslations)) {
|
|
39
|
-
this.translations.set(locale, bundle);
|
|
40
|
-
log(`Loaded bundled translations for: ${locale}`);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
27
|
+
this.i18n = createPieI18n(config);
|
|
28
|
+
log("I18nService created", {
|
|
29
|
+
locale: this.i18n.getLocale(),
|
|
30
|
+
fallbackLocale: config.fallbackLocale ?? "en-US",
|
|
31
|
+
});
|
|
43
32
|
}
|
|
44
33
|
/**
|
|
45
|
-
*
|
|
34
|
+
* Set the locale, loading its catalog first.
|
|
35
|
+
*
|
|
36
|
+
* Never falls back to the browser locale: a rendered-string change reaches a
|
|
37
|
+
* host's live delivery on their next install with no build signal on their
|
|
38
|
+
* side, so the locale has to be something the host asked for.
|
|
46
39
|
*/
|
|
47
40
|
async initialize(config) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
log(`Initializing with locale: ${locale}`);
|
|
51
|
-
await this.setLocale(locale);
|
|
41
|
+
log(`Initializing with locale: ${config.locale ?? this.i18n.getLocale()}`);
|
|
42
|
+
await this.i18n.initialize(config);
|
|
52
43
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
*
|
|
56
|
-
* @param key Translation key (e.g., 'common.save')
|
|
57
|
-
* @param params Optional parameters for interpolation
|
|
58
|
-
* @returns Translated string
|
|
59
|
-
*/
|
|
60
|
-
t(key, params) {
|
|
61
|
-
const translation = this.getTranslation(key);
|
|
62
|
-
return this.interpolate(translation, params);
|
|
44
|
+
t(key, values) {
|
|
45
|
+
return this.i18n.t(key, values);
|
|
63
46
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
*
|
|
67
|
-
* @param key Translation key
|
|
68
|
-
* @param count Count for pluralization
|
|
69
|
-
* @param params Optional parameters for interpolation
|
|
70
|
-
* @returns Translated string with plural form
|
|
71
|
-
*/
|
|
72
|
-
tn(key, count, params) {
|
|
73
|
-
const translation = this.getTranslation(key);
|
|
74
|
-
if (typeof translation === "object") {
|
|
75
|
-
const pluralForm = this.selectPluralForm(count, this.locale);
|
|
76
|
-
const text = translation[pluralForm] || translation.other;
|
|
77
|
-
return this.interpolate(text, { ...params, count });
|
|
78
|
-
}
|
|
79
|
-
return this.interpolate(translation, { ...params, count });
|
|
47
|
+
plural(key, options) {
|
|
48
|
+
return this.i18n.plural(key, options);
|
|
80
49
|
}
|
|
81
|
-
/**
|
|
82
|
-
* Get current locale
|
|
83
|
-
*/
|
|
84
50
|
getLocale() {
|
|
85
|
-
return this.
|
|
51
|
+
return this.i18n.getLocale();
|
|
86
52
|
}
|
|
87
|
-
/**
|
|
88
|
-
* Change locale (triggers async loading if needed)
|
|
89
|
-
*
|
|
90
|
-
* @param locale Locale code (e.g., 'en', 'es', 'zh', 'ar')
|
|
91
|
-
*/
|
|
92
53
|
async setLocale(locale) {
|
|
93
54
|
log(`Setting locale to: ${locale}`);
|
|
94
|
-
|
|
95
|
-
if (this.loadingPromises.has(locale)) {
|
|
96
|
-
log(`Locale ${locale} is already loading, waiting...`);
|
|
97
|
-
await this.loadingPromises.get(locale);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
// Check if already loaded
|
|
101
|
-
if (this.translations.has(locale)) {
|
|
102
|
-
log(`Locale ${locale} already loaded`);
|
|
103
|
-
this.applyLocale(locale);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
// Load translations
|
|
107
|
-
const loadingPromise = this.loadTranslationsForLocale(locale);
|
|
108
|
-
this.loadingPromises.set(locale, loadingPromise);
|
|
109
|
-
try {
|
|
110
|
-
await loadingPromise;
|
|
111
|
-
this.applyLocale(locale);
|
|
112
|
-
}
|
|
113
|
-
finally {
|
|
114
|
-
this.loadingPromises.delete(locale);
|
|
115
|
-
}
|
|
55
|
+
await this.i18n.setLocale(locale);
|
|
116
56
|
}
|
|
117
|
-
/**
|
|
118
|
-
* Get current text direction
|
|
119
|
-
*/
|
|
120
57
|
getDirection() {
|
|
121
|
-
return this.
|
|
58
|
+
return this.i18n.getDirection();
|
|
122
59
|
}
|
|
123
|
-
/**
|
|
124
|
-
* Get available locales
|
|
125
|
-
*/
|
|
126
60
|
getAvailableLocales() {
|
|
127
|
-
return
|
|
61
|
+
return this.i18n.getAvailableLocales();
|
|
128
62
|
}
|
|
129
|
-
/**
|
|
130
|
-
* Check if locale is loaded
|
|
131
|
-
*/
|
|
132
63
|
isLocaleLoaded(locale) {
|
|
133
|
-
return this.
|
|
64
|
+
return this.i18n.isLocaleLoaded(locale);
|
|
134
65
|
}
|
|
135
|
-
/**
|
|
136
|
-
* Subscribe to locale/translation changes
|
|
137
|
-
* Returns unsubscribe function
|
|
138
|
-
*/
|
|
139
66
|
subscribe(listener) {
|
|
140
|
-
this.
|
|
141
|
-
return () => {
|
|
142
|
-
this.listeners.delete(listener);
|
|
143
|
-
};
|
|
67
|
+
return this.i18n.subscribe(listener);
|
|
144
68
|
}
|
|
145
|
-
/**
|
|
146
|
-
* Check if a translation key exists
|
|
147
|
-
*/
|
|
148
69
|
hasKey(key) {
|
|
149
|
-
|
|
150
|
-
return !!bundle?.translations[key];
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* Notify all listeners of state change
|
|
154
|
-
*/
|
|
155
|
-
notifyListeners() {
|
|
156
|
-
this.listeners.forEach((listener) => listener());
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* Apply locale and notify listeners
|
|
160
|
-
*/
|
|
161
|
-
applyLocale(locale) {
|
|
162
|
-
const bundle = this.translations.get(locale);
|
|
163
|
-
if (!bundle) {
|
|
164
|
-
log(`No bundle found for locale: ${locale}`, "warn");
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
const previousLocale = this.locale;
|
|
168
|
-
this.locale = locale;
|
|
169
|
-
this.direction = bundle.direction;
|
|
170
|
-
log(`Applied locale: ${locale} (direction: ${this.direction})`);
|
|
171
|
-
this.applyDOMDirection();
|
|
172
|
-
this.notifyListeners();
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Apply direction to DOM
|
|
176
|
-
*/
|
|
177
|
-
applyDOMDirection() {
|
|
178
|
-
if (typeof document === "undefined")
|
|
179
|
-
return;
|
|
180
|
-
document.documentElement.setAttribute("dir", this.direction);
|
|
181
|
-
document.documentElement.setAttribute("lang", this.locale);
|
|
182
|
-
log(`Applied DOM attributes: dir="${this.direction}" lang="${this.locale}"`);
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* Load translations for a locale
|
|
186
|
-
*/
|
|
187
|
-
async loadTranslationsForLocale(locale) {
|
|
188
|
-
if (!this.config.loadTranslations) {
|
|
189
|
-
throw new Error(`No translation loader configured for locale: ${locale}`);
|
|
190
|
-
}
|
|
191
|
-
log(`Loading translations for: ${locale}`);
|
|
192
|
-
try {
|
|
193
|
-
const bundle = await this.config.loadTranslations(locale);
|
|
194
|
-
this.translations.set(locale, bundle);
|
|
195
|
-
log(`Loaded translations for: ${locale} (${Object.keys(bundle.translations).length} keys)`);
|
|
196
|
-
}
|
|
197
|
-
catch (error) {
|
|
198
|
-
log(`Failed to load translations for: ${locale}`, "error");
|
|
199
|
-
throw error;
|
|
200
|
-
}
|
|
70
|
+
return this.i18n.hasKey(key);
|
|
201
71
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
*/
|
|
205
|
-
getTranslation(key) {
|
|
206
|
-
// Try current locale
|
|
207
|
-
const currentBundle = this.translations.get(this.locale);
|
|
208
|
-
if (currentBundle?.translations[key]) {
|
|
209
|
-
return currentBundle.translations[key];
|
|
210
|
-
}
|
|
211
|
-
// Try fallback locale
|
|
212
|
-
const fallbackBundle = this.translations.get(this.fallbackLocale);
|
|
213
|
-
if (fallbackBundle?.translations[key]) {
|
|
214
|
-
if (this.config.debug) {
|
|
215
|
-
log(`Using fallback translation for key: ${key}`);
|
|
216
|
-
}
|
|
217
|
-
return fallbackBundle.translations[key];
|
|
218
|
-
}
|
|
219
|
-
// Missing key handling
|
|
220
|
-
if (this.config.onMissingKey) {
|
|
221
|
-
this.config.onMissingKey(key, this.locale);
|
|
222
|
-
}
|
|
223
|
-
if (this.config.debug) {
|
|
224
|
-
log(`Missing translation key: ${key} (locale: ${this.locale})`, "warn");
|
|
225
|
-
}
|
|
226
|
-
return key;
|
|
72
|
+
addCustomMessages(locale, messages) {
|
|
73
|
+
this.i18n.addCustomMessages(locale, messages);
|
|
227
74
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
* Replaces {variable} with params.variable
|
|
231
|
-
*/
|
|
232
|
-
interpolate(text, params) {
|
|
233
|
-
if (!params)
|
|
234
|
-
return text;
|
|
235
|
-
return text.replace(/\{(\w+)\}/g, (match, key) => {
|
|
236
|
-
return params[key]?.toString() || match;
|
|
237
|
-
});
|
|
75
|
+
withLocale(locale) {
|
|
76
|
+
return this.i18n.withLocale(locale);
|
|
238
77
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
* Simplified implementation - real implementation would use Intl.PluralRules
|
|
242
|
-
*/
|
|
243
|
-
selectPluralForm(count, locale) {
|
|
244
|
-
// Use Intl.PluralRules if available
|
|
245
|
-
if (typeof Intl !== "undefined" && Intl.PluralRules) {
|
|
246
|
-
try {
|
|
247
|
-
const rules = new Intl.PluralRules(locale);
|
|
248
|
-
const category = rules.select(count);
|
|
249
|
-
return category;
|
|
250
|
-
}
|
|
251
|
-
catch (error) {
|
|
252
|
-
log(`Failed to use Intl.PluralRules for locale: ${locale}`, "warn");
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
// Fallback to simple rules
|
|
256
|
-
if (count === 0)
|
|
257
|
-
return "zero";
|
|
258
|
-
if (count === 1)
|
|
259
|
-
return "one";
|
|
260
|
-
return "other";
|
|
78
|
+
formatNumber(value, options) {
|
|
79
|
+
return this.i18n.formatNumber?.(value, options) ?? String(value);
|
|
261
80
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
*/
|
|
265
|
-
detectBrowserLocale() {
|
|
266
|
-
if (typeof navigator === "undefined")
|
|
267
|
-
return "en";
|
|
268
|
-
const browserLang = navigator.language ||
|
|
269
|
-
(navigator.languages && navigator.languages[0]) ||
|
|
270
|
-
"en";
|
|
271
|
-
// Extract language code (e.g., 'en-US' -> 'en')
|
|
272
|
-
return browserLang.split("-")[0];
|
|
81
|
+
formatDate(date, options) {
|
|
82
|
+
return this.i18n.formatDate?.(date, options) ?? date.toISOString();
|
|
273
83
|
}
|
|
274
84
|
}
|
|
@@ -109,6 +109,18 @@ export declare class TTSService {
|
|
|
109
109
|
setHighlightCoordinator(coordinator: HighlightCoordinatorApi): void;
|
|
110
110
|
/**
|
|
111
111
|
* Set a late-bound provider for optional host TTS highlight target remapping.
|
|
112
|
+
*
|
|
113
|
+
* The returned disposer is the only thing that clears the registration: the
|
|
114
|
+
* caller owns the lifetime, and a provider installed once keeps remapping
|
|
115
|
+
* across stops, seek-restarts and later playbacks. Playback termination used to
|
|
116
|
+
* clear it in `stop()` and on both exits of `restartFromSeekIndex`, which left a
|
|
117
|
+
* host that installs before mount — the case a late-bound provider exists for —
|
|
118
|
+
* remapping exactly one playback and then silently falling back to identity.
|
|
119
|
+
*
|
|
120
|
+
* Installing again replaces the previous provider, so a caller that reinstalls
|
|
121
|
+
* per playback needs no disposal between them. A stale provider cannot paint
|
|
122
|
+
* outside its scope in any case: targets are validated by containment in
|
|
123
|
+
* `context.scopeElement` and a failing one falls back to its native range.
|
|
112
124
|
*/
|
|
113
125
|
setHighlightTargetResolverProvider(provider: TTSHighlightTargetResolverProvider | null): () => void;
|
|
114
126
|
/**
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* Part of PIE Assessment Toolkit.
|
|
17
17
|
*/
|
|
18
|
-
import { applyMediaFragment } from "./catalog-media.js";
|
|
18
|
+
import { applyMediaFragment, enforceMediaFragment } from "./catalog-media.js";
|
|
19
19
|
import { HighlightColor, HighlightType } from "./HighlightCoordinator.js";
|
|
20
20
|
import { resolveSpokenAudioMedia, } from "./spoken-audio-cards.js";
|
|
21
21
|
import { BrowserTTSProvider } from "./tts/browser-provider.js";
|
|
@@ -282,6 +282,18 @@ export class TTSService {
|
|
|
282
282
|
}
|
|
283
283
|
/**
|
|
284
284
|
* Set a late-bound provider for optional host TTS highlight target remapping.
|
|
285
|
+
*
|
|
286
|
+
* The returned disposer is the only thing that clears the registration: the
|
|
287
|
+
* caller owns the lifetime, and a provider installed once keeps remapping
|
|
288
|
+
* across stops, seek-restarts and later playbacks. Playback termination used to
|
|
289
|
+
* clear it in `stop()` and on both exits of `restartFromSeekIndex`, which left a
|
|
290
|
+
* host that installs before mount — the case a late-bound provider exists for —
|
|
291
|
+
* remapping exactly one playback and then silently falling back to identity.
|
|
292
|
+
*
|
|
293
|
+
* Installing again replaces the previous provider, so a caller that reinstalls
|
|
294
|
+
* per playback needs no disposal between them. A stale provider cannot paint
|
|
295
|
+
* outside its scope in any case: targets are validated by containment in
|
|
296
|
+
* `context.scopeElement` and a failing one falls back to its native range.
|
|
285
297
|
*/
|
|
286
298
|
setHighlightTargetResolverProvider(provider) {
|
|
287
299
|
this.highlightTargetResolverProvider = provider;
|
|
@@ -1731,7 +1743,7 @@ export class TTSService {
|
|
|
1731
1743
|
element.src = applyMediaFragment(source.src, media.fragment);
|
|
1732
1744
|
element.playbackRate = this.normalizePlaybackRate(Number(this.ttsConfig.rate || 1));
|
|
1733
1745
|
await new Promise((resolve, reject) => {
|
|
1734
|
-
let
|
|
1746
|
+
let disposeFragment;
|
|
1735
1747
|
let didStart = false;
|
|
1736
1748
|
let settled = false;
|
|
1737
1749
|
const markStarted = () => {
|
|
@@ -1744,8 +1756,7 @@ export class TTSService {
|
|
|
1744
1756
|
element.removeEventListener("ended", onEnded);
|
|
1745
1757
|
element.removeEventListener("error", onError);
|
|
1746
1758
|
element.removeEventListener("playing", markStarted);
|
|
1747
|
-
|
|
1748
|
-
clearInterval(endGuard);
|
|
1759
|
+
disposeFragment?.();
|
|
1749
1760
|
if (this.activeRecordedAudio?.element === element) {
|
|
1750
1761
|
this.activeRecordedAudio = null;
|
|
1751
1762
|
}
|
|
@@ -1781,16 +1792,9 @@ export class TTSService {
|
|
|
1781
1792
|
element.addEventListener("ended", onEnded);
|
|
1782
1793
|
element.addEventListener("error", onError);
|
|
1783
1794
|
element.addEventListener("playing", markStarted);
|
|
1784
|
-
//
|
|
1785
|
-
//
|
|
1786
|
-
|
|
1787
|
-
const endSeconds = media.fragment?.endSeconds;
|
|
1788
|
-
if (endSeconds !== undefined) {
|
|
1789
|
-
endGuard = setInterval(() => {
|
|
1790
|
-
if (element.currentTime >= endSeconds)
|
|
1791
|
-
onEnded();
|
|
1792
|
-
}, 100);
|
|
1793
|
-
}
|
|
1795
|
+
// Reaching the slice's end is this clip finishing, so the chunk sequence
|
|
1796
|
+
// advances rather than the element merely pausing.
|
|
1797
|
+
disposeFragment = enforceMediaFragment(element, media.fragment, onEnded);
|
|
1794
1798
|
Promise.resolve(element.play()).then(markStarted).catch(onError);
|
|
1795
1799
|
}).finally(() => {
|
|
1796
1800
|
if (this.activeRecordedAudio?.element === element) {
|
|
@@ -2092,7 +2096,6 @@ export class TTSService {
|
|
|
2092
2096
|
return;
|
|
2093
2097
|
this.setState(PlaybackState.IDLE);
|
|
2094
2098
|
this.clearHighlightsAndTracking();
|
|
2095
|
-
this.highlightTargetResolverProvider = null;
|
|
2096
2099
|
}
|
|
2097
2100
|
catch (error) {
|
|
2098
2101
|
if (runId !== this.speakRunId)
|
|
@@ -2100,7 +2103,6 @@ export class TTSService {
|
|
|
2100
2103
|
this.lastError = error instanceof Error ? error.message : String(error);
|
|
2101
2104
|
this.setState(PlaybackState.ERROR);
|
|
2102
2105
|
this.clearHighlightsAndTracking();
|
|
2103
|
-
this.highlightTargetResolverProvider = null;
|
|
2104
2106
|
throw error;
|
|
2105
2107
|
}
|
|
2106
2108
|
finally {
|
|
@@ -2205,7 +2207,6 @@ export class TTSService {
|
|
|
2205
2207
|
this.cancelRecordedAudio();
|
|
2206
2208
|
this.provider.onWordBoundary = undefined;
|
|
2207
2209
|
this.provider.stop();
|
|
2208
|
-
this.highlightTargetResolverProvider = null;
|
|
2209
2210
|
this.setState(PlaybackState.IDLE);
|
|
2210
2211
|
this.currentText = null;
|
|
2211
2212
|
// Clear highlights
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Central registry for all assessment tools. Manages tool metadata, visibility logic,
|
|
5
5
|
* and button/instance creation. Supports dynamic registration and override by integrators.
|
|
6
6
|
*/
|
|
7
|
+
import type { I18nProvider } from "@pie-players/pie-players-shared/i18n/types";
|
|
7
8
|
import type { CatalogOwnerSnapshot } from "./AccessibilityCatalogResolver.js";
|
|
8
9
|
import type { ToolContext, ToolLevel } from "./tool-context.js";
|
|
9
10
|
import type { ToolComponentOverrides } from "../tools/tool-tag-map.js";
|
|
@@ -30,6 +31,17 @@ export interface ToolToolbarButtonDefinition {
|
|
|
30
31
|
* `assertToolRegistrationShape` enforces that.
|
|
31
32
|
*/
|
|
32
33
|
icon?: string;
|
|
34
|
+
/**
|
|
35
|
+
* FontAwesome icon name, opting this button into `<nds-icon-button>` rendering
|
|
36
|
+
* where the host enables NDS icons (`ndsIcons`). Absent means the toolbar keeps
|
|
37
|
+
* its own button rendering, which is what every capability got before the NDS
|
|
38
|
+
* button existed.
|
|
39
|
+
*
|
|
40
|
+
* A declaration rather than a toolId lookup in the toolbar: which capabilities
|
|
41
|
+
* a deployment renders through the host's design system is a composition-layer
|
|
42
|
+
* decision, and the generic core names no capability.
|
|
43
|
+
*/
|
|
44
|
+
faIconName?: string;
|
|
33
45
|
ariaLabel: string;
|
|
34
46
|
tooltip?: string;
|
|
35
47
|
onClick: () => void;
|
|
@@ -49,7 +61,24 @@ export interface ToolbarContext {
|
|
|
49
61
|
};
|
|
50
62
|
itemId: string;
|
|
51
63
|
catalogId: string;
|
|
64
|
+
/**
|
|
65
|
+
* Content-alternate language: which authored alternate the catalog resolver
|
|
66
|
+
* should select. Not the interface locale — see {@link ToolbarContext.i18n}. The
|
|
67
|
+
* two are independent by QTI 3's own statement, and conflating them is how a
|
|
68
|
+
* Spanish passage ends up forcing Spanish widget chrome.
|
|
69
|
+
*/
|
|
52
70
|
language: string;
|
|
71
|
+
/**
|
|
72
|
+
* Interface-locale provider for this capability's own UI strings.
|
|
73
|
+
*
|
|
74
|
+
* Required, and always the facade `resolveInterfaceI18n` returns: the toolbar
|
|
75
|
+
* resolves it once from the toolkit runtime context, so a registration reads
|
|
76
|
+
* `toolbarContext.i18n` and neither repeats the no-publisher fallback nor
|
|
77
|
+
* misses the republish that a locale change produces. With no publisher the
|
|
78
|
+
* facade wraps the English-only default, which is why this can be required
|
|
79
|
+
* rather than optional.
|
|
80
|
+
*/
|
|
81
|
+
i18n: I18nProvider;
|
|
53
82
|
ui?: {
|
|
54
83
|
size?: string;
|
|
55
84
|
};
|
|
@@ -137,6 +166,23 @@ export interface ToolWindowShellConfig {
|
|
|
137
166
|
initialMargin?: number;
|
|
138
167
|
content?: ToolWindowShellContentConfig;
|
|
139
168
|
actions?: ToolWindowShellAction[];
|
|
169
|
+
/**
|
|
170
|
+
* Render the shell's own header controls as `<nds-icon-button>`s where the host
|
|
171
|
+
* enables NDS icons. Shells without this keep the plain `<button>` controls, and
|
|
172
|
+
* an opted-out host gets those either way.
|
|
173
|
+
*/
|
|
174
|
+
ndsHeaderControls?: boolean;
|
|
175
|
+
/**
|
|
176
|
+
* Let Tab and Shift+Tab cross between the page and this shell rather than
|
|
177
|
+
* cycling inside it.
|
|
178
|
+
*
|
|
179
|
+
* The shell is appended to `<body>`, so it sits outside the document order its
|
|
180
|
+
* opener lives in. A shell a learner works *alongside* — reading the question
|
|
181
|
+
* while using the tool — needs Tab from the opener to enter it and Shift+Tab
|
|
182
|
+
* from the first question control to reach its end. A shell a learner works
|
|
183
|
+
* *inside* wants the default trap, which keeps focus captured until dismissed.
|
|
184
|
+
*/
|
|
185
|
+
pageTabOrder?: boolean;
|
|
140
186
|
}
|
|
141
187
|
export interface HostedToolContext {
|
|
142
188
|
toolId: string;
|
|
@@ -163,6 +209,19 @@ export interface ToolToolbarRenderResult {
|
|
|
163
209
|
sync?: () => void;
|
|
164
210
|
subscribeActive?: (callback: (active: boolean) => void) => () => void;
|
|
165
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* A registration's display name in the interface locale.
|
|
214
|
+
*
|
|
215
|
+
* Precedence: the resolved `nameKey`, then `name`. A key that does not resolve
|
|
216
|
+
* falls back to `name` rather than rendering the key, so a catalog gap degrades
|
|
217
|
+
* to English instead of to `tools.something.name` on a toolbar button.
|
|
218
|
+
*
|
|
219
|
+
* Both display resolvers live here so the toolbars, the settings panels and the
|
|
220
|
+
* PNP debugger cannot each invent their own precedence.
|
|
221
|
+
*/
|
|
222
|
+
export declare function resolveToolRegistrationName(registration: Pick<ToolRegistration, "name" | "nameKey">, i18n?: I18nProvider): string;
|
|
223
|
+
/** A registration's description in the interface locale. See the name resolver. */
|
|
224
|
+
export declare function resolveToolRegistrationDescription(registration: Pick<ToolRegistration, "description" | "descriptionKey">, i18n?: I18nProvider): string;
|
|
166
225
|
export type ToolActivation = "toolbar-toggle" | "selection-gateway" | "region";
|
|
167
226
|
export type ToolSingletonScope = "section";
|
|
168
227
|
/**
|
|
@@ -177,6 +236,13 @@ export interface ToolSurfaceServices {
|
|
|
177
236
|
toolkitCoordinator: ToolkitCoordinatorApi | null;
|
|
178
237
|
ttsService: TtsServiceApi | null;
|
|
179
238
|
catalogResolver: AccessibilityCatalogResolverApi | null;
|
|
239
|
+
/**
|
|
240
|
+
* Interface-locale provider for the surface's own labels — a region's
|
|
241
|
+
* `aria-label`, a heading it emits. Required, and always the facade
|
|
242
|
+
* `resolveInterfaceI18n` returns, which wraps the English-only default where
|
|
243
|
+
* the host published nothing.
|
|
244
|
+
*/
|
|
245
|
+
i18n: I18nProvider;
|
|
180
246
|
}
|
|
181
247
|
/**
|
|
182
248
|
* What a host tells a capability when asking it to fill a surface.
|
|
@@ -310,10 +376,28 @@ export interface ToolSurfaceRenderResult {
|
|
|
310
376
|
export interface ToolRegistration {
|
|
311
377
|
/** Unique tool identifier (e.g., 'calculator', 'textToSpeech') */
|
|
312
378
|
toolId: string;
|
|
313
|
-
/**
|
|
379
|
+
/**
|
|
380
|
+
* Human-readable name.
|
|
381
|
+
*
|
|
382
|
+
* Required, and stays required: it is part of the contract a host implements
|
|
383
|
+
* when it contributes its own capability, and a host cannot be made to ship a
|
|
384
|
+
* message catalog. Treat it as the English source.
|
|
385
|
+
*/
|
|
314
386
|
name: string;
|
|
315
|
-
/** Description of what the tool does */
|
|
387
|
+
/** Description of what the tool does. English source, like {@link name}. */
|
|
316
388
|
description: string;
|
|
389
|
+
/**
|
|
390
|
+
* Message key resolving to {@link name} in the interface locale.
|
|
391
|
+
*
|
|
392
|
+
* Optional by design. A capability that supplies one is localizable; one that
|
|
393
|
+
* does not renders `name` verbatim, which is what keeps a host-authored
|
|
394
|
+
* registration working with no catalog. Resolve with
|
|
395
|
+
* {@link resolveToolRegistrationName} rather than reading either field
|
|
396
|
+
* directly, so precedence stays in one place.
|
|
397
|
+
*/
|
|
398
|
+
nameKey?: string;
|
|
399
|
+
/** Message key resolving to {@link description} in the interface locale. */
|
|
400
|
+
descriptionKey?: string;
|
|
317
401
|
/**
|
|
318
402
|
* Icon identifier or SVG string. Required for the activations that render a
|
|
319
403
|
* toolbar button; a region capability has no button, so it has no icon.
|
|
@@ -399,6 +483,26 @@ export interface ToolRegistration {
|
|
|
399
483
|
* @returns true if tool should be visible, false to hide
|
|
400
484
|
*/
|
|
401
485
|
isVisibleInContext?(context: ToolContext): boolean;
|
|
486
|
+
/**
|
|
487
|
+
* Whether this tool can act on this content at all — a capability question,
|
|
488
|
+
* not a relevance heuristic. Answering `false` withdraws the tool even where
|
|
489
|
+
* a PNP grant would otherwise keep it, which {@link
|
|
490
|
+
* ToolRegistration.isVisibleInContext} deliberately cannot do.
|
|
491
|
+
*
|
|
492
|
+
* The two gates answer different questions, and most tools declare only the
|
|
493
|
+
* first. A calculator is *applicable* to every item — a learner granted one
|
|
494
|
+
* keeps it on an item that does not look mathematical — while its relevance
|
|
495
|
+
* is a guess about usefulness. An answer eliminator on an item with no choice
|
|
496
|
+
* interaction has nothing to strike through, so no grant can make it work.
|
|
497
|
+
*
|
|
498
|
+
* Declare this only where the tool's own controls provably do nothing:
|
|
499
|
+
* withdrawing a granted accommodation on a false negative is the more
|
|
500
|
+
* expensive failure. Omitting it means "applicable".
|
|
501
|
+
*
|
|
502
|
+
* @param context - The item or element context the tool would act on
|
|
503
|
+
* @returns false to withdraw the tool from this context
|
|
504
|
+
*/
|
|
505
|
+
isApplicableToContent?(context: ToolContext): boolean;
|
|
402
506
|
/**
|
|
403
507
|
* Toolbar render contract. Required for `toolbar-toggle` and
|
|
404
508
|
* `selection-gateway`; a region capability renders through
|
|
@@ -541,6 +645,20 @@ export declare class ToolRegistry {
|
|
|
541
645
|
* @returns Array of visible tool registrations
|
|
542
646
|
*/
|
|
543
647
|
filterVisibleInContext(allowedToolIds: string[], context: ToolContext): ToolRegistration[];
|
|
648
|
+
/**
|
|
649
|
+
* Whether a tool can act on any of the contexts it would be placed against.
|
|
650
|
+
* Unlike the relevance pass this is a veto: a `false` here removes the tool
|
|
651
|
+
* from a toolbar even when a grant protects it, so a tool answers `false`
|
|
652
|
+
* only where its controls provably do nothing.
|
|
653
|
+
*
|
|
654
|
+
* A tool that declares no applicability gate is applicable. So is one
|
|
655
|
+
* evaluated against no contexts — content that has not resolved yet cannot
|
|
656
|
+
* establish that a tool is useless.
|
|
657
|
+
*
|
|
658
|
+
* @param toolId - Tool to ask
|
|
659
|
+
* @param contexts - Every context the tool could act on at this placement
|
|
660
|
+
*/
|
|
661
|
+
isApplicableToAnyContext(toolId: string, contexts: readonly ToolContext[]): boolean;
|
|
544
662
|
/**
|
|
545
663
|
* Get tool metadata for building UIs
|
|
546
664
|
* Useful for building PNP configuration interfaces
|