@pie-element/mc-populated-blank 0.1.0 → 0.2.0

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.
@@ -1,79 +1,102 @@
1
1
  <svelte:options
2
2
  customElement={{
3
+ tag: 'mc-populated-blank-element',
3
4
  shadow: 'none',
4
5
  props: {
5
6
  model: { type: 'Object' },
6
7
  session: { type: 'Object' },
7
8
  options: { type: 'Object' },
8
- onSessionChange: { type: 'Object' },
9
- onAudioStarted: { type: 'Object' },
10
- onAudioEnded: { type: 'Object' },
11
9
  },
12
10
  }}
13
11
  />
14
12
 
15
13
  <script lang="ts">
16
- import { onDestroy } from 'svelte';
17
-
18
14
  /** Must match controller `BLANK_TOKEN` (kept local to avoid pulling controller into delivery). */
19
15
  const BLANK_TOKEN = '{{blank}}';
20
-
21
- type McChoice = {
22
- id: string;
23
- labelHtml?: string;
24
- imageUrl?: string;
25
- imageAlt?: string;
26
- };
27
- type McModel = {
28
- id?: string;
29
- mode?: string;
30
- correctness?: 'correct' | 'incorrect' | string;
31
- responseCorrect?: boolean;
32
- interactionMode?: string;
33
- template?: string;
34
- layoutProfile?: string;
35
- choiceLayout?: 'horizontal' | 'vertical' | string;
36
- showVisibleTranscript?: boolean;
37
- hasAudio?: boolean;
38
- audioUrl?: string;
39
- audioTranscript?: string;
40
- choices?: McChoice[];
41
- choiceMode?: 'text' | 'image' | string;
42
- prompt?: string;
43
- sentenceHtml?: string;
44
- locale?: string;
45
- autoplayAudioEnabled?: boolean;
46
- disabled?: boolean;
47
- correctChoiceId?: string;
48
- shuffle?: boolean;
49
- lockChoiceOrder?: boolean;
50
- teacherInstructions?: string | null;
51
- };
52
- type McSession = {
53
- id?: string;
54
- element?: string;
55
- choiceId?: string;
56
- [key: string]: unknown;
16
+ const DEFAULT_LAYOUT_LIMITS = {
17
+ blankStandaloneWidthRem: 7,
18
+ blankWideWidthRem: 10,
19
+ blankUnderlineWidthPx: 2,
20
+ blankUnderlineWideWidthPx: 4,
21
+ horizontalChoiceWidthPx: 170,
22
+ horizontalChoiceWidthVw: 30,
23
+ horizontalChoiceTileMinHeightRem: 11,
24
+ horizontalChoiceContentMinHeightRem: 7.5,
25
+ selectedImageMaxHeightRem: 4,
26
+ choiceImageMaxHeightRem: 5,
27
+ listenButtonSizePx: 128,
28
+ stimulusMinColumnPx: 210,
29
+ textMinColumnPx: 260,
30
+ legendMaxChars: 120,
31
+ choiceGroupGapRem: 0.5,
32
+ choiceRowGapRem: 0.5,
33
+ toggleButtonGapRem: 0.5,
34
+ horizontalChoiceRadioTopMarginRem: 0.5,
35
+ audioBlankTemplateMarginTopRem: 0.8,
36
+ audioBlankTemplateMarginBottomRem: 1.8,
37
+ stimulusGridColumnGapRem: 2,
38
+ stimulusGridRowGapRem: 0.7,
39
+ stimulusSentenceMarginTopRem: 0.2,
40
+ stimulusChoicesMarginTopRem: 0.9,
41
+ tokenGridColumnGapRem: 1.5,
42
+ tokenGridRowGapRem: 0.8,
43
+ tokenTemplateMarginTopRem: 0.6,
44
+ tokenInlineTokenGapRem: 0.35,
45
+ tokenChoicesMarginTopRem: 0.2,
46
+ inlineGridColumnGapRem: 1.5,
47
+ inlineGridRowGapRem: 0.65,
48
+ inlineTemplateMarginTopRem: 0.45,
49
+ inlineChoicesMarginTopRem: 0.25,
50
+ } as const;
51
+ const DEFAULT_LAYOUT_PROFILE_PRESETS: Record<string, Record<string, number>> = {
52
+ audio_blank_only: {
53
+ blankWideWidthRem: 10,
54
+ blankUnderlineWideWidthPx: 4,
55
+ },
56
+ stimulus_image_blank: {
57
+ blankWideWidthRem: 10,
58
+ blankUnderlineWideWidthPx: 4,
59
+ },
60
+ token_sequence: {
61
+ blankStandaloneWidthRem: 7,
62
+ blankUnderlineWideWidthPx: 4,
63
+ },
57
64
  };
65
+ const DEFAULT_AUDIO_BUTTON_SKINS = {
66
+ default: {
67
+ silentUrl:
68
+ 'https://assets.learnosity.com/organisations/844/0c9f2aa3-3cd5-4de7-93ef-541c24ca35da.svg',
69
+ playingUrl:
70
+ 'https://assets.learnosity.com/organisations/844/231dfdc2-c113-4be5-91fb-e75a0ca5994b.svg',
71
+ },
72
+ es: {
73
+ silentUrl:
74
+ 'https://assets.learnosity.com/organisations/844/27a9d5b5-d873-4bd5-b9ba-22748782d8ba.svg',
75
+ playingUrl:
76
+ 'https://assets.learnosity.com/organisations/844/120f216d-96b7-4560-94b8-1d90710216b7.svg',
77
+ },
78
+ } as const;
79
+ const DEFAULT_UI_TEXT = {
80
+ answerChoices: 'Answer choices',
81
+ selectedAnswerInSentence: 'Selected answer in sentence',
82
+ showCorrectAnswer: 'Show correct answer',
83
+ hideCorrectAnswer: 'Hide correct answer',
84
+ clickToEnableAutoplay: 'Click to enable audio autoplay',
85
+ audioResourceUnavailable: 'Audio is enabled but no playable audio URL is configured.',
86
+ transcriptLabel: 'Transcript',
87
+ listenLabelEn: 'Listen',
88
+ listenLabelEs: 'Escuchar',
89
+ } as const;
58
90
 
59
- let props = $props<{
60
- model?: McModel;
61
- session?: McSession;
62
- options?: Record<string, unknown>;
63
- onSessionChange?: (session: McSession) => void;
64
- onAudioStarted?: () => void;
65
- onAudioEnded?: () => void;
66
- }>();
91
+ let props = $props<{ model?: any; session?: any; options?: any }>();
67
92
  let audioEl = $state<HTMLAudioElement | null>(null);
68
- let activeUtterance = $state<SpeechSynthesisUtterance | null>(null);
69
- let isSynthSpeaking = $state(false);
70
93
  let isMediaPlaying = $state(false);
71
94
  let localChoiceId = $state('');
72
95
  let autoPlayPromptOpen = $state(false);
73
96
  let autoplayAttempted = $state(false);
74
97
  const instanceId = `mc-populated-blank-${Math.random().toString(36).slice(2, 10)}`;
75
98
 
76
- const isEvaluateMode = $derived(props?.model?.mode === 'evaluate');
99
+ const isEvaluateMode = $derived(props?.model?.env?.mode === 'evaluate');
77
100
  const correctness = $derived(props?.model?.correctness);
78
101
  const isCorrect = $derived(correctness === 'correct');
79
102
  const isIncorrect = $derived(correctness === 'incorrect');
@@ -92,17 +115,83 @@ const choiceLayout = $derived(
92
115
  props?.model?.choiceLayout || (isAudioOnlyMode || isBlankOnlyTemplate ? 'horizontal' : 'vertical')
93
116
  );
94
117
  const isHorizontalChoices = $derived(choiceLayout === 'horizontal');
95
- const isVicFamilyLayout = $derived(layoutProfile === 'inline_sentence' && !isHorizontalChoices);
96
- const isSelVicFamilyLayout = $derived(isVicFamilyLayout && !!props?.model?.hasAudio);
97
118
  const showVisibleTranscript = $derived(!!props?.model?.showVisibleTranscript);
98
119
  const hasPlayableAudio = $derived(!!props?.model?.hasAudio && !!props?.model?.audioUrl);
99
- const hasTranscriptOnlyAudio = $derived(
100
- !!props?.model?.hasAudio && !props?.model?.audioUrl && !!props?.model?.audioTranscript
101
- );
120
+ const hasAudioButMissingResource = $derived(!!props?.model?.hasAudio && !props?.model?.audioUrl);
102
121
  const hasInlineSentenceAudioLayout = $derived(
103
122
  layoutProfile === 'inline_sentence' && !!props?.model?.hasAudio
104
123
  );
124
+ const uiText = $derived.by(() => ({
125
+ ...DEFAULT_UI_TEXT,
126
+ ...(props?.model?.uiText || {}),
127
+ }));
128
+ const profilePresetLimits = $derived.by(() => {
129
+ const profile = String(layoutProfile || '');
130
+ const defaults = DEFAULT_LAYOUT_PROFILE_PRESETS[profile] || {};
131
+ const customPresets =
132
+ props?.model?.layoutProfilePresets && typeof props.model.layoutProfilePresets === 'object'
133
+ ? props.model.layoutProfilePresets
134
+ : {};
135
+ const custom =
136
+ customPresets[profile] && typeof customPresets[profile] === 'object'
137
+ ? customPresets[profile]
138
+ : {};
139
+ return {
140
+ ...defaults,
141
+ ...custom,
142
+ };
143
+ });
144
+ const layoutLimits = $derived.by(() => {
145
+ const configured =
146
+ props?.model?.layoutLimits && typeof props.model.layoutLimits === 'object'
147
+ ? props.model.layoutLimits
148
+ : {};
149
+ return {
150
+ ...DEFAULT_LAYOUT_LIMITS,
151
+ ...profilePresetLimits,
152
+ ...configured,
153
+ };
154
+ });
155
+ const rootStyle = $derived.by(() =>
156
+ [
157
+ `--mpb-listen-button-size:${layoutLimits.listenButtonSizePx}px`,
158
+ `--mpb-blank-standalone-width:${layoutLimits.blankStandaloneWidthRem}rem`,
159
+ `--mpb-blank-wide-width:${layoutLimits.blankWideWidthRem}rem`,
160
+ `--mpb-blank-underline-width:${layoutLimits.blankUnderlineWidthPx}px`,
161
+ `--mpb-blank-underline-wide-width:${layoutLimits.blankUnderlineWideWidthPx}px`,
162
+ `--mpb-selected-image-max-height:${layoutLimits.selectedImageMaxHeightRem}rem`,
163
+ `--mpb-choice-image-max-height:${layoutLimits.choiceImageMaxHeightRem}rem`,
164
+ `--mpb-choice-width-px:${layoutLimits.horizontalChoiceWidthPx}px`,
165
+ `--mpb-choice-width-vw:${layoutLimits.horizontalChoiceWidthVw}vw`,
166
+ `--mpb-choice-tile-min-height:${layoutLimits.horizontalChoiceTileMinHeightRem}rem`,
167
+ `--mpb-choice-content-min-height:${layoutLimits.horizontalChoiceContentMinHeightRem}rem`,
168
+ `--mpb-stimulus-min-column:${layoutLimits.stimulusMinColumnPx}px`,
169
+ `--mpb-text-min-column:${layoutLimits.textMinColumnPx}px`,
170
+ `--mpb-choice-group-gap:${layoutLimits.choiceGroupGapRem}rem`,
171
+ `--mpb-choice-row-gap:${layoutLimits.choiceRowGapRem}rem`,
172
+ `--mpb-toggle-button-gap:${layoutLimits.toggleButtonGapRem}rem`,
173
+ `--mpb-horizontal-choice-radio-top-margin:${layoutLimits.horizontalChoiceRadioTopMarginRem}rem`,
174
+ `--mpb-audio-blank-template-margin-top:${layoutLimits.audioBlankTemplateMarginTopRem}rem`,
175
+ `--mpb-audio-blank-template-margin-bottom:${layoutLimits.audioBlankTemplateMarginBottomRem}rem`,
176
+ `--mpb-stimulus-grid-column-gap:${layoutLimits.stimulusGridColumnGapRem}rem`,
177
+ `--mpb-stimulus-grid-row-gap:${layoutLimits.stimulusGridRowGapRem}rem`,
178
+ `--mpb-stimulus-sentence-margin-top:${layoutLimits.stimulusSentenceMarginTopRem}rem`,
179
+ `--mpb-stimulus-choices-margin-top:${layoutLimits.stimulusChoicesMarginTopRem}rem`,
180
+ `--mpb-token-grid-column-gap:${layoutLimits.tokenGridColumnGapRem}rem`,
181
+ `--mpb-token-grid-row-gap:${layoutLimits.tokenGridRowGapRem}rem`,
182
+ `--mpb-token-template-margin-top:${layoutLimits.tokenTemplateMarginTopRem}rem`,
183
+ `--mpb-token-inline-token-gap:${layoutLimits.tokenInlineTokenGapRem}rem`,
184
+ `--mpb-token-choices-margin-top:${layoutLimits.tokenChoicesMarginTopRem}rem`,
185
+ `--mpb-inline-grid-column-gap:${layoutLimits.inlineGridColumnGapRem}rem`,
186
+ `--mpb-inline-grid-row-gap:${layoutLimits.inlineGridRowGapRem}rem`,
187
+ `--mpb-inline-template-margin-top:${layoutLimits.inlineTemplateMarginTopRem}rem`,
188
+ `--mpb-inline-choices-margin-top:${layoutLimits.inlineChoicesMarginTopRem}rem`,
189
+ ].join(';')
190
+ );
105
191
  const useFeatureButtonAudio = $derived.by(() => {
192
+ if (typeof props?.model?.useFeatureButtonAudio === 'boolean') {
193
+ return !!props.model.useFeatureButtonAudio;
194
+ }
106
195
  const profile = layoutProfile || '';
107
196
  return (
108
197
  !!props?.model?.hasAudio &&
@@ -123,73 +212,23 @@ const templateParts = $derived.by(() => {
123
212
  after: t.slice(idx + BLANK_TOKEN.length),
124
213
  };
125
214
  });
126
-
127
- function plainTextFromHtml(value = ''): string {
128
- return value
129
- .replace(/<[^>]+>/g, ' ')
130
- .replace(/&nbsp;/g, ' ')
131
- .replace(/\s+/g, ' ')
132
- .trim();
133
- }
134
-
135
215
  const blankWidth = $derived.by(() => {
136
216
  if (layoutProfile === 'audio_blank_only' || layoutProfile === 'stimulus_image_blank') {
137
- return '10rem';
217
+ return `${layoutLimits.blankWideWidthRem}rem`;
138
218
  }
139
219
  if (isBlankOnlyTemplate) {
140
- return '7rem';
141
- }
142
- if (choiceMode === 'text') {
143
- const modelChoices = Array.isArray(props?.model?.choices) ? props.model.choices : [];
144
- const longestChoiceChars = modelChoices.reduce((max: number, choice: McChoice) => {
145
- const textLength = plainTextFromHtml(choice?.labelHtml || '').length;
146
- return Math.max(max, textLength);
147
- }, 0);
148
- if (longestChoiceChars > 0) {
149
- const widthInCh = Math.min(24, Math.max(7, longestChoiceChars + 1));
150
- return `${widthInCh}ch`;
151
- }
220
+ return `${layoutLimits.blankStandaloneWidthRem}rem`;
152
221
  }
153
222
  return 'auto';
154
223
  });
155
224
  const blankBorderWidth = $derived.by(() => {
156
225
  if (layoutProfile === 'audio_blank_only' || layoutProfile === 'stimulus_image_blank') {
157
- return '4px';
226
+ return `${layoutLimits.blankUnderlineWideWidthPx}px`;
158
227
  }
159
- return '2px';
228
+ return `${layoutLimits.blankUnderlineWidthPx}px`;
160
229
  });
161
230
 
162
231
  const choices = $derived(Array.isArray(props?.model?.choices) ? props.model.choices : []);
163
- const shouldShuffleChoices = $derived.by(() => {
164
- const modelShuffle = !!props?.model?.shuffle;
165
- const optionsShuffle = !!props?.options?.shuffle;
166
- const lockChoiceOrder = !!props?.model?.lockChoiceOrder || !!props?.options?.lockChoiceOrder;
167
- return (modelShuffle || optionsShuffle) && !lockChoiceOrder;
168
- });
169
- const shuffleSeed = $derived.by(
170
- () =>
171
- `${props?.session?.id || props?.model?.id || instanceId}:${choices.map((c: McChoice) => c.id).join('|')}`
172
- );
173
- function hashString(value: string): number {
174
- let hash = 2166136261;
175
- for (let i = 0; i < value.length; i++) {
176
- hash ^= value.charCodeAt(i);
177
- hash = Math.imul(hash, 16777619);
178
- }
179
- return hash >>> 0;
180
- }
181
- function deterministicShuffle<T extends { id?: string }>(items: T[], seed: string): T[] {
182
- return [...items]
183
- .map((item, index) => ({
184
- item,
185
- key: hashString(`${seed}:${item?.id || index}:${index}`),
186
- }))
187
- .sort((a, b) => a.key - b.key)
188
- .map((entry) => entry.item);
189
- }
190
- const renderedChoices = $derived.by(() =>
191
- shouldShuffleChoices ? deterministicShuffle(choices, shuffleSeed) : choices
192
- );
193
232
  const choiceMode = $derived(props?.model?.choiceMode || 'text');
194
233
  const selectedId = $derived(props?.session?.choiceId || localChoiceId || '');
195
234
  const radioGroupName = $derived(`${instanceId}-choice-group-${props?.model?.id || '1'}`);
@@ -203,13 +242,20 @@ const displayChoiceId = $derived.by(() => {
203
242
  return selectedId;
204
243
  });
205
244
 
206
- const displayChoice = $derived.by(() =>
207
- renderedChoices.find((c: McChoice) => c.id === displayChoiceId)
208
- );
245
+ const displayChoice = $derived.by(() => choices.find((c: any) => c.id === displayChoiceId));
209
246
 
210
247
  const legendText = $derived.by(() => {
211
- const plain = plainTextFromHtml(props?.model?.prompt || '');
212
- return plain.length > 120 ? `${plain.slice(0, 117)}…` : plain || t('answerChoices');
248
+ const plain = (props?.model?.prompt || '')
249
+ .replace(/<[^>]+>/g, ' ')
250
+ .replace(/\s+/g, ' ')
251
+ .trim();
252
+ const maxChars = Math.max(
253
+ 8,
254
+ Number(layoutLimits.legendMaxChars) || DEFAULT_LAYOUT_LIMITS.legendMaxChars
255
+ );
256
+ return plain.length > maxChars
257
+ ? `${plain.slice(0, Math.max(1, maxChars - 1))}…`
258
+ : plain || uiText.answerChoices;
213
259
  });
214
260
 
215
261
  const promptId = $derived(`${instanceId}-prompt`);
@@ -232,47 +278,32 @@ const lang = $derived.by(() => {
232
278
  const locale = props?.model?.locale || '';
233
279
  return locale ? locale.slice(0, 2) : 'en';
234
280
  });
235
-
236
- const uiStrings = {
237
- en: {
238
- answerChoices: 'Answer choices',
239
- correctSelected: 'Correct answer selected',
240
- incorrectSelected: 'Incorrect answer selected',
241
- selectedAnswerInSentence: 'Selected answer in sentence',
242
- selectedAnswerImage: 'Selected answer image',
243
- showCorrect: 'Show correct answer',
244
- hideCorrect: 'Hide correct answer',
245
- enableAutoplay: 'Click to enable audio autoplay',
246
- listen: 'Listen',
247
- teacherInstructions: 'Teacher instructions',
248
- },
249
- es: {
250
- answerChoices: 'Opciones de respuesta',
251
- correctSelected: 'Respuesta correcta seleccionada',
252
- incorrectSelected: 'Respuesta incorrecta seleccionada',
253
- selectedAnswerInSentence: 'Respuesta seleccionada en la oración',
254
- selectedAnswerImage: 'Imagen de la respuesta seleccionada',
255
- showCorrect: 'Mostrar respuesta correcta',
256
- hideCorrect: 'Ocultar respuesta correcta',
257
- enableAutoplay: 'Haga clic para habilitar la reproducción automática de audio',
258
- listen: 'Escuchar',
259
- teacherInstructions: 'Instrucciones para el docente',
260
- },
261
- } as const;
262
- type UiKey = keyof (typeof uiStrings)['en'];
263
- function t(key: UiKey): string {
264
- return (lang === 'es' ? uiStrings.es[key] : uiStrings.en[key]) || uiStrings.en[key];
265
- }
281
+ const audioErrorMessage = $derived.by(() =>
282
+ hasAudioButMissingResource ? uiText.audioResourceUnavailable : ''
283
+ );
266
284
 
267
285
  const resultText = $derived.by(() => {
268
286
  if (!isEvaluateMode || showCorrectAnswer) return '';
269
- if (isCorrect) return t('correctSelected');
270
- if (isIncorrect && selectedId) return t('incorrectSelected');
287
+ if (isCorrect) return 'Correct answer selected';
288
+ if (isIncorrect && selectedId) return 'Incorrect answer selected';
271
289
  return '';
272
290
  });
273
291
 
274
- function emitSession(updatedSession: McSession) {
275
- props?.onSessionChange?.(updatedSession);
292
+ function emitSession(updatedSession: any, sourceEl?: HTMLElement | null) {
293
+ const host =
294
+ (sourceEl?.closest?.('mc-populated-blank-element') as any) ||
295
+ (document.querySelector('mc-populated-blank-element') as any);
296
+ if (host?.onSessionChange) {
297
+ host.onSessionChange(updatedSession);
298
+ return;
299
+ }
300
+ host?.dispatchEvent(
301
+ new CustomEvent('session-changed', {
302
+ bubbles: true,
303
+ composed: true,
304
+ detail: { complete: !!updatedSession?.choiceId, component: 'mc-populated-blank' },
305
+ })
306
+ );
276
307
  }
277
308
 
278
309
  function onRadioChange(e: Event) {
@@ -286,7 +317,7 @@ function onRadioChange(e: Event) {
286
317
  element: 'mc-populated-blank',
287
318
  choiceId,
288
319
  };
289
- emitSession(updatedSession);
320
+ emitSession(updatedSession, input);
290
321
  }
291
322
 
292
323
  function toggleCorrectAnswer() {
@@ -320,45 +351,42 @@ function handleEnableAutoplayClick() {
320
351
  audioEl.play().finally(() => {
321
352
  autoPlayPromptOpen = false;
322
353
  });
323
- return;
324
- }
325
- if (hasTranscriptOnlyAudio) {
326
- speakTranscript();
327
- autoPlayPromptOpen = false;
328
354
  }
329
355
  }
330
356
 
331
- function onAudioPlaying() {
357
+ function onAudioPlaying(e: Event) {
358
+ const host = (e.currentTarget as HTMLElement)?.closest('mc-populated-blank-element') as any;
332
359
  isMediaPlaying = true;
333
- props?.onAudioStarted?.();
360
+ host?.onAudioStarted?.();
334
361
  autoPlayPromptOpen = false;
335
362
  }
336
363
 
337
- function onAudioEnded() {
364
+ function onAudioEnded(e: Event) {
365
+ const host = (e.currentTarget as HTMLElement)?.closest('mc-populated-blank-element') as any;
338
366
  isMediaPlaying = false;
339
- props?.onAudioEnded?.();
367
+ host?.onAudioEnded?.();
340
368
  }
341
369
 
342
- function speechButtonLabel() {
343
- return t('listen');
370
+ function speechButtonLabel(locale = '') {
371
+ return locale.toLowerCase().startsWith('es') ? uiText.listenLabelEs : uiText.listenLabelEn;
344
372
  }
345
373
 
346
374
  const featureAudioSkin = $derived.by(() => {
347
375
  const locale = String(props?.model?.locale || '').toLowerCase();
348
- if (locale.startsWith('es')) {
349
- return {
350
- silentUrl:
351
- 'https://assets.learnosity.com/organisations/844/27a9d5b5-d873-4bd5-b9ba-22748782d8ba.svg',
352
- playingUrl:
353
- 'https://assets.learnosity.com/organisations/844/120f216d-96b7-4560-94b8-1d90710216b7.svg',
354
- };
355
- }
356
- return {
357
- silentUrl:
358
- 'https://assets.learnosity.com/organisations/844/0c9f2aa3-3cd5-4de7-93ef-541c24ca35da.svg',
359
- playingUrl:
360
- 'https://assets.learnosity.com/organisations/844/231dfdc2-c113-4be5-91fb-e75a0ca5994b.svg',
361
- };
376
+ const lang = locale.slice(0, 2);
377
+ const byLocale =
378
+ props?.model?.audioButtonSkinsByLocale &&
379
+ typeof props.model.audioButtonSkinsByLocale === 'object'
380
+ ? props.model.audioButtonSkinsByLocale
381
+ : {};
382
+ const customSingle =
383
+ props?.model?.audioButtonSkin && typeof props.model.audioButtonSkin === 'object'
384
+ ? props.model.audioButtonSkin
385
+ : null;
386
+ const defaultSkin = locale.startsWith('es')
387
+ ? DEFAULT_AUDIO_BUTTON_SKINS.es
388
+ : DEFAULT_AUDIO_BUTTON_SKINS.default;
389
+ return byLocale[locale] || byLocale[lang] || byLocale.default || customSingle || defaultSkin;
362
390
  });
363
391
 
364
392
  function playFeatureAudio() {
@@ -366,45 +394,9 @@ function playFeatureAudio() {
366
394
  audioEl.play().catch(() => {
367
395
  // Keep behavior resilient: if playback is blocked, user can retry.
368
396
  });
369
- return;
370
397
  }
371
- speakTranscript();
372
398
  }
373
399
 
374
- function speakTranscript() {
375
- const transcript = (props?.model?.audioTranscript || '').trim();
376
- if (!transcript || !window?.speechSynthesis) return;
377
-
378
- window.speechSynthesis.cancel();
379
- const utterance = new SpeechSynthesisUtterance(transcript);
380
- utterance.lang = props?.model?.locale || 'en-US';
381
-
382
- utterance.onstart = () => {
383
- isSynthSpeaking = true;
384
- props?.onAudioStarted?.();
385
- };
386
-
387
- utterance.onend = () => {
388
- isSynthSpeaking = false;
389
- activeUtterance = null;
390
- props?.onAudioEnded?.();
391
- };
392
-
393
- utterance.onerror = () => {
394
- isSynthSpeaking = false;
395
- activeUtterance = null;
396
- };
397
-
398
- activeUtterance = utterance;
399
- window.speechSynthesis.speak(utterance);
400
- }
401
-
402
- onDestroy(() => {
403
- if (activeUtterance && window?.speechSynthesis) {
404
- window.speechSynthesis.cancel();
405
- }
406
- });
407
-
408
400
  $effect(() => {
409
401
  if (props?.session?.choiceId) {
410
402
  localChoiceId = props.session.choiceId;
@@ -421,50 +413,46 @@ $effect(() => {
421
413
  audioEl.play().catch(() => {
422
414
  autoPlayPromptOpen = true;
423
415
  });
424
- return;
425
- }
426
-
427
- if (props?.model?.audioTranscript) {
428
- autoPlayPromptOpen = true;
429
416
  }
430
417
  });
431
418
  </script>
432
419
 
433
420
  <div
434
- class={`p-4 mc-populated-blank-root layout-${layoutProfile} ${hasInlineSentenceAudioLayout ? 'has-inline-audio' : ''} ${isVicFamilyLayout ? 'vic-family-layout' : ''} ${isSelVicFamilyLayout ? 'sel-vic-family-layout' : ''}`}
421
+ class={`p-4 mc-populated-blank-root pie-element pie-element-mc-populated-blank pie-delivery-root layout-${layoutProfile} ${hasInlineSentenceAudioLayout ? 'has-inline-audio' : ''}`}
435
422
  lang={lang}
423
+ style={rootStyle}
436
424
  >
437
425
  {#if props?.model?.prompt}
438
- <div class="mb-4 prose" id={promptId}>{@html props.model.prompt}</div>
426
+ <div class="mb-4 prose pie-prompt" id={promptId}>{@html props.model.prompt}</div>
439
427
  {/if}
440
428
 
441
429
  {#if props?.model?.hasAudio}
442
- <div class="mb-4 audio-container">
430
+ <div class="mb-4 audio-container pie-audio-container">
443
431
  {#if hasPlayableAudio && useFeatureButtonAudio}
444
432
  <audio
445
433
  bind:this={audioEl}
446
- class="sr-only"
434
+ class="sr-only pie-audio-player"
447
435
  preload="metadata"
448
436
  src={props.model.audioUrl}
449
437
  aria-hidden="true"
450
438
  tabindex="-1"
451
- onplaying={onAudioPlaying}
452
- onended={onAudioEnded}
439
+ on:playing={onAudioPlaying}
440
+ on:ended={onAudioEnded}
453
441
  ></audio>
454
442
  <button
455
- class="listen-button rli-feature-audio"
443
+ class="listen-button pie-listen-button rli-feature-audio"
456
444
  type="button"
457
- aria-label={speechButtonLabel()}
458
- onclick={playFeatureAudio}
445
+ aria-label={speechButtonLabel(props?.model?.locale)}
446
+ on:click={playFeatureAudio}
459
447
  >
460
448
  <img
461
- class={`listen-feature-icon rli-feature-listen ${isMediaPlaying || isSynthSpeaking ? '' : 'listen-active'}`}
449
+ class={`listen-feature-icon pie-listen-icon rli-feature-listen ${isMediaPlaying ? '' : 'listen-active'}`}
462
450
  src={featureAudioSkin.silentUrl}
463
451
  alt=""
464
452
  aria-hidden="true"
465
453
  />
466
454
  <img
467
- class={`listen-feature-icon rli-feature-listen ${isMediaPlaying || isSynthSpeaking ? 'listen-active' : ''}`}
455
+ class={`listen-feature-icon pie-listen-icon rli-feature-listen ${isMediaPlaying ? 'listen-active' : ''}`}
468
456
  src={featureAudioSkin.playingUrl}
469
457
  alt=""
470
458
  aria-hidden="true"
@@ -474,84 +462,64 @@ $effect(() => {
474
462
  <audio
475
463
  bind:this={audioEl}
476
464
  controls
477
- class="w-full max-w-md"
465
+ class="w-full max-w-md pie-audio-player"
478
466
  preload="metadata"
479
467
  src={props.model.audioUrl}
480
468
  aria-describedby={props?.model?.audioTranscript ? transcriptId : undefined}
481
- onplaying={onAudioPlaying}
482
- onended={onAudioEnded}
469
+ on:playing={onAudioPlaying}
470
+ on:ended={onAudioEnded}
483
471
  >
484
472
  <track kind="captions" />
485
473
  </audio>
486
474
  {#if autoPlayPromptOpen}
487
- <button class="mt-2 text-sm underline" type="button" onclick={handleEnableAutoplayClick}>
488
- {t('enableAutoplay')}
475
+ <button
476
+ class="mt-2 text-sm underline pie-audio-autoplay-enable"
477
+ type="button"
478
+ on:click={handleEnableAutoplayClick}
479
+ >
480
+ {uiText.clickToEnableAutoplay}
489
481
  </button>
490
482
  {/if}
491
- {:else if hasTranscriptOnlyAudio}
492
- <button
493
- class="listen-button rli-feature-audio"
494
- type="button"
495
- aria-label={speechButtonLabel()}
496
- onclick={playFeatureAudio}
497
- >
498
- <img
499
- class={`listen-feature-icon rli-feature-listen ${isMediaPlaying || isSynthSpeaking ? '' : 'listen-active'}`}
500
- src={featureAudioSkin.silentUrl}
501
- alt=""
502
- aria-hidden="true"
503
- />
504
- <img
505
- class={`listen-feature-icon rli-feature-listen ${isMediaPlaying || isSynthSpeaking ? 'listen-active' : ''}`}
506
- src={featureAudioSkin.playingUrl}
507
- alt=""
508
- aria-hidden="true"
509
- />
510
- </button>
483
+ {:else if hasAudioButMissingResource}
484
+ <p class="text-sm text-red-700 pie-audio-error" role="alert">{audioErrorMessage}</p>
511
485
  {/if}
512
486
  {#if props?.model?.audioTranscript}
513
487
  <p
514
- class={`text-sm mt-2 text-gray-700 ${showVisibleTranscript ? '' : 'sr-only'}`}
488
+ class={`text-sm mt-2 text-gray-700 pie-audio-transcript ${showVisibleTranscript ? '' : 'sr-only'}`}
515
489
  id={transcriptId}
516
490
  >
517
- {props.model.audioTranscript}
491
+ <strong>{uiText.transcriptLabel}:</strong> {props.model.audioTranscript}
518
492
  </p>
519
493
  {/if}
520
494
  </div>
521
495
  {/if}
522
496
 
523
- {#if props?.model?.teacherInstructions}
524
- <div class="mb-4 teacher-instructions">
525
- <h4 class="teacher-instructions-title">{t('teacherInstructions')}</h4>
526
- <div class="prose teacher-instructions-content">{@html props.model.teacherInstructions}</div>
527
- </div>
528
- {/if}
529
-
530
497
  {#if props?.model?.sentenceHtml}
531
- <div class="mb-3 prose prose-p:my-1 sentence-line" aria-describedby={templateDescribedBy}>
498
+ <div class="mb-3 prose prose-p:my-1 sentence-line pie-sentence-line" aria-describedby={templateDescribedBy}>
532
499
  {@html props.model.sentenceHtml}
533
500
  </div>
534
501
  {/if}
535
502
 
536
503
  {#if !isAudioOnlyMode}
537
- <div class="mb-4 template-line" aria-describedby={templateDescribedBy}>
504
+ <div class="mb-4 template-line pie-template-line" aria-describedby={templateDescribedBy}>
538
505
  {@html templateParts.before}
539
506
  <span
540
- class={`inline-flex items-center min-h-[1.5em] px-2 mx-1 border-b-2 border-gray-500 align-baseline blank-slot ${isBlankOnlyTemplate ? 'blank-slot-standalone' : ''}`}
507
+ class={`inline-flex items-center min-h-[1.5em] px-2 mx-1 border-b-2 border-gray-500 align-baseline blank-slot pie-blank-slot ${isBlankOnlyTemplate ? 'blank-slot-standalone pie-blank-slot-standalone' : ''}`}
541
508
  style={`width:${blankWidth};border-bottom-width:${blankBorderWidth};`}
542
509
  role="status"
543
510
  aria-live="polite"
544
511
  aria-atomic="true"
545
- aria-label={t('selectedAnswerInSentence')}
512
+ aria-label={uiText.selectedAnswerInSentence}
546
513
  >
547
514
  {#if choiceMode === 'image' && displayChoice?.imageUrl}
548
515
  <img
549
516
  src={displayChoice.imageUrl}
550
- alt={displayChoice.imageAlt || t('selectedAnswerImage')}
551
- class="max-h-16 w-auto object-contain"
517
+ alt={displayChoice.imageAlt || 'Selected answer image'}
518
+ class="w-auto object-contain pie-blank-image"
519
+ style="max-height:var(--mpb-selected-image-max-height, 4rem);"
552
520
  />
553
521
  {:else if displayChoice?.labelHtml}
554
- <span class="blank-inner choice-label">{@html displayChoice.labelHtml}</span>
522
+ <span class="blank-inner choice-label pie-blank-value">{@html displayChoice.labelHtml}</span>
555
523
  {:else}
556
524
  <span class="blank-inner-empty" aria-hidden="true">&nbsp;</span>
557
525
  {/if}
@@ -563,44 +531,51 @@ $effect(() => {
563
531
  {#if isEvaluateMode && isIncorrect}
564
532
  <button
565
533
  type="button"
566
- class="mb-3 flex items-center gap-2 cursor-pointer select-none"
567
- onclick={toggleCorrectAnswer}
534
+ class="mb-3 flex items-center cursor-pointer select-none pie-toggle-correct-answer"
535
+ style="gap:var(--mpb-toggle-button-gap, 0.5rem);"
536
+ on:click={toggleCorrectAnswer}
568
537
  aria-pressed={showCorrectAnswer}
569
538
  >
570
539
  <span class="text-sm hover:underline">
571
- {showCorrectAnswer ? t('hideCorrect') : t('showCorrect')}
540
+ {showCorrectAnswer ? uiText.hideCorrectAnswer : uiText.showCorrectAnswer}
572
541
  </span>
573
542
  </button>
574
543
  {/if}
575
544
  {#if resultText}
576
- <p id={resultId} class="sr-only" role="status" aria-live="polite">{resultText}</p>
545
+ <p id={resultId} class="sr-only pie-result-feedback" role="status" aria-live="polite">{resultText}</p>
577
546
  {/if}
578
547
 
579
- <fieldset class="border-0 p-0 m-0" disabled={props?.model?.disabled}>
580
- <legend class="sr-only" id={legendId}>{legendText}</legend>
548
+ <fieldset class="border-0 p-0 m-0 pie-choices-fieldset" disabled={props?.model?.disabled}>
549
+ <legend class="sr-only pie-choices-legend" id={legendId}>{legendText}</legend>
581
550
  <div
582
- class={`gap-2 ${isHorizontalChoices ? 'flex flex-row flex-wrap items-start justify-center' : 'flex flex-col'}`}
551
+ class={`pie-choices ${isHorizontalChoices ? 'flex flex-row flex-wrap items-start justify-center' : 'flex flex-col'}`}
552
+ style="gap:var(--mpb-choice-group-gap, 0.5rem);"
583
553
  role="radiogroup"
584
554
  tabindex="-1"
585
555
  aria-labelledby={choicesGroupLabelledBy}
586
556
  aria-describedby={resultText ? resultId : undefined}
587
- onkeydown={onRadioGroupKeydown}
557
+ on:keydown={onRadioGroupKeydown}
588
558
  >
589
- {#each renderedChoices as c (c.id)}
559
+ {#each choices as c (c.id)}
590
560
  <div
591
- class={`flex items-start gap-2 choice-row ${isHorizontalChoices ? 'choice-row-horizontal' : ''} ${((showCorrectAnswer && isEvaluateMode ? props?.model?.correctChoiceId : selectedId) === c.id) ? 'is-selected' : ''}`}
561
+ class={`flex items-start choice-row pie-choice ${isHorizontalChoices ? 'choice-row-horizontal pie-choice-horizontal' : ''} ${((showCorrectAnswer && isEvaluateMode ? props?.model?.correctChoiceId : selectedId) === c.id) ? 'is-selected pie-choice-selected' : ''}`}
562
+ style="gap:var(--mpb-choice-row-gap, 0.5rem);"
592
563
  >
593
564
  {#if isHorizontalChoices}
594
- <label for={`${instanceId}-opt-${c.id}`} class="cursor-pointer choice-tile text-center">
595
- <span class="choice-tile-content">
565
+ <label
566
+ for={`${instanceId}-opt-${c.id}`}
567
+ class="cursor-pointer choice-tile text-center pie-choice-tile"
568
+ >
569
+ <span class="choice-tile-content pie-choice-tile-content">
596
570
  {#if choiceMode === 'image' && c.imageUrl}
597
571
  <img
598
572
  src={c.imageUrl}
599
573
  alt={c.imageAlt || `Choice ${c.id}`}
600
- class="max-h-20 object-contain mx-auto"
574
+ class="object-contain mx-auto pie-choice-image"
575
+ style="max-height:var(--mpb-choice-image-max-height, 5rem);"
601
576
  />
602
577
  {:else}
603
- <span class="choice-label">{@html c.labelHtml || ''}</span>
578
+ <span class="choice-label pie-choice-label">{@html c.labelHtml || ''}</span>
604
579
  {/if}
605
580
  </span>
606
581
  <input
@@ -612,8 +587,8 @@ $effect(() => {
612
587
  (showCorrectAnswer && isEvaluateMode ? props?.model?.correctChoiceId : selectedId) === c.id
613
588
  }
614
589
  disabled={props?.model?.disabled}
615
- onchange={onRadioChange}
616
- class="choice-radio-bottom"
590
+ on:change={onRadioChange}
591
+ class="choice-radio-bottom pie-choice-radio pie-choice-radio-bottom"
617
592
  />
618
593
  </label>
619
594
  {:else}
@@ -626,22 +601,31 @@ $effect(() => {
626
601
  (showCorrectAnswer && isEvaluateMode ? props?.model?.correctChoiceId : selectedId) === c.id
627
602
  }
628
603
  disabled={props?.model?.disabled}
629
- onchange={onRadioChange}
630
- class="mt-1"
604
+ on:change={onRadioChange}
605
+ class="choice-radio-inline pie-choice-radio pie-choice-radio-inline"
631
606
  />
632
- <label for={`${instanceId}-opt-${c.id}`} class="cursor-pointer flex-1">
607
+ <label for={`${instanceId}-opt-${c.id}`} class="cursor-pointer flex-1 pie-choice-label-wrap">
633
608
  {#if choiceMode === 'image' && c.imageUrl}
634
- <img src={c.imageUrl} alt={c.imageAlt || `Choice ${c.id}`} class="max-h-20 object-contain" />
609
+ <img
610
+ src={c.imageUrl}
611
+ alt={c.imageAlt || `Choice ${c.id}`}
612
+ class="object-contain pie-choice-image"
613
+ style="max-height:var(--mpb-choice-image-max-height, 5rem);"
614
+ />
635
615
  {:else}
636
- <span class="choice-label">{@html c.labelHtml || ''}</span>
616
+ <span class="choice-label pie-choice-label">{@html c.labelHtml || ''}</span>
637
617
  {/if}
638
618
  </label>
639
619
  {/if}
640
620
  {#if isEvaluateMode && !showCorrectAnswer}
641
621
  {#if selectedId === c.id && isCorrect}
642
- <span class="text-green-600 text-sm font-medium" aria-hidden="true">✓</span>
622
+ <span class="text-green-600 text-sm font-medium pie-choice-feedback-correct" aria-hidden="true">
623
+ ✓
624
+ </span>
643
625
  {:else if selectedId === c.id && isIncorrect}
644
- <span class="text-red-600 text-sm font-medium" aria-hidden="true">✗</span>
626
+ <span class="text-red-600 text-sm font-medium pie-choice-feedback-incorrect" aria-hidden="true">
627
+ ✗
628
+ </span>
645
629
  {/if}
646
630
  {/if}
647
631
  </div>
@@ -694,12 +678,12 @@ $effect(() => {
694
678
  }
695
679
 
696
680
  .blank-slot-standalone {
697
- width: 7rem;
681
+ width: var(--mpb-blank-standalone-width, 7rem);
698
682
  }
699
683
 
700
684
  .listen-button {
701
- width: 128px;
702
- height: 128px;
685
+ width: var(--mpb-listen-button-size, 128px);
686
+ height: var(--mpb-listen-button-size, 128px);
703
687
  border: 0;
704
688
  padding: 0;
705
689
  background-color: transparent;
@@ -712,8 +696,8 @@ $effect(() => {
712
696
  }
713
697
 
714
698
  .listen-feature-icon {
715
- width: 128px;
716
- height: 128px;
699
+ width: var(--mpb-listen-button-size, 128px);
700
+ height: var(--mpb-listen-button-size, 128px);
717
701
  object-fit: contain;
718
702
  display: none;
719
703
  }
@@ -725,7 +709,7 @@ $effect(() => {
725
709
  .choice-row-horizontal {
726
710
  flex-direction: column;
727
711
  align-items: center;
728
- width: min(170px, 30vw);
712
+ width: min(var(--mpb-choice-width-px, 170px), var(--mpb-choice-width-vw, 30vw));
729
713
  gap: 0;
730
714
  }
731
715
 
@@ -735,7 +719,7 @@ $effect(() => {
735
719
  justify-content: space-between;
736
720
  align-items: center;
737
721
  width: 100%;
738
- min-height: 11rem;
722
+ min-height: var(--mpb-choice-tile-min-height, 11rem);
739
723
  padding: 0.8rem 0.65rem 0.5rem;
740
724
  border-radius: 8px;
741
725
  background: transparent;
@@ -747,88 +731,19 @@ $effect(() => {
747
731
  display: flex;
748
732
  align-items: center;
749
733
  justify-content: center;
750
- min-height: 7.5rem;
734
+ min-height: var(--mpb-choice-content-min-height, 7.5rem);
751
735
  }
752
736
 
753
737
  .choice-row-horizontal:hover .choice-tile {
754
- background: #f2f2f2;
738
+ background: #ececec;
755
739
  }
756
740
 
757
741
  .choice-row-horizontal.is-selected .choice-tile {
758
- background: #fcfcd3;
742
+ background: #eceabf;
759
743
  }
760
744
 
761
745
  .choice-row-horizontal.is-selected:hover .choice-tile {
762
- background: #fcfcd3;
763
- }
764
-
765
- .choice-row:not(.choice-row-horizontal):hover {
766
- background: #f2f2f2;
767
- border-radius: 6px;
768
- }
769
-
770
- .choice-row.is-selected:not(.choice-row-horizontal) {
771
- background: #fcfcd3;
772
- border-radius: 6px;
773
- }
774
-
775
- :global(.access-contrast-3) .choice-row-horizontal,
776
- :global(.access-contrast-6) .choice-row-horizontal {
777
- border-bottom-width: 4px;
778
- border-bottom-style: solid;
779
- padding-bottom: 10px;
780
- }
781
-
782
- :global(.access-contrast-3) .choice-row:not(.choice-row-horizontal),
783
- :global(.access-contrast-6) .choice-row:not(.choice-row-horizontal) {
784
- border-left-width: 4px;
785
- border-left-style: solid;
786
- padding-left: 0.35rem;
787
- }
788
-
789
- :global(.access-contrast-3) .choice-row-horizontal,
790
- :global(.access-contrast-3) .choice-row:not(.choice-row-horizontal) {
791
- border-color: #2d4e84;
792
- }
793
-
794
- :global(.access-contrast-6) .choice-row-horizontal,
795
- :global(.access-contrast-6) .choice-row:not(.choice-row-horizontal) {
796
- border-color: #000;
797
- }
798
-
799
- :global(.access-contrast-3) .choice-row:hover,
800
- :global(.access-contrast-6) .choice-row:hover {
801
- border-radius: 0;
802
- }
803
-
804
- :global(.access-contrast-3) .choice-row:hover {
805
- border-color: #fff25d;
806
- color: #fff25d;
807
- }
808
-
809
- :global(.access-contrast-6) .choice-row:hover {
810
- border-color: #fff;
811
- color: #fff;
812
- }
813
-
814
- :global(.access-contrast-3) .choice-row:hover:not(.is-selected):not(.choice-row-horizontal),
815
- :global(.access-contrast-6) .choice-row:hover:not(.is-selected):not(.choice-row-horizontal) {
816
- background: inherit;
817
- }
818
-
819
- :global(.access-contrast-3) .choice-row-horizontal:hover:not(.is-selected) .choice-tile,
820
- :global(.access-contrast-6) .choice-row-horizontal:hover:not(.is-selected) .choice-tile {
821
- background: inherit;
822
- }
823
-
824
- :global(.access-contrast-3) .choice-row.is-selected:hover,
825
- :global(.access-contrast-6) .choice-row.is-selected:hover {
826
- color: #000;
827
- }
828
-
829
- :global(.access-contrast-3) .choice-row-horizontal.is-selected:hover .choice-tile,
830
- :global(.access-contrast-6) .choice-row-horizontal.is-selected:hover .choice-tile {
831
- background: #fcfcd3;
746
+ background: #eceabf;
832
747
  }
833
748
 
834
749
  .choice-row-horizontal :global(p) {
@@ -841,34 +756,6 @@ $effect(() => {
841
756
  margin: 0;
842
757
  }
843
758
 
844
- .teacher-instructions {
845
- border-left: 4px solid #60a5fa;
846
- background: #eff6ff;
847
- padding: 0.5rem 0.75rem;
848
- border-radius: 4px;
849
- }
850
-
851
- .teacher-instructions-title {
852
- margin: 0 0 0.3rem;
853
- font-size: 0.9rem;
854
- font-weight: 600;
855
- }
856
-
857
- .vic-family-layout {
858
- margin-left: auto;
859
- margin-right: auto;
860
- max-width: 800px;
861
- }
862
-
863
- .vic-family-layout .choice-row {
864
- user-select: none;
865
- margin-right: 10px;
866
- }
867
-
868
- .sel-vic-family-layout {
869
- font-size: 1.5em;
870
- }
871
-
872
759
  .blank-inner {
873
760
  width: 100%;
874
761
  display: inline-flex;
@@ -878,7 +765,11 @@ $effect(() => {
878
765
  }
879
766
 
880
767
  .choice-radio-bottom {
881
- margin-top: 0.5rem;
768
+ margin-top: var(--mpb-horizontal-choice-radio-top-margin, 0.5rem);
769
+ }
770
+
771
+ .choice-radio-inline {
772
+ margin-top: var(--mpb-horizontal-choice-radio-top-margin, 0.5rem);
882
773
  }
883
774
 
884
775
  .layout-audio_blank_only .audio-container,
@@ -890,24 +781,25 @@ $effect(() => {
890
781
  .layout-audio_blank_only .template-line,
891
782
  .layout-stimulus_image_blank .template-line {
892
783
  text-align: center;
893
- margin: 0.8rem 0 1.8rem;
784
+ margin: var(--mpb-audio-blank-template-margin-top, 0.8rem) 0
785
+ var(--mpb-audio-blank-template-margin-bottom, 1.8rem);
894
786
  }
895
787
 
896
788
  .layout-audio_blank_only .blank-slot,
897
789
  .layout-stimulus_image_blank .blank-slot {
898
- width: 10rem;
899
- border-bottom-width: 4px;
790
+ width: var(--mpb-blank-wide-width, 10rem);
791
+ border-bottom-width: var(--mpb-blank-underline-wide-width, 4px);
900
792
  }
901
793
 
902
794
  .layout-stimulus_image_blank {
903
795
  display: grid;
904
- grid-template-columns: minmax(210px, 1fr) auto;
796
+ grid-template-columns: minmax(var(--mpb-stimulus-min-column, 210px), 1fr) auto;
905
797
  grid-template-areas:
906
798
  'sentence audio'
907
799
  '. template'
908
800
  'choices choices';
909
- column-gap: 2rem;
910
- row-gap: 0.7rem;
801
+ column-gap: var(--mpb-stimulus-grid-column-gap, 2rem);
802
+ row-gap: var(--mpb-stimulus-grid-row-gap, 0.7rem);
911
803
  align-items: start;
912
804
  }
913
805
 
@@ -918,7 +810,7 @@ $effect(() => {
918
810
 
919
811
  .layout-stimulus_image_blank .sentence-line {
920
812
  grid-area: sentence;
921
- margin: 0.2rem 0 0;
813
+ margin: var(--mpb-stimulus-sentence-margin-top, 0.2rem) 0 0;
922
814
  }
923
815
 
924
816
  .layout-stimulus_image_blank .template-line {
@@ -930,17 +822,17 @@ $effect(() => {
930
822
 
931
823
  .layout-stimulus_image_blank fieldset {
932
824
  grid-area: choices;
933
- margin-top: 0.9rem;
825
+ margin-top: var(--mpb-stimulus-choices-margin-top, 0.9rem);
934
826
  }
935
827
 
936
828
  .layout-token_sequence {
937
829
  display: grid;
938
- grid-template-columns: minmax(260px, 1fr) auto;
830
+ grid-template-columns: minmax(var(--mpb-text-min-column, 260px), 1fr) auto;
939
831
  grid-template-areas:
940
832
  'template audio'
941
833
  'choices choices';
942
- column-gap: 1.5rem;
943
- row-gap: 0.8rem;
834
+ column-gap: var(--mpb-token-grid-column-gap, 1.5rem);
835
+ row-gap: var(--mpb-token-grid-row-gap, 0.8rem);
944
836
  align-items: start;
945
837
  }
946
838
 
@@ -952,34 +844,34 @@ $effect(() => {
952
844
 
953
845
  .layout-token_sequence .template-line {
954
846
  grid-area: template;
955
- margin: 0.6rem 0 0;
847
+ margin: var(--mpb-token-template-margin-top, 0.6rem) 0 0;
956
848
  justify-self: center;
957
849
  text-align: center;
958
850
  }
959
851
 
960
852
  .layout-token_sequence .template-line :global(span + span) {
961
- margin-left: 0.35rem;
853
+ margin-left: var(--mpb-token-inline-token-gap, 0.35rem);
962
854
  }
963
855
 
964
856
  .layout-token_sequence .blank-slot {
965
- width: 7rem;
966
- border-bottom-width: 4px;
967
- margin-left: 0.35rem;
857
+ width: var(--mpb-blank-standalone-width, 7rem);
858
+ border-bottom-width: var(--mpb-blank-underline-wide-width, 4px);
859
+ margin-left: var(--mpb-token-inline-token-gap, 0.35rem);
968
860
  }
969
861
 
970
862
  .layout-token_sequence fieldset {
971
863
  grid-area: choices;
972
- margin-top: 0.2rem;
864
+ margin-top: var(--mpb-token-choices-margin-top, 0.2rem);
973
865
  }
974
866
 
975
867
  .layout-inline_sentence.has-inline-audio {
976
868
  display: grid;
977
- grid-template-columns: minmax(260px, 1fr) auto;
869
+ grid-template-columns: minmax(var(--mpb-text-min-column, 260px), 1fr) auto;
978
870
  grid-template-areas:
979
871
  'template audio'
980
872
  'choices choices';
981
- column-gap: 1.5rem;
982
- row-gap: 0.65rem;
873
+ column-gap: var(--mpb-inline-grid-column-gap, 1.5rem);
874
+ row-gap: var(--mpb-inline-grid-row-gap, 0.65rem);
983
875
  align-items: start;
984
876
  }
985
877
 
@@ -991,13 +883,13 @@ $effect(() => {
991
883
 
992
884
  .layout-inline_sentence.has-inline-audio .template-line {
993
885
  grid-area: template;
994
- margin: 0.45rem 0 0;
886
+ margin: var(--mpb-inline-template-margin-top, 0.45rem) 0 0;
995
887
  text-align: center;
996
888
  justify-self: center;
997
889
  }
998
890
 
999
891
  .layout-inline_sentence.has-inline-audio fieldset {
1000
892
  grid-area: choices;
1001
- margin-top: 0.25rem;
893
+ margin-top: var(--mpb-inline-choices-margin-top, 0.25rem);
1002
894
  }
1003
895
  </style>