@pie-players/pie-default-tool-loaders 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.
@@ -13,6 +13,8 @@
13
13
  import { hasReadableText } from "@pie-players/pie-assessment-toolkit/tools/internal";
14
14
  import { createToolElement, resolveToolTag, } from "@pie-players/pie-assessment-toolkit/tools/internal";
15
15
  import { applyOverlaySurface, createScopedVisibilityBinding, syncButtonAndOverlayVisibility, } from "@pie-players/pie-assessment-toolkit/tools/internal";
16
+ import { buildSelectionActions } from "./selection-actions.js";
17
+ import { renderOverlayToolbar } from "./overlay-toolbar-render.js";
16
18
  /**
17
19
  * Line Reader tool registration
18
20
  *
@@ -23,6 +25,8 @@ export const lineReaderToolRegistration = {
23
25
  toolId: "lineReader",
24
26
  name: "Line Reader",
25
27
  description: "Reading guide overlay",
28
+ nameKey: "tools.lineReader.name",
29
+ descriptionKey: "tools.lineReader.description",
26
30
  icon: "bars-3",
27
31
  // Line reader appears where there's text to read
28
32
  supportedLevels: ["section", "passage", "rubric", "item"],
@@ -32,8 +36,6 @@ export const lineReaderToolRegistration = {
32
36
  "readingMask", // QTI 3.0 standard (reading.readingMask)
33
37
  "readingGuide", // QTI 3.0 standard (reading.readingGuide)
34
38
  "readingRuler", // QTI 3.0 standard (reading.readingRuler)
35
- "lineReader", // Common variant
36
- "trackingGuide", // Common variant
37
39
  ],
38
40
  /**
39
41
  * Pass 2: Line reader is relevant when readable text is present
@@ -42,37 +44,10 @@ export const lineReaderToolRegistration = {
42
44
  return hasReadableText(context);
43
45
  },
44
46
  renderToolbar(context, toolbarContext) {
45
- const visibility = createScopedVisibilityBinding(this.toolId, toolbarContext);
46
- const button = {
47
- toolId: this.toolId,
48
- label: this.name,
49
- icon: typeof this.icon === "function" ? this.icon(context) : this.icon,
50
- disabled: false,
51
- ariaLabel: "Line reader - Reading guide",
52
- tooltip: "Line Reader",
53
- onClick: () => toolbarContext.toggleTool(this.toolId),
54
- active: visibility.isActive(),
55
- };
56
- const componentOverrides = toolbarContext.componentOverrides ?? {};
57
- const overlay = createToolElement(this.toolId, context, toolbarContext, componentOverrides);
58
- overlay.setAttribute("tool-id", visibility.fullToolId);
59
- applyOverlaySurface(overlay, "frameless");
60
- return {
61
- toolId: this.toolId,
62
- button,
63
- elements: [{ element: overlay, mount: "after-buttons" }],
64
- sync: () => {
65
- syncButtonAndOverlayVisibility({
66
- button,
67
- overlay,
68
- isActive: visibility.isActive,
69
- });
70
- if (toolbarContext.toolkitCoordinator) {
71
- overlay.toolkitCoordinator = toolbarContext.toolkitCoordinator;
72
- }
73
- },
74
- subscribeActive: visibility.subscribeActive,
75
- };
47
+ return renderOverlayToolbar(this, context, toolbarContext, {
48
+ surface: "frameless",
49
+ handsOverCoordinator: true,
50
+ });
76
51
  },
77
52
  };
78
53
  /**
@@ -85,6 +60,8 @@ export const themeToolRegistration = {
85
60
  toolId: "theme",
86
61
  name: "Theme",
87
62
  description: "Accessible themes and contrast",
63
+ nameKey: "tools.theme.name",
64
+ descriptionKey: "tools.theme.description",
88
65
  icon: "swatch",
89
66
  // Color scheme is assessment-wide
90
67
  supportedLevels: ["assessment", "section"],
@@ -94,9 +71,7 @@ export const themeToolRegistration = {
94
71
  "highContrastDisplay", // QTI 3.0 standard (visual.highContrastDisplay)
95
72
  "colorContrast", // QTI 3.0 standard (visual.colorContrast)
96
73
  "invertColors", // QTI 3.0 standard (visual.invertColors)
97
- "theme", // Canonical id
98
- "highContrast", // Common variant
99
- "customColors", // Common variant
74
+ "theme", // Canonical id, and this registration's toolId
100
75
  ],
101
76
  /**
102
77
  * Pass 2: Color scheme is always relevant when allowed
@@ -105,51 +80,16 @@ export const themeToolRegistration = {
105
80
  return true; // Always show if allowed by orchestrator
106
81
  },
107
82
  renderToolbar(context, toolbarContext) {
108
- const visibility = createScopedVisibilityBinding(this.toolId, toolbarContext);
109
- const button = {
110
- toolId: this.toolId,
111
- label: this.name,
112
- icon: typeof this.icon === "function" ? this.icon(context) : this.icon,
113
- disabled: false,
114
- ariaLabel: "Theme - Change colors and contrast",
115
- tooltip: "Theme",
116
- onClick: () => toolbarContext.toggleTool(this.toolId),
117
- active: visibility.isActive(),
118
- };
119
- const componentOverrides = toolbarContext.componentOverrides ?? {};
120
- const overlay = createToolElement(this.toolId, context, toolbarContext, componentOverrides);
121
- overlay.setAttribute("tool-id", visibility.fullToolId);
122
- return {
123
- toolId: this.toolId,
124
- button,
125
- elements: [
126
- {
127
- element: overlay,
128
- mount: "after-buttons",
129
- shell: {
130
- title: this.name,
131
- draggable: true,
132
- resizable: false,
133
- closeable: true,
134
- initialWidth: 520,
135
- initialHeight: 380,
136
- minWidth: 420,
137
- minHeight: 300,
138
- },
139
- },
140
- ],
141
- sync: () => {
142
- syncButtonAndOverlayVisibility({
143
- button,
144
- overlay,
145
- isActive: visibility.isActive,
146
- });
147
- if (toolbarContext.toolkitCoordinator) {
148
- overlay.toolkitCoordinator = toolbarContext.toolkitCoordinator;
149
- }
83
+ return renderOverlayToolbar(this, context, toolbarContext, {
84
+ shell: {
85
+ resizable: false,
86
+ initialWidth: 520,
87
+ initialHeight: 380,
88
+ minWidth: 420,
89
+ minHeight: 300,
150
90
  },
151
- subscribeActive: visibility.subscribeActive,
152
- };
91
+ handsOverCoordinator: true,
92
+ });
153
93
  },
154
94
  };
155
95
  /**
@@ -162,6 +102,8 @@ export const annotationToolbarRegistration = {
162
102
  toolId: "annotationToolbar",
163
103
  name: "Highlighter",
164
104
  description: "Highlight and annotate text",
105
+ nameKey: "tools.annotationToolbar.name",
106
+ descriptionKey: "tools.annotationToolbar.description",
165
107
  icon: "highlighter",
166
108
  activation: "selection-gateway",
167
109
  singletonScope: "section",
@@ -181,9 +123,6 @@ export const annotationToolbarRegistration = {
181
123
  pnpSupportIds: [
182
124
  "highlighting", // QTI 3.0 standard (cognitive.highlighting / reading.wordHighlighting)
183
125
  "annotations", // QTI 3.0 standard (cognitive.annotations)
184
- "highlighter", // Common variant
185
- "textHighlight", // Common variant
186
- "annotation", // Common variant
187
126
  ],
188
127
  /**
189
128
  * Pass 2: Annotation is relevant when readable text is present
@@ -217,40 +156,17 @@ export const annotationToolbarRegistration = {
217
156
  element.ttsService = current.services.ttsService;
218
157
  element.highlightCoordinator =
219
158
  current.services.toolkitCoordinator?.highlightCoordinator ?? null;
159
+ // The second door onto the dictionaries. The gateway renders these and
160
+ // knows nothing of what they open; the pairing is composition's, which is
161
+ // why the list is built in `selection-actions.ts` and not here.
162
+ element.selectionActions = buildSelectionActions(current.services);
220
163
  };
221
164
  applyServices(context);
222
165
  return { element, sync: applyServices };
223
166
  },
224
167
  renderToolbar(context, toolbarContext) {
225
- const visibility = createScopedVisibilityBinding(this.toolId, toolbarContext);
226
- const button = {
227
- toolId: this.toolId,
228
- label: this.name,
229
- icon: typeof this.icon === "function" ? this.icon(context) : this.icon,
230
- disabled: false,
231
- ariaLabel: "Annotation toolbar - Highlight text",
232
- tooltip: "Highlight",
233
- onClick: () => toolbarContext.toggleTool(this.toolId),
234
- active: visibility.isActive(),
235
- };
236
- const componentOverrides = toolbarContext.componentOverrides ?? {};
237
- const overlay = createToolElement(this.toolId, context, toolbarContext, componentOverrides);
238
- overlay.setAttribute("tool-id", visibility.fullToolId);
239
- return {
240
- toolId: this.toolId,
241
- button,
242
- elements: [{ element: overlay, mount: "after-buttons" }],
243
- sync: () => {
244
- syncButtonAndOverlayVisibility({
245
- button,
246
- overlay,
247
- isActive: visibility.isActive,
248
- });
249
- if (toolbarContext.toolkitCoordinator) {
250
- overlay.toolkitCoordinator = toolbarContext.toolkitCoordinator;
251
- }
252
- },
253
- subscribeActive: visibility.subscribeActive,
254
- };
168
+ return renderOverlayToolbar(this, context, toolbarContext, {
169
+ handsOverCoordinator: true,
170
+ });
255
171
  },
256
172
  };
@@ -144,6 +144,8 @@ export const audioTranscriptRegistration = {
144
144
  toolId: AUDIO_TRANSCRIPT_FEATURE_ID,
145
145
  name: "Audio Transcript",
146
146
  description: "Text of an audio prompt, read before the content it transcribes",
147
+ nameKey: "tools.audioTranscript.name",
148
+ descriptionKey: "tools.audioTranscript.description",
147
149
  supportedLevels: ["item", "passage"],
148
150
  pnpSupportIds: [AUDIO_TRANSCRIPT_FEATURE_ID],
149
151
  // No button to press: a transcript is either authored-visible, or granted and
@@ -173,7 +175,11 @@ export const audioTranscriptRegistration = {
173
175
  applyTranscript(region, transcript);
174
176
  return {
175
177
  element: region,
176
- ariaLabel: AUDIO_TRANSCRIPT_REGION_LABEL,
178
+ // `AUDIO_TRANSCRIPT_REGION_LABEL` stays the English source and the
179
+ // exported constant tests address; the catalog supplies the rendered
180
+ // label when a host published a provider.
181
+ ariaLabel: context.services.i18n?.t("tools.audioTranscript.regionA11y") ??
182
+ AUDIO_TRANSCRIPT_REGION_LABEL,
177
183
  // Reads the context it is handed, never the one captured above: on a
178
184
  // re-resolve the host's context carries the current card, and a learner must
179
185
  // not keep reading the previous item's transcript.
@@ -2,18 +2,22 @@
2
2
  * Calculator Tool Registration
3
3
  *
4
4
  * Registers the calculator tool with support for multiple calculator types
5
- * (basic, scientific, graphing) via Desmos provider.
5
+ * (basic, scientific, graphing) through a host-selected provider.
6
6
  *
7
7
  * Maps to QTI 3.0 standard access features:
8
8
  * - calculator (cognitive support)
9
9
  * - graphingCalculator (assessment tool)
10
10
  */
11
11
  import type { ToolRegistration } from "@pie-players/pie-assessment-toolkit/tools/internal";
12
+ import type { ToolProviderConfig } from "@pie-players/pie-assessment-toolkit/tools/internal";
13
+ export type CalculatorProviderId = "calculator-desmos" | "calculator-geogebra" | "calculator-cortex";
14
+ export declare const DEFAULT_CALCULATOR_PROVIDER_ID: CalculatorProviderId;
15
+ export declare function resolveCalculatorProviderId(config: ToolProviderConfig | undefined): CalculatorProviderId;
12
16
  /**
13
17
  * Calculator tool registration
14
18
  *
15
19
  * Supports:
16
- * - Basic, scientific, and graphing calculators via Desmos
20
+ * - Basic, scientific, and graphing calculators through Desmos, GeoGebra, or Cortex
17
21
  * - Context-aware visibility (shows only when math content is detected)
18
22
  * - Item level only
19
23
  */
@@ -2,7 +2,7 @@
2
2
  * Calculator Tool Registration
3
3
  *
4
4
  * Registers the calculator tool with support for multiple calculator types
5
- * (basic, scientific, graphing) via Desmos provider.
5
+ * (basic, scientific, graphing) through a host-selected provider.
6
6
  *
7
7
  * Maps to QTI 3.0 standard access features:
8
8
  * - calculator (cognitive support)
@@ -11,7 +11,45 @@
11
11
  import { hasMathContent } from "@pie-players/pie-assessment-toolkit/tools/internal";
12
12
  import { createScopedToolId } from "@pie-players/pie-assessment-toolkit/tools/internal";
13
13
  import { DesmosToolProvider } from "@pie-players/pie-assessment-toolkit/tools/internal";
14
+ import { CortexToolProvider } from "@pie-players/pie-assessment-toolkit/tools/internal";
15
+ import { GeoGebraToolProvider } from "@pie-players/pie-assessment-toolkit/tools/internal";
14
16
  import { createToolElement } from "@pie-players/pie-assessment-toolkit/tools/internal";
17
+ export const DEFAULT_CALCULATOR_PROVIDER_ID = "calculator-desmos";
18
+ export function resolveCalculatorProviderId(config) {
19
+ const configured = config?.provider?.id;
20
+ if (configured === undefined || configured === "") {
21
+ return DEFAULT_CALCULATOR_PROVIDER_ID;
22
+ }
23
+ if (configured === "calculator-desmos" ||
24
+ configured === "calculator-geogebra" ||
25
+ configured === "calculator-cortex") {
26
+ return configured;
27
+ }
28
+ throw new Error(`Unsupported calculator provider "${String(configured)}". Expected "calculator-desmos", "calculator-geogebra", or "calculator-cortex".`);
29
+ }
30
+ function createCalculatorToolProvider(config) {
31
+ switch (resolveCalculatorProviderId(config)) {
32
+ case "calculator-cortex":
33
+ return new CortexToolProvider();
34
+ case "calculator-geogebra":
35
+ return new GeoGebraToolProvider();
36
+ default:
37
+ return new DesmosToolProvider();
38
+ }
39
+ }
40
+ function getCalculatorInstanceConfig(config) {
41
+ const theme = config?.theme;
42
+ return {
43
+ settings: config?.settings && typeof config.settings === "object"
44
+ ? { ...config.settings }
45
+ : {},
46
+ restrictedMode: config?.restrictedMode === true,
47
+ locale: typeof config?.locale === "string" ? config.locale : undefined,
48
+ theme: theme === "light" || theme === "dark" || theme === "auto"
49
+ ? theme
50
+ : undefined,
51
+ };
52
+ }
15
53
  // The toolbar parent re-derives `renderedTools` whenever item state changes
16
54
  // (e.g. the learner answers a question and `effectiveItem`/`renderContext`
17
55
  // recompute). Calculator initialization is expensive (Desmos boot, container
@@ -45,7 +83,9 @@ function setCachedOverlay(coordinator, fullToolId, element) {
45
83
  scoped.set(fullToolId, element);
46
84
  }
47
85
  function normalizeCalculatorType(value) {
48
- return value === "basic" || value === "scientific" ? value : null;
86
+ return value === "basic" || value === "scientific" || value === "graphing"
87
+ ? value
88
+ : null;
49
89
  }
50
90
  function getCalculatorRenderParams(toolbarContext) {
51
91
  const params = toolbarContext.getToolRenderParams?.("calculator") ?? {};
@@ -61,15 +101,27 @@ function getCalculatorRenderParams(toolbarContext) {
61
101
  return {
62
102
  calculatorType,
63
103
  availableTypes,
64
- displayName: calculatorType === "scientific"
65
- ? "Scientific Calculator"
66
- : calculatorType === "basic"
67
- ? "Basic Calculator"
68
- : "Calculator",
104
+ displayName: toolbarContext.i18n.t(CALCULATOR_NAME_KEYS[calculatorType ?? "untyped"]),
69
105
  };
70
106
  }
71
- function applyCalculatorParamsToElement(element, calculatorType, availableTypes) {
107
+ /**
108
+ * The calculator's displayed name, per host-declared type.
109
+ *
110
+ * `untyped` keeps the pre-adoption behaviour of naming the scientific
111
+ * calculator, which is what the toolbar has always announced when the host
112
+ * declares no type.
113
+ */
114
+ const CALCULATOR_NAME_KEYS = {
115
+ basic: "tools.calculator.nameBasic",
116
+ scientific: "tools.calculator.nameScientific",
117
+ graphing: "tools.calculator.nameGraphing",
118
+ untyped: "tools.calculator.name",
119
+ };
120
+ function applyCalculatorParamsToElement(element, calculatorType, availableTypes, providerId, calculatorConfig) {
72
121
  const calculatorElement = element;
122
+ calculatorElement.providerId = providerId;
123
+ calculatorElement.calculatorConfig = calculatorConfig;
124
+ element.setAttribute("provider-id", providerId);
73
125
  if (calculatorType) {
74
126
  calculatorElement.calculatorType = calculatorType;
75
127
  element.setAttribute("calculator-type", calculatorType);
@@ -89,7 +141,7 @@ function applyCalculatorParamsToElement(element, calculatorType, availableTypes)
89
141
  * Calculator tool registration
90
142
  *
91
143
  * Supports:
92
- * - Basic, scientific, and graphing calculators via Desmos
144
+ * - Basic, scientific, and graphing calculators through Desmos, GeoGebra, or Cortex
93
145
  * - Context-aware visibility (shows only when math content is detected)
94
146
  * - Item level only
95
147
  */
@@ -97,27 +149,18 @@ export const calculatorToolRegistration = {
97
149
  toolId: "calculator",
98
150
  name: "Calculator",
99
151
  description: "Multi-type calculator (basic, scientific, graphing)",
152
+ nameKey: "tools.calculator.name",
153
+ descriptionKey: "tools.calculator.description",
100
154
  icon: "calculator",
101
155
  provider: {
102
- getProviderId: (config) => typeof config?.provider?.id === "string" && config.provider.id.length > 0
103
- ? config.provider.id
104
- : "calculator-desmos",
105
- createProvider: () => new DesmosToolProvider(),
156
+ getProviderId: resolveCalculatorProviderId,
157
+ createProvider: createCalculatorToolProvider,
106
158
  getInitConfig: (config) => config?.provider?.init ?? {},
107
159
  getAuthFetcher: (config) => {
108
160
  const runtimeAuthFetcher = config?.provider?.runtime?.authFetcher;
109
- if (typeof runtimeAuthFetcher === "function")
110
- return runtimeAuthFetcher;
111
- return async () => {
112
- const response = await fetch("/api/tools/desmos/auth", {
113
- method: "GET",
114
- credentials: "same-origin",
115
- });
116
- if (!response.ok) {
117
- throw new Error(`Failed to fetch Desmos auth config (${response.status})`);
118
- }
119
- return (await response.json());
120
- };
161
+ return typeof runtimeAuthFetcher === "function"
162
+ ? runtimeAuthFetcher
163
+ : undefined;
121
164
  },
122
165
  lazy: true,
123
166
  },
@@ -125,11 +168,13 @@ export const calculatorToolRegistration = {
125
168
  supportedLevels: ["item"],
126
169
  // PNP support IDs that enable this tool
127
170
  // Maps to QTI 3.0 standard features: calculator, graphingCalculator
171
+ // A type is not a feature id: `calculatorType` arrives through the host's
172
+ // render params, so `basicCalculator` / `scientificCalculator` granted the
173
+ // same untyped calculator these two do and only looked like they selected a
174
+ // variant.
128
175
  pnpSupportIds: [
129
176
  "calculator", // QTI 3.0 standard (cognitive.calculator)
130
177
  "graphingCalculator", // QTI 3.0 standard (assessment.graphingCalculator)
131
- "basicCalculator", // Common variant
132
- "scientificCalculator", // Common variant
133
178
  ],
134
179
  /**
135
180
  * Pass 2: Determine if calculator is relevant in this context
@@ -143,6 +188,9 @@ export const calculatorToolRegistration = {
143
188
  },
144
189
  renderToolbar(context, toolbarContext) {
145
190
  const { calculatorType, availableTypes, displayName } = getCalculatorRenderParams(toolbarContext);
191
+ const calculatorToolConfig = toolbarContext.toolkitCoordinator?.config.tools?.providers?.calculator;
192
+ const providerId = resolveCalculatorProviderId(calculatorToolConfig);
193
+ const calculatorConfig = getCalculatorInstanceConfig(calculatorToolConfig);
146
194
  const fullToolId = createScopedToolId(this.toolId, toolbarContext.scope.level, toolbarContext.scope.scopeId);
147
195
  const componentOverrides = toolbarContext.componentOverrides;
148
196
  const cachedOverlay = getCachedOverlay(toolbarContext.toolCoordinator, fullToolId);
@@ -153,19 +201,21 @@ export const calculatorToolRegistration = {
153
201
  }
154
202
  overlay.setAttribute("tool-id", fullToolId);
155
203
  overlay.toolkitCoordinator = toolbarContext.toolkitCoordinator;
156
- applyCalculatorParamsToElement(overlay, calculatorType, availableTypes);
157
- const openLabel = calculatorType === null
158
- ? "Open scientific calculator"
159
- : `Open ${displayName.toLowerCase()}`;
160
- const closeLabel = calculatorType === null
161
- ? "Close scientific calculator"
162
- : `Close ${displayName.toLowerCase()}`;
204
+ applyCalculatorParamsToElement(overlay, calculatorType, availableTypes, providerId, calculatorConfig);
163
205
  const button = {
164
206
  toolId: this.toolId,
165
207
  label: displayName,
166
208
  icon: typeof this.icon === "function" ? this.icon(context) : this.icon,
209
+ // Routes this button through <nds-icon-button> where the host enables NDS
210
+ // icons. Declared here because which capabilities render in the host's
211
+ // design system is a composition decision; the toolbar used to map it from
212
+ // the `calculator` toolId, which put a capability name in the generic core.
213
+ faIconName: "calculator",
167
214
  disabled: false,
168
- ariaLabel: openLabel,
215
+ // The name stays put across open and closed. The toolbar mirrors
216
+ // `active` onto the button as `aria-pressed`, so the state is already
217
+ // exposed; a name that also swapped to "Close …" would contradict it.
218
+ ariaLabel: displayName,
169
219
  tooltip: displayName,
170
220
  onClick: () => toolbarContext.toggleTool(this.toolId),
171
221
  active: toolbarContext.isToolVisible(fullToolId),
@@ -181,16 +231,63 @@ export const calculatorToolRegistration = {
181
231
  element: overlay,
182
232
  mount: "after-buttons",
183
233
  shell: {
184
- title: this.name,
234
+ // The variant name the button already carries — "Basic Calculator",
235
+ // not the registration's generic one.
236
+ title: displayName,
185
237
  draggable: true,
186
238
  resizable: true,
187
239
  closeable: true,
188
- initialWidth: 380,
189
- initialHeight: 420,
240
+ /*
241
+ * Sized per type from what each layout measures, rather than one
242
+ * size for all three. A shell subtracts ~74px of header from the
243
+ * height it is given, and the numbers below are the content each
244
+ * type needs plus room for its history tape:
245
+ *
246
+ * - basic: strip, display, edit row and a four-row keypad measure
247
+ * 398px. 560 left ~90px of blank above the entry line, which is
248
+ * the gap that made the panel look mis-sized.
249
+ * - scientific: the same plus a keypad layer tab row.
250
+ * - graphing: the expression rail beside the plot, whose column
251
+ * wants 486px on its own — viewport controls, a 14rem board floor
252
+ * and the readout that *is* the graph for assistive technology,
253
+ * since the board itself is `aria-hidden`. At 620 that column was
254
+ * 12px short and the readout's last line was cut off.
255
+ *
256
+ * The minimums stay shared across types and providers. This
257
+ * registration serves Desmos, GeoGebra and Cortex alike, so a
258
+ * graphing-only floor would move the resize limit under two vendors
259
+ * whose layouts were never measured for it. Cortex below 42rem
260
+ * stacks the rail above the plot and needs 701px there, which no
261
+ * spacing tier closes — it scrolls its own content instead of
262
+ * clipping it, which is the contract every size below the opening
263
+ * one already relies on.
264
+ */
265
+ initialWidth: calculatorType === "graphing" ? 720 : 380,
266
+ initialHeight: calculatorType === "graphing"
267
+ ? 660
268
+ : calculatorType === "basic"
269
+ ? 500
270
+ : 560,
190
271
  minWidth: 380,
191
- minHeight: 420,
192
- initialAlign: "bottom-right",
272
+ minHeight: 480,
273
+ /*
274
+ * `bottom-right` put a 560-620px-tall shell over the items
275
+ * column it shares the viewport with — tall enough, at
276
+ * ordinary viewport heights, to sit on top of a sibling
277
+ * item's own toolbar row and block its button from mouse
278
+ * and touch input. `bottom-left` keeps the same footprint
279
+ * over the passage column instead, which carries no
280
+ * per-item controls to collide with.
281
+ */
282
+ initialAlign: "bottom-left",
193
283
  initialMargin: 16,
284
+ // Header controls in the host's design system, and the layout that
285
+ // goes with them.
286
+ ndsHeaderControls: true,
287
+ // A learner reads the question while using the calculator, so Tab and
288
+ // Shift+Tab cross between the page and the shell instead of cycling
289
+ // inside it.
290
+ pageTabOrder: true,
194
291
  content: {
195
292
  overflowY: "auto",
196
293
  preserveMinHeight: true,
@@ -203,10 +300,12 @@ export const calculatorToolRegistration = {
203
300
  const active = toolbarContext.isToolVisible(fullToolId);
204
301
  button.active = active;
205
302
  button.label = displayName;
206
- button.ariaLabel = active ? closeLabel : openLabel;
207
- button.tooltip = active
208
- ? `Close ${displayName.toLowerCase()}`
209
- : displayName;
303
+ // Static across the toggle. The previous `Close ${name.toLowerCase()}`
304
+ // was a hardcoded English template built by lowercasing a localized
305
+ // noun — "Close rekenmachine" under nl-NL — and it is the state
306
+ // `aria-pressed` already carries.
307
+ button.ariaLabel = displayName;
308
+ button.tooltip = displayName;
210
309
  if (lastVisibleState !== active) {
211
310
  overlay.visible = active;
212
311
  lastVisibleState = active;
@@ -214,7 +313,7 @@ export const calculatorToolRegistration = {
214
313
  if (overlay.toolkitCoordinator !== toolbarContext.toolkitCoordinator) {
215
314
  overlay.toolkitCoordinator = toolbarContext.toolkitCoordinator;
216
315
  }
217
- applyCalculatorParamsToElement(overlay, calculatorType, availableTypes);
316
+ applyCalculatorParamsToElement(overlay, calculatorType, availableTypes, providerId, calculatorConfig);
218
317
  },
219
318
  subscribeActive: (callback) => {
220
319
  if (!toolbarContext.subscribeVisibility)
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Dictionary Tool Registrations
3
+ *
4
+ * Word lookup and picture (symbol) lookup, each a floating panel opened from the
5
+ * toolbar.
6
+ *
7
+ * Neither declares a universal support id. A dictionary is a granted accommodation,
8
+ * not a universal affordance: on a vocabulary item it is construct-relevant, so
9
+ * handing it to every learner by default would change what the item measures. A
10
+ * programme grants it through the PNP like any other accommodation.
11
+ *
12
+ * Both panels also accept a `term` from whatever selection affordance a host offers,
13
+ * but neither depends on one. A sighted keyboard-only learner cannot originate a text
14
+ * selection in non-editable content — Chromium does not extend one with Shift+Arrow
15
+ * there without caret browsing, an OS toggle absent on mobile — so a selection-only
16
+ * dictionary is unreachable for them. The panel's own field is the keyboard route.
17
+ */
18
+ import type { ToolRegistration } from "@pie-players/pie-assessment-toolkit/tools/internal";
19
+ /**
20
+ * What distinguishes one dictionary capability from another.
21
+ *
22
+ * A dictionary's language belongs to the learner, not to the content: the learner who
23
+ * needs a Spanish gloss is reading an English passage. So a deployment that offers two
24
+ * languages at once composes two capabilities, each granted on its own through the PNP,
25
+ * rather than one capability that follows whatever the section was authored in. That is
26
+ * also how the delivery systems PIE is measured against ship it — English Dictionary and
27
+ * Spanish Dictionary as two entries a programme grants separately.
28
+ *
29
+ * Catalog keys are derived from `toolId` (`tools.<toolId>.name` and so on), which holds for
30
+ * every packaged variant. A host composing a variant against its own catalog passes
31
+ * `messageKeyPrefix`; a key that does not resolve falls back to `name` and `description`,
32
+ * so a host catalog that lacks one is a plain label rather than a broken button.
33
+ */
34
+ export interface DictionaryVariantOptions {
35
+ /** Capability id, unique per language. Defaults to the base tool's id. */
36
+ toolId?: string;
37
+ name?: string;
38
+ description?: string;
39
+ /** Catalog prefix for this variant's strings. Defaults to `tools.<toolId>`. */
40
+ messageKeyPrefix?: string;
41
+ /** PNP ids that grant this variant. A language variant needs its own. */
42
+ pnpSupportIds?: string[];
43
+ /**
44
+ * The corpus language this capability looks words up in, as a BCP-47 tag.
45
+ *
46
+ * Sent with every lookup, outranking the toolbar's content-alternate language and
47
+ * outranked by a language the host names in this tool's render params. Absent on the
48
+ * base capabilities, which follow the content.
49
+ */
50
+ lookupLanguage?: string;
51
+ }
52
+ /**
53
+ * Dictionary tool registration
54
+ *
55
+ * Word lookup from a host-supplied service, in one language. Call this again with another
56
+ * `toolId`, `pnpSupportIds` and `lookupLanguage` to offer a second language beside it.
57
+ */
58
+ export declare function createDictionaryToolRegistration(options?: DictionaryVariantOptions): ToolRegistration;
59
+ /** The content-following dictionary every deployment gets. */
60
+ export declare const dictionaryToolRegistration: ToolRegistration;
61
+ /**
62
+ * Picture Dictionary tool registration
63
+ *
64
+ * Symbol or picture lookup from a host-supplied service. Sized wider than the word
65
+ * dictionary because its results are a grid rather than a column of text.
66
+ */
67
+ export declare function createPictureDictionaryToolRegistration(options?: DictionaryVariantOptions): ToolRegistration;
68
+ /** The content-following picture dictionary every deployment gets. */
69
+ export declare const pictureDictionaryToolRegistration: ToolRegistration;
70
+ /**
71
+ * Spanish variants, packaged because a Spanish gloss for English content is a named
72
+ * accommodation rather than a hypothetical. Neither appears unless a PNP grants it.
73
+ */
74
+ export declare const spanishDictionaryToolRegistration: ToolRegistration;
75
+ export declare const spanishPictureDictionaryToolRegistration: ToolRegistration;