@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.
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.0",
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,41 @@
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
+ stimulusGridColumnGapRem: 2,
26
+ stimulusGridRowGapRem: 0.7,
27
+ stimulusSentenceMarginTopRem: 0.2,
28
+ stimulusChoicesMarginTopRem: 0.9,
29
+ tokenGridColumnGapRem: 1.5,
30
+ tokenGridRowGapRem: 0.8,
31
+ tokenTemplateMarginTopRem: 0.6,
32
+ tokenInlineTokenGapRem: 0.35,
33
+ tokenChoicesMarginTopRem: 0.2,
34
+ inlineGridColumnGapRem: 1.5,
35
+ inlineGridRowGapRem: 0.65,
36
+ inlineTemplateMarginTopRem: 0.45,
37
+ inlineChoicesMarginTopRem: 0.25,
38
+ } as const;
3
39
 
4
40
  export default {
5
41
  model: {
@@ -10,6 +46,21 @@ export default {
10
46
  interactionMode: 'populate_blank' as const,
11
47
  layoutProfile: '',
12
48
  choiceLayout: '',
49
+ layoutProfilePresets: {},
50
+ layoutLimits: { ...DEFAULT_LAYOUT_LIMITS },
51
+ audioButtonSkin: null,
52
+ audioButtonSkinsByLocale: {},
53
+ uiText: {
54
+ answerChoices: 'Answer choices',
55
+ selectedAnswerInSentence: 'Selected answer in sentence',
56
+ showCorrectAnswer: 'Show correct answer',
57
+ hideCorrectAnswer: 'Hide correct answer',
58
+ clickToEnableAutoplay: 'Click to enable audio autoplay',
59
+ audioResourceUnavailable: 'Audio is enabled but no playable audio URL is configured.',
60
+ transcriptLabel: 'Transcript',
61
+ listenLabelEn: 'Listen',
62
+ listenLabelEs: 'Escuchar',
63
+ },
13
64
  sentenceHtml: '',
14
65
  template: `<p>The answer is ${BLANK_TOKEN}.</p>`,
15
66
  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,28 @@ 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: {
167
+ ...DEFAULT_LAYOUT_LIMITS,
168
+ ...(normalizedQuestion.layoutLimits || {}),
169
+ },
170
+ audioButtonSkin:
171
+ normalizedQuestion.audioButtonSkin && typeof normalizedQuestion.audioButtonSkin === 'object'
172
+ ? normalizedQuestion.audioButtonSkin
173
+ : null,
174
+ audioButtonSkinsByLocale:
175
+ normalizedQuestion.audioButtonSkinsByLocale &&
176
+ typeof normalizedQuestion.audioButtonSkinsByLocale === 'object'
177
+ ? normalizedQuestion.audioButtonSkinsByLocale
178
+ : {},
179
+ uiText:
180
+ normalizedQuestion.uiText && typeof normalizedQuestion.uiText === 'object'
181
+ ? normalizedQuestion.uiText
182
+ : {},
154
183
  sentenceHtml: normalizedQuestion.sentenceHtml || null,
155
184
  template: normalizedQuestion.template,
156
185
  choiceMode: normalizedQuestion.choiceMode,
@@ -161,6 +190,10 @@ export const model = async (question: any, session: any, env: any, updateSession
161
190
  audioUrl: normalizedQuestion.hasAudio ? normalizedQuestion.audioUrl : null,
162
191
  audioTranscript: normalizedQuestion.hasAudio ? normalizedQuestion.audioTranscript : null,
163
192
  showVisibleTranscript: !!normalizedQuestion.showVisibleTranscript,
193
+ useFeatureButtonAudio:
194
+ typeof normalizedQuestion.useFeatureButtonAudio === 'boolean'
195
+ ? normalizedQuestion.useFeatureButtonAudio
196
+ : undefined,
164
197
  locale: normalizedQuestion.locale || '',
165
198
  disabled: env.mode !== 'gather',
166
199
  view: env.mode === 'view',
@@ -263,9 +296,55 @@ export const validate = (model: any = {}, _config: any = {}) => {
263
296
 
264
297
  if (model.hasAudio) {
265
298
  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';
299
+ if (!hasAudioUrl) {
300
+ errors.audioUrl = 'A playable audio URL is required when audio is enabled';
301
+ }
302
+ }
303
+
304
+ const limits = model?.layoutLimits;
305
+ if (limits && typeof limits === 'object') {
306
+ const numericLimitKeys = [
307
+ 'blankStandaloneWidthRem',
308
+ 'blankWideWidthRem',
309
+ 'blankUnderlineWidthPx',
310
+ 'blankUnderlineWideWidthPx',
311
+ 'horizontalChoiceWidthPx',
312
+ 'horizontalChoiceWidthVw',
313
+ 'horizontalChoiceTileMinHeightRem',
314
+ 'horizontalChoiceContentMinHeightRem',
315
+ 'selectedImageMaxHeightRem',
316
+ 'choiceImageMaxHeightRem',
317
+ 'listenButtonSizePx',
318
+ 'stimulusMinColumnPx',
319
+ 'textMinColumnPx',
320
+ 'legendMaxChars',
321
+ 'choiceGroupGapRem',
322
+ 'choiceRowGapRem',
323
+ 'toggleButtonGapRem',
324
+ 'horizontalChoiceRadioTopMarginRem',
325
+ 'audioBlankTemplateMarginTopRem',
326
+ 'audioBlankTemplateMarginBottomRem',
327
+ 'stimulusGridColumnGapRem',
328
+ 'stimulusGridRowGapRem',
329
+ 'stimulusSentenceMarginTopRem',
330
+ 'stimulusChoicesMarginTopRem',
331
+ 'tokenGridColumnGapRem',
332
+ 'tokenGridRowGapRem',
333
+ 'tokenTemplateMarginTopRem',
334
+ 'tokenInlineTokenGapRem',
335
+ 'tokenChoicesMarginTopRem',
336
+ 'inlineGridColumnGapRem',
337
+ 'inlineGridRowGapRem',
338
+ 'inlineTemplateMarginTopRem',
339
+ 'inlineChoicesMarginTopRem',
340
+ ];
341
+ for (const key of numericLimitKeys) {
342
+ if (limits[key] === undefined || limits[key] === null || limits[key] === '') continue;
343
+ const value = Number(limits[key]);
344
+ if (!Number.isFinite(value) || value <= 0) {
345
+ errors.layoutLimits = `${key} must be a positive number when provided`;
346
+ break;
347
+ }
269
348
  }
270
349
  }
271
350