@tinyweb_dev/oe-exam-sdk 1.0.3 → 1.0.4

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.
@@ -99,6 +99,19 @@ const ColorToolbarBtn = ({ editor }) => {
99
99
  : 'text-gray-600 hover:bg-gray-200 hover:text-gray-900'}`, children: [_jsx(Palette, { className: "h-3.5 w-3.5" }), _jsx("span", { className: "h-3.5 w-3.5 rounded-full border border-gray-300 shadow-sm", style: { backgroundColor: currentColor || '#0f172a' } })] }), showMenu && (_jsxs("div", { className: "absolute left-0 top-full z-50 mt-1 flex flex-col gap-2 rounded-lg border border-gray-200 bg-white p-2.5 shadow-xl min-w-[170px]", children: [_jsx("div", { className: "text-[11px] font-medium text-gray-500 px-0.5", children: "M\u00E0u ch\u1EEF nhanh" }), _jsx("div", { className: "grid grid-cols-4 gap-1.5", children: COLOR_PRESETS.map((p) => (_jsx("button", { type: "button", title: p.label, onClick: () => handleSetColor(p.color), className: "h-6 w-6 rounded-full border border-gray-200 shadow-xs transition-transform hover:scale-110 focus:outline-none", style: { backgroundColor: p.color } }, p.color))) }), _jsxs("div", { className: "flex items-center justify-between gap-2 pt-1.5 border-t border-gray-100", children: [_jsxs("label", { className: "flex items-center gap-1.5 text-[11px] text-gray-600 cursor-pointer", children: [_jsx("span", { children: "T\u00F9y ch\u1ECDn:" }), _jsx("input", { type: "color", value: currentColor || '#000000', onChange: (e) => handleSetColor(e.target.value), className: "h-5 w-5 cursor-pointer rounded border-0 p-0" })] }), _jsx("button", { type: "button", onClick: () => handleSetColor(''), className: "text-[11px] font-medium text-red-500 hover:underline", children: "M\u1EB7c \u0111\u1ECBnh" })] })] }))] }));
100
100
  };
101
101
  const ToolbarSep = () => _jsx("div", { className: "mx-1 h-4 w-px bg-gray-200" });
102
+ const BLANK_STYLE_OPTIONS = [
103
+ { value: 'letter', label: 'Letter', hint: 'Ô 1 ký tự' },
104
+ { value: 'word', label: 'Word', hint: 'Từ ngắn' },
105
+ { value: 'phrase', label: 'Phrase', hint: 'Cụm / câu' },
106
+ ];
107
+ function BlankStyleSwitch({ value, onChange, }) {
108
+ return (_jsxs("div", { role: "radiogroup", "aria-label": "Ki\u1EC3u \u00F4 tr\u1ED1ng", className: "inline-flex flex-wrap items-center gap-1 rounded-md border border-gray-200 bg-white p-1", children: [BLANK_STYLE_OPTIONS.map((option) => {
109
+ const selected = value === option.value;
110
+ return (_jsxs("button", { type: "button", role: "radio", "aria-checked": selected, title: option.hint, onClick: () => onChange(option.value), className: `inline-flex items-center gap-1.5 rounded px-2.5 py-1 text-sm font-medium transition-colors ${selected
111
+ ? 'border border-teal-300 bg-teal-50 text-teal-700'
112
+ : 'border border-transparent text-gray-400 hover:bg-gray-50 hover:text-gray-600'}`, children: [option.label, _jsx("span", { className: `text-[10px] font-normal ${selected ? 'text-teal-600' : 'text-gray-400'}`, children: option.hint })] }, option.value));
113
+ }), value === 'translate' && (_jsxs("span", { role: "radio", "aria-checked": true, title: "Ch\u1EA5m LLM \u2014 gi\u1EEF nguy\u00EAn \u0111\u1EBFn khi ch\u1ECDn style kh\u00E1c", className: "inline-flex items-center gap-1.5 rounded border border-teal-300 bg-teal-50 px-2.5 py-1 text-sm font-medium text-teal-700", children: ["Translate", _jsx("span", { className: "text-[10px] font-normal text-teal-600", children: "D\u1ECBch (LLM)" })] }))] }));
114
+ }
102
115
  const EditorToolbar = ({ editor }) => {
103
116
  if (!editor)
104
117
  return null;
@@ -126,7 +139,7 @@ export function WordFillStructuredFormCreator({ initialData, onSave, onCancel, o
126
139
  // Page rubric (Pearson draft.instruction). Prefer instruction; legacy load may use explanation.
127
140
  const [instruction, setInstruction] = useState(initialData?.instruction || initialData?.explanation || '');
128
141
  const [explanation, setExplanation] = useState(initialData?.explanation || '');
129
- const [blankStyle, setBlankStyle] = useState(initialData?.blankStyle);
142
+ const [blankStyle, setBlankStyle] = useState(initialData?.blankStyle ?? 'word');
130
143
  // ── Multiple Images & Audios ────────────────────────────────────────────────
131
144
  const [imageUrls, setImageUrls] = useState(() => {
132
145
  if (Array.isArray(initialData?.imageUrls) && initialData.imageUrls.length > 0) {
@@ -250,7 +263,7 @@ export function WordFillStructuredFormCreator({ initialData, onSave, onCancel, o
250
263
  : '');
251
264
  }
252
265
  setAnswers(normalizeWfsfAnswersMap(initialData?.answers || {}));
253
- setBlankStyle(initialData?.blankStyle);
266
+ setBlankStyle(initialData?.blankStyle ?? 'word');
254
267
  const nextMode = detectWfsfBankMode(initialData?.viewMode ?? viewModeProp, initialData?.wordBank);
255
268
  setBankMode(nextMode);
256
269
  if (nextMode === 'WITH_WORD_BANK') {
@@ -481,7 +494,7 @@ export function WordFillStructuredFormCreator({ initialData, onSave, onCancel, o
481
494
  return (_jsxs("div", { className: "space-y-4", children: [isGrouped && effectiveGroupType === 'BASIC' && (_jsx(BasicQuestionGroupCard, { groupNumber: currentGroupNumber, title: groupTitle, subtitle: groupSubtitle, content: groupContent, imageUrl: groupImageUrl, onTitleChange: setGroupTitle, onSubtitleChange: setGroupSubtitle, onContentChange: setGroupContent, onImageChange: setGroupImageUrl, groupConfig: groupType ? groupConfig : undefined, isReadOnly: !isFirstInGroup, uploadIdPrefix: `word-fill-structured-${partId ?? 'group'}` })), isGrouped && effectiveGroupType === 'DOCUMENT' && (_jsx(DocumentGroupCard, { groupNumber: currentGroupNumber, title: groupTitle, subtitle: groupSubtitle, content: groupContent, imageUrl: groupImageUrl, onTitleChange: setGroupTitle, onSubtitleChange: setGroupSubtitle, onContentChange: setGroupContent, onImageChange: setGroupImageUrl, groupConfig: groupType ? groupConfig : undefined, isReadOnly: !isFirstInGroup, uploadIdPrefix: `word-fill-structured-${partId ?? 'document'}` })), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-teal-100/50", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-teal-500 via-cyan-500 to-emerald-500" }), _jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gray-100", children: _jsx(PenLine, { className: "h-4 w-4 text-teal-600" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-base font-bold text-gray-900", children: "\u0110i\u1EC1n t\u1EEB v\u00E0o bi\u1EC3u m\u1EABu c\u00F3 c\u1EA5u tr\u00FAc" }), _jsxs("p", { className: "text-xs text-gray-500", children: ["Nh\u1EADp n\u1ED9i dung, \u0111\u1EB7t ch\u1ED7 tr\u1ED1ng b\u1EB1ng ", `{0}`, " ", `{1}`, "\u2026 r\u1ED3i \u0111i\u1EC1n \u0111\u00E1p \u00E1n b\u00EAn d\u01B0\u1EDBi"] })] })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsPreviewMode(true), disabled: expectedBlankKeys.length === 0 || expectedBlankKeys.some(k => !hasWfsfBlankAnswer(answers[k])), className: "gap-2 border-teal-200 text-teal-600 hover:bg-teal-50 hover:text-teal-700", children: [_jsx(Eye, { className: "h-4 w-4" }), "Xem tr\u01B0\u1EDBc"] })] }) }), _jsxs(CardContent, { className: "space-y-4 px-4 pb-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { htmlFor: "wfsf-instruction", className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(Info, { className: "h-4 w-4 text-teal-500" }), "Instruction", _jsx("span", { className: "text-sm font-normal text-gray-500", children: "(h\u01B0\u1EDBng d\u1EABn b\u00E0i t\u1EADp)" })] }), _jsx(Textarea, { id: "wfsf-instruction", value: instruction, onChange: (e) => setInstruction(e.target.value), placeholder: "VD: Complete the words in the sentences. Use the words in the box.", rows: 2, className: "min-h-[64px] resize-y border-gray-200 bg-white transition-all focus:border-teal-300 focus:ring-teal-100" })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsx(PointsInput, { value: points, onChange: (e) => setPoints(Number(e.target.value) || 1) }), _jsxs("div", { role: "button", tabIndex: 0, onClick: () => setCaseSensitive(!caseSensitive), onKeyDown: (e) => { if (e.key === ' ' || e.key === 'Enter')
482
495
  setCaseSensitive(!caseSensitive); }, style: { paddingBlock: 'calc(var(--spacing) * 1)' }, className: `inline-flex cursor-pointer items-center gap-2 rounded-md border px-3.5 text-sm font-medium transition-colors select-none ${caseSensitive
483
496
  ? 'border-teal-300 bg-teal-50 text-teal-700'
484
- : 'border-gray-200 bg-white text-gray-400 hover:text-gray-600'}`, children: [_jsx(Switch, { checked: caseSensitive, onCheckedChange: setCaseSensitive, className: caseSensitive ? 'data-[state=checked]:bg-teal-500 pointer-events-none' : 'pointer-events-none', tabIndex: -1 }), "Ph\u00E2n bi\u1EC7t Hoa / th\u01B0\u1EDDng"] })] }), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { htmlFor: "wfsf-title", className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(MessageSquare, { className: "h-4 w-4 text-teal-500" }), "Ti\u00EAu \u0111\u1EC1", _jsx("span", { className: "text-sm font-normal text-gray-500", children: "(tu\u1EF3 ch\u1ECDn)" })] }), _jsx(Input, { id: "wfsf-title", value: effectiveTitle, onChange: (e) => setEffectiveTitle(e.target.value), placeholder: "Nh\u1EADp ti\u00EAu \u0111\u1EC1 c\u00E2u h\u1ECFi...", className: "border-gray-200 bg-white transition-all focus:border-teal-300 focus:ring-teal-100" })] }), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs(Label, { className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(Volume2, { className: "h-4 w-4 text-teal-500" }), "File \u00E2m thanh (Audio)", _jsx("span", { className: "text-sm font-normal text-gray-500", children: "(tu\u1EF3 ch\u1ECDn)" })] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setAudioUrls((prev) => [...prev, '']), className: "gap-1.5 border-teal-200 text-teal-600 hover:bg-teal-50 hover:text-teal-700", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm audio"] })] }), audioUrls.length === 0 ? (_jsx("div", { className: "flex items-center justify-between rounded-lg border border-dashed border-gray-200 bg-gray-50/50 p-3 text-xs text-gray-500", children: _jsx("span", { children: "Ch\u01B0a c\u00F3 file audio. Nh\u1EA5n \"Th\u00EAm audio\" \u0111\u1EC3 upload file nghe cho c\u00E2u h\u1ECFi." }) })) : (_jsx("div", { className: "space-y-2.5", children: audioUrls.map((audioUrl, index) => (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50/30 p-2.5", children: [_jsx("span", { className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-teal-100 text-xs font-semibold text-teal-700", children: index + 1 }), _jsx("div", { className: "flex-1", children: _jsx(FileUpload, { id: `wfsf-audio-${index}`, label: "", accept: "audio/*", value: audioUrl, onChange: (url) => {
497
+ : 'border-gray-200 bg-white text-gray-400 hover:text-gray-600'}`, children: [_jsx(Switch, { checked: caseSensitive, onCheckedChange: setCaseSensitive, className: caseSensitive ? 'data-[state=checked]:bg-teal-500 pointer-events-none' : 'pointer-events-none', tabIndex: -1 }), "Ph\u00E2n bi\u1EC7t Hoa / th\u01B0\u1EDDng"] }), _jsx(BlankStyleSwitch, { value: blankStyle, onChange: setBlankStyle })] }), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { htmlFor: "wfsf-title", className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(MessageSquare, { className: "h-4 w-4 text-teal-500" }), "Ti\u00EAu \u0111\u1EC1", _jsx("span", { className: "text-sm font-normal text-gray-500", children: "(tu\u1EF3 ch\u1ECDn)" })] }), _jsx(Input, { id: "wfsf-title", value: effectiveTitle, onChange: (e) => setEffectiveTitle(e.target.value), placeholder: "Nh\u1EADp ti\u00EAu \u0111\u1EC1 c\u00E2u h\u1ECFi...", className: "border-gray-200 bg-white transition-all focus:border-teal-300 focus:ring-teal-100" })] }), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs(Label, { className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(Volume2, { className: "h-4 w-4 text-teal-500" }), "File \u00E2m thanh (Audio)", _jsx("span", { className: "text-sm font-normal text-gray-500", children: "(tu\u1EF3 ch\u1ECDn)" })] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setAudioUrls((prev) => [...prev, '']), className: "gap-1.5 border-teal-200 text-teal-600 hover:bg-teal-50 hover:text-teal-700", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm audio"] })] }), audioUrls.length === 0 ? (_jsx("div", { className: "flex items-center justify-between rounded-lg border border-dashed border-gray-200 bg-gray-50/50 p-3 text-xs text-gray-500", children: _jsx("span", { children: "Ch\u01B0a c\u00F3 file audio. Nh\u1EA5n \"Th\u00EAm audio\" \u0111\u1EC3 upload file nghe cho c\u00E2u h\u1ECFi." }) })) : (_jsx("div", { className: "space-y-2.5", children: audioUrls.map((audioUrl, index) => (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50/30 p-2.5", children: [_jsx("span", { className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-teal-100 text-xs font-semibold text-teal-700", children: index + 1 }), _jsx("div", { className: "flex-1", children: _jsx(FileUpload, { id: `wfsf-audio-${index}`, label: "", accept: "audio/*", value: audioUrl, onChange: (url) => {
485
498
  setAudioUrls((prev) => {
486
499
  const next = [...prev];
487
500
  next[index] = url;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinyweb_dev/oe-exam-sdk",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Reusable OceanEdu question components.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",