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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pie-element/mc-populated-blank",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Multiple-choice option populates an inline blank in a sentence template (Svelte 5 PIE element)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -30,9 +30,6 @@
30
30
  "development": "./src/print/index.ts",
31
31
  "types": "./dist/print/index.d.ts",
32
32
  "default": "./dist/print/index.js"
33
- },
34
- "./iife": {
35
- "default": "./dist/index.iife.js"
36
33
  }
37
34
  },
38
35
  "files": [
@@ -1,5 +1,43 @@
1
1
  /** Token authors must include exactly once in `template` (HTML). */
2
2
  export const BLANK_TOKEN = '{{blank}}';
3
+ /** Baseline values tuned to match historical CQT visuals; override via model.layoutLimits. */
4
+ export const DEFAULT_LAYOUT_LIMITS = {
5
+ blankStandaloneWidthRem: 7,
6
+ blankWideWidthRem: 10,
7
+ blankUnderlineWidthPx: 2,
8
+ blankUnderlineWideWidthPx: 4,
9
+ horizontalChoiceWidthPx: 170,
10
+ horizontalChoiceWidthVw: 30,
11
+ horizontalChoiceTileMinHeightRem: 11,
12
+ horizontalChoiceContentMinHeightRem: 7.5,
13
+ selectedImageMaxHeightRem: 4,
14
+ choiceImageMaxHeightRem: 5,
15
+ listenButtonSizePx: 128,
16
+ stimulusMinColumnPx: 210,
17
+ textMinColumnPx: 260,
18
+ legendMaxChars: 120,
19
+ choiceGroupGapRem: 0.5,
20
+ choiceRowGapRem: 0.5,
21
+ toggleButtonGapRem: 0.5,
22
+ horizontalChoiceRadioTopMarginRem: 0.5,
23
+ audioBlankTemplateMarginTopRem: 0.8,
24
+ audioBlankTemplateMarginBottomRem: 1.8,
25
+ audioInstructionsMaxWidthPx: 875,
26
+ narrowHorizontalChoiceMaxWidthPx: 230,
27
+ stimulusGridColumnGapRem: 2,
28
+ stimulusGridRowGapRem: 0.7,
29
+ stimulusSentenceMarginTopRem: 0.2,
30
+ stimulusChoicesMarginTopRem: 0.9,
31
+ tokenGridColumnGapRem: 1.5,
32
+ tokenGridRowGapRem: 0.8,
33
+ tokenTemplateMarginTopRem: 0.6,
34
+ tokenInlineTokenGapRem: 0.35,
35
+ tokenChoicesMarginTopRem: 0.2,
36
+ inlineGridColumnGapRem: 1.5,
37
+ inlineGridRowGapRem: 0.65,
38
+ inlineTemplateMarginTopRem: 0.45,
39
+ inlineChoicesMarginTopRem: 0.25,
40
+ } as const;
3
41
 
4
42
  export default {
5
43
  model: {
@@ -10,6 +48,21 @@ export default {
10
48
  interactionMode: 'populate_blank' as const,
11
49
  layoutProfile: '',
12
50
  choiceLayout: '',
51
+ layoutProfilePresets: {},
52
+ layoutLimits: { ...DEFAULT_LAYOUT_LIMITS },
53
+ audioButtonSkin: null,
54
+ audioButtonSkinsByLocale: {},
55
+ uiText: {
56
+ answerChoices: 'Answer choices',
57
+ selectedAnswerInSentence: 'Selected answer in sentence',
58
+ showCorrectAnswer: 'Show correct answer',
59
+ hideCorrectAnswer: 'Hide correct answer',
60
+ clickToEnableAutoplay: 'Click to enable audio autoplay',
61
+ audioResourceUnavailable: 'Audio is enabled but no playable audio URL is configured.',
62
+ transcriptLabel: 'Transcript',
63
+ listenLabelEn: 'Listen',
64
+ listenLabelEs: 'Escuchar',
65
+ },
13
66
  sentenceHtml: '',
14
67
  template: `<p>The answer is ${BLANK_TOKEN}.</p>`,
15
68
  choiceMode: 'text' as const,
@@ -1,5 +1,5 @@
1
1
  import { isEmpty } from 'lodash-es';
2
- import defaults, { BLANK_TOKEN } from './defaults';
2
+ import defaults, { BLANK_TOKEN, DEFAULT_LAYOUT_LIMITS } from './defaults';
3
3
 
4
4
  export function countBlankTokens(template: string): number {
5
5
  if (!template) return 0;
@@ -72,7 +72,14 @@ export const outcome = (question: any, session: any, env: any) =>
72
72
  resolve({ score, empty: false, traceLog });
73
73
  });
74
74
 
75
- export const createDefaultModel = (model: any = {}) => ({ ...defaults.model, ...model });
75
+ export const createDefaultModel = (model: any = {}) => ({
76
+ ...defaults.model,
77
+ ...model,
78
+ layoutLimits: {
79
+ ...DEFAULT_LAYOUT_LIMITS,
80
+ ...(model?.layoutLimits || {}),
81
+ },
82
+ });
76
83
 
77
84
  export const normalizeSession = (s: any) => ({ ...s });
78
85
 
@@ -151,6 +158,25 @@ export const model = async (question: any, session: any, env: any, updateSession
151
158
  interactionMode: normalizedQuestion.interactionMode || 'populate_blank',
152
159
  layoutProfile: normalizedQuestion.layoutProfile || '',
153
160
  choiceLayout: normalizedQuestion.choiceLayout || '',
161
+ layoutProfilePresets:
162
+ normalizedQuestion.layoutProfilePresets &&
163
+ typeof normalizedQuestion.layoutProfilePresets === 'object'
164
+ ? normalizedQuestion.layoutProfilePresets
165
+ : {},
166
+ layoutLimits: normalizedQuestion.layoutLimits || { ...DEFAULT_LAYOUT_LIMITS },
167
+ audioButtonSkin:
168
+ normalizedQuestion.audioButtonSkin && typeof normalizedQuestion.audioButtonSkin === 'object'
169
+ ? normalizedQuestion.audioButtonSkin
170
+ : null,
171
+ audioButtonSkinsByLocale:
172
+ normalizedQuestion.audioButtonSkinsByLocale &&
173
+ typeof normalizedQuestion.audioButtonSkinsByLocale === 'object'
174
+ ? normalizedQuestion.audioButtonSkinsByLocale
175
+ : {},
176
+ uiText:
177
+ normalizedQuestion.uiText && typeof normalizedQuestion.uiText === 'object'
178
+ ? normalizedQuestion.uiText
179
+ : {},
154
180
  sentenceHtml: normalizedQuestion.sentenceHtml || null,
155
181
  template: normalizedQuestion.template,
156
182
  choiceMode: normalizedQuestion.choiceMode,
@@ -161,6 +187,10 @@ export const model = async (question: any, session: any, env: any, updateSession
161
187
  audioUrl: normalizedQuestion.hasAudio ? normalizedQuestion.audioUrl : null,
162
188
  audioTranscript: normalizedQuestion.hasAudio ? normalizedQuestion.audioTranscript : null,
163
189
  showVisibleTranscript: !!normalizedQuestion.showVisibleTranscript,
190
+ useFeatureButtonAudio:
191
+ typeof normalizedQuestion.useFeatureButtonAudio === 'boolean'
192
+ ? normalizedQuestion.useFeatureButtonAudio
193
+ : undefined,
164
194
  locale: normalizedQuestion.locale || '',
165
195
  disabled: env.mode !== 'gather',
166
196
  view: env.mode === 'view',
@@ -263,9 +293,57 @@ export const validate = (model: any = {}, _config: any = {}) => {
263
293
 
264
294
  if (model.hasAudio) {
265
295
  const hasAudioUrl = !!model.audioUrl?.trim();
266
- const hasTranscript = !!model.audioTranscript?.trim();
267
- if (!hasAudioUrl && !hasTranscript) {
268
- errors.audioTranscript = 'Audio transcript or audio URL is required when audio is enabled';
296
+ if (!hasAudioUrl) {
297
+ errors.audioUrl = 'A playable audio URL is required when audio is enabled';
298
+ }
299
+ }
300
+
301
+ const limits = model?.layoutLimits;
302
+ if (limits && typeof limits === 'object') {
303
+ const numericLimitKeys = [
304
+ 'blankStandaloneWidthRem',
305
+ 'blankWideWidthRem',
306
+ 'blankUnderlineWidthPx',
307
+ 'blankUnderlineWideWidthPx',
308
+ 'horizontalChoiceWidthPx',
309
+ 'horizontalChoiceWidthVw',
310
+ 'horizontalChoiceTileMinHeightRem',
311
+ 'horizontalChoiceContentMinHeightRem',
312
+ 'selectedImageMaxHeightRem',
313
+ 'choiceImageMaxHeightRem',
314
+ 'listenButtonSizePx',
315
+ 'stimulusMinColumnPx',
316
+ 'textMinColumnPx',
317
+ 'legendMaxChars',
318
+ 'choiceGroupGapRem',
319
+ 'choiceRowGapRem',
320
+ 'toggleButtonGapRem',
321
+ 'horizontalChoiceRadioTopMarginRem',
322
+ 'audioBlankTemplateMarginTopRem',
323
+ 'audioBlankTemplateMarginBottomRem',
324
+ 'audioInstructionsMaxWidthPx',
325
+ 'narrowHorizontalChoiceMaxWidthPx',
326
+ 'stimulusGridColumnGapRem',
327
+ 'stimulusGridRowGapRem',
328
+ 'stimulusSentenceMarginTopRem',
329
+ 'stimulusChoicesMarginTopRem',
330
+ 'tokenGridColumnGapRem',
331
+ 'tokenGridRowGapRem',
332
+ 'tokenTemplateMarginTopRem',
333
+ 'tokenInlineTokenGapRem',
334
+ 'tokenChoicesMarginTopRem',
335
+ 'inlineGridColumnGapRem',
336
+ 'inlineGridRowGapRem',
337
+ 'inlineTemplateMarginTopRem',
338
+ 'inlineChoicesMarginTopRem',
339
+ ];
340
+ for (const key of numericLimitKeys) {
341
+ if (limits[key] === undefined || limits[key] === null || limits[key] === '') continue;
342
+ const value = Number(limits[key]);
343
+ if (!Number.isFinite(value) || value <= 0) {
344
+ errors.layoutLimits = `${key} must be a positive number when provided`;
345
+ break;
346
+ }
269
347
  }
270
348
  }
271
349