@pie-players/pie-default-tool-loaders 0.3.64 → 0.3.66

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.
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Accessibility Tools Registrations
3
+ *
4
+ * Registers tools for visual accessibility and reading support:
5
+ * - Line Reader (reading guide)
6
+ * - Color Scheme (theme/contrast)
7
+ * - Annotation Toolbar (text highlighting)
8
+ *
9
+ * Maps to QTI 3.0 standard access features from:
10
+ * - visual category: highContrastDisplay, colorContrast
11
+ * - reading category: readingMask, readingGuide, highlighting
12
+ */
13
+ import { hasReadableText } from "@pie-players/pie-assessment-toolkit/tools/internal";
14
+ import { createToolElement, resolveToolTag, } from "@pie-players/pie-assessment-toolkit/tools/internal";
15
+ import { applyOverlaySurface, createScopedVisibilityBinding, syncButtonAndOverlayVisibility, } from "@pie-players/pie-assessment-toolkit/tools/internal";
16
+ /**
17
+ * Line Reader tool registration
18
+ *
19
+ * Provides a reading guide overlay to help track reading position.
20
+ * Useful for text-heavy passages and questions.
21
+ */
22
+ export const lineReaderToolRegistration = {
23
+ toolId: "lineReader",
24
+ name: "Line Reader",
25
+ description: "Reading guide overlay",
26
+ icon: "bars-3",
27
+ // Line reader appears where there's text to read
28
+ supportedLevels: ["section", "passage", "rubric", "item"],
29
+ // PNP support IDs
30
+ // Maps to QTI 3.0 standard features: readingMask, readingGuide, readingRuler
31
+ pnpSupportIds: [
32
+ "readingMask", // QTI 3.0 standard (reading.readingMask)
33
+ "readingGuide", // QTI 3.0 standard (reading.readingGuide)
34
+ "readingRuler", // QTI 3.0 standard (reading.readingRuler)
35
+ "lineReader", // Common variant
36
+ "trackingGuide", // Common variant
37
+ ],
38
+ /**
39
+ * Pass 2: Line reader is relevant when readable text is present
40
+ */
41
+ isVisibleInContext(context) {
42
+ return hasReadableText(context);
43
+ },
44
+ 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
+ };
76
+ },
77
+ };
78
+ /**
79
+ * Theme tool registration
80
+ *
81
+ * Provides accessible theme and contrast controls.
82
+ * Global tool that affects entire assessment.
83
+ */
84
+ export const themeToolRegistration = {
85
+ toolId: "theme",
86
+ name: "Theme",
87
+ description: "Accessible themes and contrast",
88
+ icon: "swatch",
89
+ // Color scheme is assessment-wide
90
+ supportedLevels: ["assessment", "section"],
91
+ // PNP support IDs
92
+ // Maps to QTI 3.0 standard features: highContrastDisplay, colorContrast, invertColors
93
+ pnpSupportIds: [
94
+ "highContrastDisplay", // QTI 3.0 standard (visual.highContrastDisplay)
95
+ "colorContrast", // QTI 3.0 standard (visual.colorContrast)
96
+ "invertColors", // QTI 3.0 standard (visual.invertColors)
97
+ "theme", // Canonical id
98
+ "highContrast", // Common variant
99
+ "customColors", // Common variant
100
+ ],
101
+ /**
102
+ * Pass 2: Color scheme is always relevant when allowed
103
+ */
104
+ isVisibleInContext(context) {
105
+ return true; // Always show if allowed by orchestrator
106
+ },
107
+ 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
+ }
150
+ },
151
+ subscribeActive: visibility.subscribeActive,
152
+ };
153
+ },
154
+ };
155
+ /**
156
+ * Annotation Toolbar registration
157
+ *
158
+ * Text highlighting with CSS Custom Highlight API.
159
+ * Zero DOM mutation, optimal performance.
160
+ */
161
+ export const annotationToolbarRegistration = {
162
+ toolId: "annotationToolbar",
163
+ name: "Highlighter",
164
+ description: "Highlight and annotate text",
165
+ icon: "highlighter",
166
+ activation: "selection-gateway",
167
+ singletonScope: "section",
168
+ /**
169
+ * Section-scoped singleton surface. The gateway used to be mounted by
170
+ * `PieSectionPlayerBaseElement.svelte`, which named this tool id in three
171
+ * places — the policy check, the module load and the element — so no host
172
+ * could contribute a second section-scoped capability. Declaring the surface
173
+ * moves that wiring into this registration, and the renderer discovers it
174
+ * through `ToolRegistry.getToolsBySurface("section-overlay")`.
175
+ */
176
+ surfaces: ["section-overlay"],
177
+ // Annotation appears where there's text content
178
+ supportedLevels: ["passage", "rubric", "item", "element"],
179
+ // PNP support IDs
180
+ // Maps to QTI 3.0 standard features: highlighting, annotations
181
+ pnpSupportIds: [
182
+ "highlighting", // QTI 3.0 standard (cognitive.highlighting / reading.wordHighlighting)
183
+ "annotations", // QTI 3.0 standard (cognitive.annotations)
184
+ "highlighter", // Common variant
185
+ "textHighlight", // Common variant
186
+ "annotation", // Common variant
187
+ ],
188
+ /**
189
+ * Pass 2: Annotation is relevant when readable text is present
190
+ */
191
+ isVisibleInContext(context) {
192
+ return hasReadableText(context);
193
+ },
194
+ /**
195
+ * Mount the section-scoped gateway. The element positions itself (`position:
196
+ * fixed` inside its shadow root) and finds its scope through the toolkit's
197
+ * context, which bubbles from wherever the host mounted it, so this needs to
198
+ * know nothing about the host's layout.
199
+ */
200
+ renderSurface(context) {
201
+ const componentOverrides = context.componentOverrides ?? {};
202
+ const tagName = resolveToolTag(context.toolId, componentOverrides);
203
+ if (typeof customElements !== "undefined" && !customElements.get(tagName)) {
204
+ // The host loads the module before mounting, so reaching here means the
205
+ // optional package is absent. Declining is the right answer: mounting an
206
+ // undefined element would render an empty box the learner cannot use.
207
+ return null;
208
+ }
209
+ const element = document.createElement(tagName);
210
+ // Reads the context it is handed. These were reactive props before the
211
+ // gateway moved behind `renderSurface`, and a host calling
212
+ // `updateAssessment(...)` mid-session swaps the coordinator without
213
+ // remounting — closing over the render-time services would leave the gateway
214
+ // highlighting into the previous session's coordinator.
215
+ const applyServices = (current) => {
216
+ element.enabled = true;
217
+ element.ttsService = current.services.ttsService;
218
+ element.highlightCoordinator =
219
+ current.services.toolkitCoordinator?.highlightCoordinator ?? null;
220
+ };
221
+ applyServices(context);
222
+ return { element, sync: applyServices };
223
+ },
224
+ 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
+ };
255
+ },
256
+ };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Audio transcript: the text of an audio prompt, rendered for the learners
3
+ * entitled to it.
4
+ *
5
+ * Two kinds of content wear the same shape, and the card says which:
6
+ *
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.
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.
15
+ *
16
+ * The toolkit renders the text, not the element. Before this, the transcript rode
17
+ * on `model.audioTranscript` and `mc-populated-blank` revealed its own copy when
18
+ * an ancestor carried `.rli-with-audio-transcript` — which made delivering an
19
+ * accommodation the host's job, required every element carrying audio to
20
+ * reimplement the reveal, and put an element-specific CSS class in the contract.
21
+ * Rendering here is the same shape signing already has, and it is what makes one
22
+ * mechanism serve braille, simplified-language and the rest.
23
+ *
24
+ * No `aria-describedby` back to the element's audio control, deliberately. A
25
+ * description is announced as a flat string when a control takes focus, so
26
+ * pointing one at a multi-sentence transcript is worse to listen to than reading
27
+ * order: this renders a labelled region immediately before the content, where a
28
+ * screen-reader user meets it on the way to the control it belongs to.
29
+ *
30
+ * It ships in the packaged set, unlike signing, because its presentation half has
31
+ * to work with no host opt-in: an item authored to show its transcript must show
32
+ * it in every player, and a deployment that forgot an import would silently
33
+ * deliver a designed-for reading support as nothing. The accommodation half is
34
+ * still policy-gated exactly as signing is, and `transcript` is deliberately
35
+ * absent from the universal preset — a content-dependent support never belongs in
36
+ * a wholesale grant.
37
+ */
38
+ import type { ToolContentDependencyContext, ToolRegistration } from "@pie-players/pie-assessment-toolkit/tools/internal";
39
+ /** QTI 3.0 / AfA support id gating an audio transcript. */
40
+ export declare const AUDIO_TRANSCRIPT_FEATURE_ID = "transcript";
41
+ /** Catalog type carrying the transcript text. */
42
+ export declare const AUDIO_TRANSCRIPT_CATALOG_TYPE = "transcript";
43
+ /** Accessible name of the rendered region, and its class hook for themes. */
44
+ export declare const AUDIO_TRANSCRIPT_REGION_LABEL = "Transcript";
45
+ export declare const AUDIO_TRANSCRIPT_REGION_CLASS = "pie-tool-audio-transcript";
46
+ /** The transcript to render, and why it is being rendered. */
47
+ export interface ResolvedAudioTranscript {
48
+ catalogId: string;
49
+ text: string;
50
+ language?: string;
51
+ /** True when the card declares itself authored presentation. */
52
+ always: boolean;
53
+ }
54
+ /**
55
+ * The transcript an entity carries, or `null`.
56
+ *
57
+ * Read from the owner snapshot the resolver produced. Item/passage/model
58
+ * traversal and registration precedence have already been applied, so this
59
+ * capability owns only transcript interpretation.
60
+ *
61
+ * The cards are read directly rather than through `AccessibilityCatalogResolver`,
62
+ * which flattens a card to type/language/content and so cannot carry
63
+ * `visibility` — the one attribute this decision turns on. Nothing is lost by it:
64
+ * the resolver's value is choosing between languages of the same alternate, and a
65
+ * transcript is in the language of the audio it transcribes, one per prompt.
66
+ */
67
+ export declare function resolveAudioTranscript(context: ToolContentDependencyContext): ResolvedAudioTranscript | null;
68
+ /**
69
+ * Host surface this capability fills: the full-width slot above a card's content.
70
+ *
71
+ * Item cards and passage cards open the same surface, so declaring it once
72
+ * reaches both — a passage can carry an audio prompt as an item can.
73
+ */
74
+ export declare const CONTENT_LEAD_SURFACE = "content-lead";
75
+ export declare const audioTranscriptRegistration: ToolRegistration;
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Audio transcript: the text of an audio prompt, rendered for the learners
3
+ * entitled to it.
4
+ *
5
+ * Two kinds of content wear the same shape, and the card says which:
6
+ *
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.
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.
15
+ *
16
+ * The toolkit renders the text, not the element. Before this, the transcript rode
17
+ * on `model.audioTranscript` and `mc-populated-blank` revealed its own copy when
18
+ * an ancestor carried `.rli-with-audio-transcript` — which made delivering an
19
+ * accommodation the host's job, required every element carrying audio to
20
+ * reimplement the reveal, and put an element-specific CSS class in the contract.
21
+ * Rendering here is the same shape signing already has, and it is what makes one
22
+ * mechanism serve braille, simplified-language and the rest.
23
+ *
24
+ * No `aria-describedby` back to the element's audio control, deliberately. A
25
+ * description is announced as a flat string when a control takes focus, so
26
+ * pointing one at a multi-sentence transcript is worse to listen to than reading
27
+ * order: this renders a labelled region immediately before the content, where a
28
+ * screen-reader user meets it on the way to the control it belongs to.
29
+ *
30
+ * It ships in the packaged set, unlike signing, because its presentation half has
31
+ * to work with no host opt-in: an item authored to show its transcript must show
32
+ * it in every player, and a deployment that forgot an import would silently
33
+ * deliver a designed-for reading support as nothing. The accommodation half is
34
+ * still policy-gated exactly as signing is, and `transcript` is deliberately
35
+ * absent from the universal preset — a content-dependent support never belongs in
36
+ * a wholesale grant.
37
+ */
38
+ /** QTI 3.0 / AfA support id gating an audio transcript. */
39
+ export const AUDIO_TRANSCRIPT_FEATURE_ID = "transcript";
40
+ /** Catalog type carrying the transcript text. */
41
+ export const AUDIO_TRANSCRIPT_CATALOG_TYPE = "transcript";
42
+ /** Accessible name of the rendered region, and its class hook for themes. */
43
+ export const AUDIO_TRANSCRIPT_REGION_LABEL = "Transcript";
44
+ export const AUDIO_TRANSCRIPT_REGION_CLASS = "pie-tool-audio-transcript";
45
+ /** Card value that makes a transcript authored presentation. */
46
+ const ALWAYS_VISIBLE = "always";
47
+ const isTranscriptCard = (card) => !!card && card.catalog === AUDIO_TRANSCRIPT_CATALOG_TYPE;
48
+ /**
49
+ * Whether a card declares itself authored presentation.
50
+ *
51
+ * Absent reads as the accommodation, the same fail-closed reading the policy
52
+ * engine takes of a silent profile: a card that says nothing about presentation
53
+ * has not claimed to be presentation.
54
+ */
55
+ const isAlwaysVisible = (card) => typeof card.visibility === "string" &&
56
+ card.visibility.trim().toLowerCase() === ALWAYS_VISIBLE;
57
+ const cardText = (card) => typeof card.content === "string" ? card.content.trim() : "";
58
+ /**
59
+ * The transcript an entity carries, or `null`.
60
+ *
61
+ * Read from the owner snapshot the resolver produced. Item/passage/model
62
+ * traversal and registration precedence have already been applied, so this
63
+ * capability owns only transcript interpretation.
64
+ *
65
+ * The cards are read directly rather than through `AccessibilityCatalogResolver`,
66
+ * which flattens a card to type/language/content and so cannot carry
67
+ * `visibility` — the one attribute this decision turns on. Nothing is lost by it:
68
+ * the resolver's value is choosing between languages of the same alternate, and a
69
+ * transcript is in the language of the audio it transcribes, one per prompt.
70
+ */
71
+ export function resolveAudioTranscript(context) {
72
+ if (!context.catalogs)
73
+ return null;
74
+ let firstOnGrant = null;
75
+ for (const { catalogId, card } of context.catalogs.cards) {
76
+ if (!isTranscriptCard(card))
77
+ continue;
78
+ const text = cardText(card);
79
+ // A card with no text is not a transcript; rendering an empty region
80
+ // would announce an alternate that is not there.
81
+ if (!text)
82
+ continue;
83
+ const resolved = {
84
+ catalogId,
85
+ text,
86
+ language: typeof card.language === "string" ? card.language : undefined,
87
+ always: isAlwaysVisible(card),
88
+ };
89
+ // An `always` card answers whether or not policy granted anything, so it
90
+ // wins over an accommodation card found earlier.
91
+ if (resolved.always)
92
+ return resolved;
93
+ firstOnGrant ??= resolved;
94
+ }
95
+ if (!firstOnGrant)
96
+ return null;
97
+ // The accommodation half: without a grant this learner is not entitled to it,
98
+ // and being asked anyway is the price of also serving the authored case.
99
+ return context.granted ? firstOnGrant : null;
100
+ }
101
+ /**
102
+ * Host surface this capability fills: the full-width slot above a card's content.
103
+ *
104
+ * Item cards and passage cards open the same surface, so declaring it once
105
+ * reaches both — a passage can carry an audio prompt as an item can.
106
+ */
107
+ export const CONTENT_LEAD_SURFACE = "content-lead";
108
+ /**
109
+ * A labelled region holding the transcript text.
110
+ *
111
+ * Plain DOM rather than a custom element: there is no state, no lifecycle and
112
+ * nothing to load, so a package and a registration ceremony would buy nothing.
113
+ * `renderSurface` promises an `HTMLElement`, not a custom one.
114
+ */
115
+ function buildTranscriptRegion() {
116
+ const region = document.createElement("section");
117
+ region.className = AUDIO_TRANSCRIPT_REGION_CLASS;
118
+ // A region rather than a bare paragraph so it is reachable as a landmark and
119
+ // announces its purpose before its content.
120
+ region.setAttribute("role", "region");
121
+ const text = document.createElement("p");
122
+ text.className = `${AUDIO_TRANSCRIPT_REGION_CLASS}__text`;
123
+ region.appendChild(text);
124
+ return region;
125
+ }
126
+ const applyTranscript = (region, transcript) => {
127
+ const text = region.querySelector(`.${AUDIO_TRANSCRIPT_REGION_CLASS}__text`);
128
+ if (!text)
129
+ return;
130
+ text.textContent = transcript?.text ?? "";
131
+ if (transcript?.language) {
132
+ region.setAttribute("lang", transcript.language);
133
+ }
134
+ else {
135
+ region.removeAttribute("lang");
136
+ }
137
+ // Which half put it on screen, for a policy debugger and for hosts that theme
138
+ // authored presentation differently from an accommodation.
139
+ region.dataset.transcriptVisibility = transcript?.always
140
+ ? "always"
141
+ : "onGrant";
142
+ };
143
+ export const audioTranscriptRegistration = {
144
+ toolId: AUDIO_TRANSCRIPT_FEATURE_ID,
145
+ name: "Audio Transcript",
146
+ description: "Text of an audio prompt, read before the content it transcribes",
147
+ supportedLevels: ["item", "passage"],
148
+ pnpSupportIds: [AUDIO_TRANSCRIPT_FEATURE_ID],
149
+ // No button to press: a transcript is either authored-visible, or granted and
150
+ // present, or absent. `region` is the activation for a capability with no
151
+ // toolbar presence, and tools-config validation reports a `tools.placement`
152
+ // entry naming it as unplaceable rather than silently doing nothing.
153
+ activation: "region",
154
+ surfaces: [CONTENT_LEAD_SURFACE],
155
+ // The resource half of the AfA pair, and the only reader of the card.
156
+ requiresAuthoredContent: {
157
+ resolve(context) {
158
+ return resolveAudioTranscript(context);
159
+ },
160
+ description: "An accessibility catalog card of type `transcript` on the item or passage",
161
+ },
162
+ // Authored presentation is not a grant, so the content has to be consulted even
163
+ // when policy granted nothing; `resolveAudioTranscript` returns null for an
164
+ // accommodation card in that case.
165
+ resolvesWithoutGrant: true,
166
+ renderSurface(context) {
167
+ const transcript = context.content;
168
+ // No content means the host asked before resolving, or resolved to nothing.
169
+ // Declining is the honest answer; an empty region is not.
170
+ if (!transcript)
171
+ return null;
172
+ const region = buildTranscriptRegion();
173
+ applyTranscript(region, transcript);
174
+ return {
175
+ element: region,
176
+ ariaLabel: AUDIO_TRANSCRIPT_REGION_LABEL,
177
+ // Reads the context it is handed, never the one captured above: on a
178
+ // re-resolve the host's context carries the current card, and a learner must
179
+ // not keep reading the previous item's transcript.
180
+ sync: (current) => applyTranscript(region, current.content),
181
+ };
182
+ },
183
+ };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Calculator Tool Registration
3
+ *
4
+ * Registers the calculator tool with support for multiple calculator types
5
+ * (basic, scientific, graphing) via Desmos provider.
6
+ *
7
+ * Maps to QTI 3.0 standard access features:
8
+ * - calculator (cognitive support)
9
+ * - graphingCalculator (assessment tool)
10
+ */
11
+ import type { ToolRegistration } from "@pie-players/pie-assessment-toolkit/tools/internal";
12
+ /**
13
+ * Calculator tool registration
14
+ *
15
+ * Supports:
16
+ * - Basic, scientific, and graphing calculators via Desmos
17
+ * - Context-aware visibility (shows only when math content is detected)
18
+ * - Item level only
19
+ */
20
+ export declare const calculatorToolRegistration: ToolRegistration;