@pie-players/pie-default-tool-loaders 0.3.65 → 0.3.67

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,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
+ };
@@ -1,29 +1 @@
1
- /**
2
- * Placement presets for the packaged capability set.
3
- *
4
- * Composition data for the same reason the tag map and the registrations are:
5
- * they name capabilities. The toolkit keeps `DEFAULT_TOOL_PLACEMENT`, which is
6
- * empty at every level and names none.
7
- */
8
- /**
9
- * Every packaged capability at every level it supports. Prefer explicit host
10
- * configuration over adopting this wholesale.
11
- */
12
- export declare const PACKAGED_TOOL_PLACEMENT: {
13
- readonly assessment: readonly ["theme"];
14
- readonly section: readonly ["theme"];
15
- readonly item: readonly ["textToSpeech", "highlighter", "annotationToolbar", "graph", "periodicTable"];
16
- readonly passage: readonly ["textToSpeech", "highlighter", "annotationToolbar", "lineReader"];
17
- readonly rubric: readonly ["textToSpeech", "highlighter", "annotationToolbar", "lineReader"];
18
- readonly element: readonly ["calculator", "answerEliminator", "textToSpeech", "ruler", "protractor", "highlighter", "annotationToolbar", "graph", "periodicTable"];
19
- };
20
- /**
21
- * Each packaged capability once, at its usual assessment surface. Not
22
- * exhaustive: `supportedLevels` still bounds where a capability can run, and a
23
- * host can place them differently for its own UX.
24
- */
25
- export declare const SECTION_PLAYER_PREFERRED_TOOL_PLACEMENT: {
26
- section: string[];
27
- item: string[];
28
- passage: string[];
29
- };
1
+ export { PACKAGED_TOOL_PLACEMENT, SECTION_PLAYER_PREFERRED_TOOL_PLACEMENT, } from "./packaged-capability-composition.js";
@@ -1,52 +1 @@
1
- /**
2
- * Placement presets for the packaged capability set.
3
- *
4
- * Composition data for the same reason the tag map and the registrations are:
5
- * they name capabilities. The toolkit keeps `DEFAULT_TOOL_PLACEMENT`, which is
6
- * empty at every level and names none.
7
- */
8
- /**
9
- * Every packaged capability at every level it supports. Prefer explicit host
10
- * configuration over adopting this wholesale.
11
- */
12
- export const PACKAGED_TOOL_PLACEMENT = {
13
- assessment: ["theme"],
14
- section: ["theme"],
15
- item: [
16
- "textToSpeech",
17
- "highlighter",
18
- "annotationToolbar",
19
- "graph",
20
- "periodicTable",
21
- ],
22
- passage: ["textToSpeech", "highlighter", "annotationToolbar", "lineReader"],
23
- rubric: ["textToSpeech", "highlighter", "annotationToolbar", "lineReader"],
24
- element: [
25
- "calculator",
26
- "answerEliminator",
27
- "textToSpeech",
28
- "ruler",
29
- "protractor",
30
- "highlighter",
31
- "annotationToolbar",
32
- "graph",
33
- "periodicTable",
34
- ],
35
- };
36
- /**
37
- * Each packaged capability once, at its usual assessment surface. Not
38
- * exhaustive: `supportedLevels` still bounds where a capability can run, and a
39
- * host can place them differently for its own UX.
40
- */
41
- export const SECTION_PLAYER_PREFERRED_TOOL_PLACEMENT = {
42
- section: [
43
- "theme",
44
- "graph",
45
- "periodicTable",
46
- "lineReader",
47
- "ruler",
48
- "protractor",
49
- ],
50
- item: ["calculator", "textToSpeech", "answerEliminator", "annotationToolbar"],
51
- passage: ["textToSpeech", "annotationToolbar"],
52
- };
1
+ export { PACKAGED_TOOL_PLACEMENT, SECTION_PLAYER_PREFERRED_TOOL_PLACEMENT, } from "./packaged-capability-composition.js";
@@ -1,16 +1 @@
1
- import type { ToolTagMap } from "@pie-players/pie-assessment-toolkit/tools/internal";
2
- /**
3
- * Element tag for each packaged tool.
4
- *
5
- * Composition data, not core data: it is a catalogue of which capabilities exist
6
- * in a deployment and which element renders each. It lived in the toolkit, where
7
- * a core module naming eleven capabilities meant a host could not add a twelfth
8
- * without editing our package — the map was overridable, but its default was
9
- * core-resident, so the core still had to know the names.
10
- *
11
- * `resolveToolTag` in the toolkit reads only the overrides it is handed, and
12
- * `createPackagedToolRegistry` installs this map through
13
- * `ToolRegistry.setComponentOverrides`. A host composing its own registry
14
- * supplies its own map the same way.
15
- */
16
- export declare const PACKAGED_TOOL_TAG_MAP: ToolTagMap;
1
+ export { PACKAGED_TOOL_TAG_MAP } from "./packaged-capability-composition.js";
@@ -1,27 +1 @@
1
- /**
2
- * Element tag for each packaged tool.
3
- *
4
- * Composition data, not core data: it is a catalogue of which capabilities exist
5
- * in a deployment and which element renders each. It lived in the toolkit, where
6
- * a core module naming eleven capabilities meant a host could not add a twelfth
7
- * without editing our package — the map was overridable, but its default was
8
- * core-resident, so the core still had to know the names.
9
- *
10
- * `resolveToolTag` in the toolkit reads only the overrides it is handed, and
11
- * `createPackagedToolRegistry` installs this map through
12
- * `ToolRegistry.setComponentOverrides`. A host composing its own registry
13
- * supplies its own map the same way.
14
- */
15
- export const PACKAGED_TOOL_TAG_MAP = {
16
- calculator: "pie-tool-calculator",
17
- textToSpeech: "pie-tool-text-to-speech",
18
- ruler: "pie-tool-ruler",
19
- protractor: "pie-tool-protractor",
20
- answerEliminator: "pie-tool-answer-eliminator",
21
- highlighter: "pie-tool-annotation-toolbar",
22
- lineReader: "pie-tool-line-reader",
23
- theme: "pie-tool-theme",
24
- annotationToolbar: "pie-tool-annotation-toolbar",
25
- graph: "pie-tool-graph",
26
- periodicTable: "pie-tool-periodic-table",
27
- };
1
+ export { PACKAGED_TOOL_TAG_MAP } from "./packaged-capability-composition.js";
@@ -1,33 +1 @@
1
- /**
2
- * Named preset of the PNP support ids the packaged tool set treats as universal
3
- * features — data, not a derivation.
4
- *
5
- * This list was previously computed at import time by unioning every registered
6
- * tool's `pnpSupportIds`, which made registry membership decide eligibility tier
7
- * and had to be corrected by a compile-time exclusion list no host could extend.
8
- * The ids below are that computation's output, frozen: adopt it, extend it, or
9
- * replace it alongside the district and test-administration configuration, the
10
- * same way any other policy input is chosen.
11
- *
12
- * Two properties this list must keep:
13
- *
14
- * 1. **Universal features only.** An accommodation belongs here only if the
15
- * program has decided it is universal — which is a decision about the
16
- * program, not about the capability. TTS sits here because the packaged set
17
- * treats it as a universal feature; it is a documented accommodation in
18
- * other programs.
19
- * 2. **No content-dependent capability.** A capability declaring
20
- * `requiresAuthoredContent` needs an authored resource to have anything to
21
- * show, so granting it wholesale grants an accommodation to learners who
22
- * have no documented need for it. `tests/universal-supports.test.ts`
23
- * enforces this against the registrations rather than against a name list.
24
- */
25
- import type { PersonalNeedsProfile } from "@pie-players/pie-players-shared/types";
26
- export declare const UNIVERSAL_SUPPORTS_PRESET: readonly string[];
27
- /**
28
- * Build a profile granting {@link UNIVERSAL_SUPPORTS_PRESET}.
29
- *
30
- * A fresh array per call: a profile flows into policy inputs that hosts mutate,
31
- * and handing out a shared reference would let one host's edit reach another's.
32
- */
33
- export declare function createUniversalPersonalNeedsProfile(): PersonalNeedsProfile;
1
+ export { UNIVERSAL_SUPPORTS_PRESET, createUniversalPersonalNeedsProfile, } from "./packaged-capability-composition.js";
@@ -1,77 +1 @@
1
- /**
2
- * Named preset of the PNP support ids the packaged tool set treats as universal
3
- * features — data, not a derivation.
4
- *
5
- * This list was previously computed at import time by unioning every registered
6
- * tool's `pnpSupportIds`, which made registry membership decide eligibility tier
7
- * and had to be corrected by a compile-time exclusion list no host could extend.
8
- * The ids below are that computation's output, frozen: adopt it, extend it, or
9
- * replace it alongside the district and test-administration configuration, the
10
- * same way any other policy input is chosen.
11
- *
12
- * Two properties this list must keep:
13
- *
14
- * 1. **Universal features only.** An accommodation belongs here only if the
15
- * program has decided it is universal — which is a decision about the
16
- * program, not about the capability. TTS sits here because the packaged set
17
- * treats it as a universal feature; it is a documented accommodation in
18
- * other programs.
19
- * 2. **No content-dependent capability.** A capability declaring
20
- * `requiresAuthoredContent` needs an authored resource to have anything to
21
- * show, so granting it wholesale grants an accommodation to learners who
22
- * have no documented need for it. `tests/universal-supports.test.ts`
23
- * enforces this against the registrations rather than against a name list.
24
- */
25
- export const UNIVERSAL_SUPPORTS_PRESET = Object.freeze([
26
- "angleMeasurement",
27
- "annotation",
28
- "annotations",
29
- "answerEliminator",
30
- "answerMasking",
31
- "basicCalculator",
32
- "calculator",
33
- "chemistryReference",
34
- "choiceMasking",
35
- "colorContrast",
36
- "coordinatePlane",
37
- "customColors",
38
- "elementReference",
39
- "graph",
40
- "graphingCalculator",
41
- "graphingTool",
42
- "highContrast",
43
- "highContrastDisplay",
44
- "highlighter",
45
- "highlighting",
46
- "invertColors",
47
- "lineReader",
48
- "measurement",
49
- "periodicTable",
50
- "protractor",
51
- "readAloud",
52
- "readingGuide",
53
- "readingMask",
54
- "readingRuler",
55
- "ruler",
56
- "scientificCalculator",
57
- "speechOutput",
58
- "strikethrough",
59
- "textHighlight",
60
- "textToSpeech",
61
- "theme",
62
- "trackingGuide",
63
- "tts",
64
- ]);
65
- /**
66
- * Build a profile granting {@link UNIVERSAL_SUPPORTS_PRESET}.
67
- *
68
- * A fresh array per call: a profile flows into policy inputs that hosts mutate,
69
- * and handing out a shared reference would let one host's edit reach another's.
70
- */
71
- export function createUniversalPersonalNeedsProfile() {
72
- return {
73
- supports: [...UNIVERSAL_SUPPORTS_PRESET],
74
- prohibitedSupports: [],
75
- activateAtInit: [],
76
- };
77
- }
1
+ export { UNIVERSAL_SUPPORTS_PRESET, createUniversalPersonalNeedsProfile, } from "./packaged-capability-composition.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pie-players/pie-default-tool-loaders",
3
- "version": "0.3.65",
3
+ "version": "0.3.67",
4
4
  "type": "module",
5
5
  "description": "Composition layer for the packaged PIE assessment tools: registrations, tag map, placement presets, universal supports and lazy module loaders",
6
6
  "repository": {
@@ -25,25 +25,26 @@
25
25
  "README.md"
26
26
  ],
27
27
  "scripts": {
28
- "build": "tsc -p tsconfig.json",
28
+ "build": "bun run check:composition && tsc -p tsconfig.json",
29
+ "check:composition": "bun test tests/packaged-capability-composition.test.ts",
29
30
  "dev": "tsc -p tsconfig.json --watch",
30
31
  "typecheck": "tsc -p tsconfig.json --noEmit",
31
32
  "lint": "biome check .",
32
33
  "test": "bun test"
33
34
  },
34
35
  "dependencies": {
35
- "@pie-players/pie-assessment-toolkit": "0.3.65",
36
- "@pie-players/pie-players-shared": "0.3.65",
37
- "@pie-players/pie-tool-annotation-toolbar": "0.3.65",
38
- "@pie-players/pie-tool-answer-eliminator": "0.3.65",
39
- "@pie-players/pie-tool-calculator-desmos": "0.3.65",
40
- "@pie-players/pie-tool-graph": "0.3.65",
41
- "@pie-players/pie-tool-line-reader": "0.3.65",
42
- "@pie-players/pie-tool-periodic-table": "0.3.65",
43
- "@pie-players/pie-tool-protractor": "0.3.65",
44
- "@pie-players/pie-tool-ruler": "0.3.65",
45
- "@pie-players/pie-tool-theme": "0.3.65",
46
- "@pie-players/pie-tool-tts-inline": "0.3.65"
36
+ "@pie-players/pie-assessment-toolkit": "0.3.67",
37
+ "@pie-players/pie-players-shared": "0.3.67",
38
+ "@pie-players/pie-tool-annotation-toolbar": "0.3.67",
39
+ "@pie-players/pie-tool-answer-eliminator": "0.3.67",
40
+ "@pie-players/pie-tool-calculator-desmos": "0.3.67",
41
+ "@pie-players/pie-tool-graph": "0.3.67",
42
+ "@pie-players/pie-tool-line-reader": "0.3.67",
43
+ "@pie-players/pie-tool-periodic-table": "0.3.67",
44
+ "@pie-players/pie-tool-protractor": "0.3.67",
45
+ "@pie-players/pie-tool-ruler": "0.3.67",
46
+ "@pie-players/pie-tool-theme": "0.3.67",
47
+ "@pie-players/pie-tool-tts-inline": "0.3.67"
47
48
  },
48
49
  "devDependencies": {
49
50
  "@biomejs/biome": "^2.5.7",