speakid-story-builder 1.0.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/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # Story Builder
2
+
3
+ Interactive story game for English practice (SPEAKID). The story is chosen first; it defines which questions the student answers. Answers are then woven into a comic-style story.
4
+
5
+ ## Flow
6
+
7
+ 1. **Lobby** — Play
8
+ 2. **Play** — Pick a random story → story provides `inputKeys` and `comprehensionQuestions` → show **Questions** (5 inputs, first is always "What is your name?")
9
+ 3. **Story** — Slides with substituted answers; prev/next navigation
10
+ 4. **Comprehension** — Questions about the story (if any)
11
+ 5. **End** — Try again (new story) | Return to lobby
12
+
13
+ ## Tech
14
+
15
+ - React 18+, TypeScript, Vite
16
+ - Layout: 1280×720 base, scaled to viewport; portrait mobile shows "Please rotate your device"
17
+ - CSS Modules, all selectors under `.root` for NPM host isolation
18
+
19
+ ## Dev
20
+
21
+ ```bash
22
+ npm install
23
+ npm run dev
24
+ ```
25
+
26
+ Open http://localhost:5174
27
+
28
+ ## Build (library)
29
+
30
+ ```bash
31
+ npm run build
32
+ ```
33
+
34
+ Output: `dist/` (ES + CJS + types + style.css).
35
+
36
+ ## Use as NPM
37
+
38
+ ```tsx
39
+ import { StoryBuilder } from "speakid-story-builder";
40
+ import "speakid-story-builder/style.css";
41
+
42
+ <StoryBuilder
43
+ questions={questions}
44
+ stories={stories}
45
+ onEvent={(e) => console.log(e)}
46
+ />
47
+ ```
48
+
49
+ ## Data
50
+
51
+ - **Question bank**: `Question { id, answerKey, text, placeholder? }`. Stories reference keys (e.g. `name`, `goal`, `problem`).
52
+ - **Stories**: `Story { id, slides, inputKeys, comprehensionQuestions? }`. `inputKeys` is the list of answer keys needed (e.g. `["name", "goal", "problem", "food", "place"]`); first should be `name`.
@@ -0,0 +1,9 @@
1
+ import type { Question, Story, GameEvent, StoryBuilderConfig } from "../types";
2
+ export interface StoryBuilderGameProps {
3
+ config?: StoryBuilderConfig;
4
+ questions: Question[];
5
+ stories: Story[];
6
+ onEvent?: (event: GameEvent) => void;
7
+ }
8
+ export declare function StoryBuilder({ config, questions: questionBank, stories, onEvent, }: StoryBuilderGameProps): import("react/jsx-runtime").JSX.Element | null;
9
+ //# sourceMappingURL=StoryBuilderGame.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StoryBuilderGame.d.ts","sourceRoot":"","sources":["../../src/components/StoryBuilderGame.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAkB,SAAS,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAkB/F,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,OAAO,EAAE,KAAK,EAAE,CAAC;IACjB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;CACtC;AAED,wBAAgB,YAAY,CAAC,EAC3B,MAAM,EACN,SAAS,EAAE,YAAY,EACvB,OAAO,EACP,OAAO,GACR,EAAE,qBAAqB,kDAwTvB"}
@@ -0,0 +1,2 @@
1
+ export declare const DEFAULT_QUESTIONS_PER_ROUND = 5;
2
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,2BAA2B,IAAI,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { Question } from "../types";
2
+ /** Question bank: flat, serializable. answerKey used for story placeholders. */
3
+ export declare const questions: Question[];
4
+ //# sourceMappingURL=questions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"questions.d.ts","sourceRoot":"","sources":["../../src/data/questions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEzC,gFAAgF;AAChF,eAAO,MAAM,SAAS,EAAE,QAAQ,EA4D/B,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { Story } from "../types";
2
+ /**
3
+ * All stories. Images live in public/stories/{story.id}/ (e.g. slide1.png).
4
+ * Add a new story: create folder public/stories/{id}/, add slides and imageUrls, add questions for inputKeys.
5
+ * See docs/ADDING_STORIES.md.
6
+ */
7
+ export declare const stories: Story[];
8
+ //# sourceMappingURL=stories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stories.d.ts","sourceRoot":"","sources":["../../src/data/stories.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEtC;;;;GAIG;AACH,eAAO,MAAM,OAAO,EAAE,KAAK,EA2Q1B,CAAC"}
@@ -0,0 +1,6 @@
1
+ export { StoryBuilder } from "./components/StoryBuilderGame";
2
+ export type { StoryBuilderGameProps as StoryBuilderProps } from "./components/StoryBuilderGame";
3
+ export type { Question, Story, Slide, ComprehensionQuestion, Answers, Stage, GameEvent, StoryBuilderConfig, } from "./types";
4
+ export { getQuestionsForStory } from "./utils/getQuestionsForStory";
5
+ export { renderText } from "./utils/renderText";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAC7D,YAAY,EAAE,qBAAqB,IAAI,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAChG,YAAY,EACV,QAAQ,EACR,KAAK,EACL,KAAK,EACL,qBAAqB,EACrB,OAAO,EACP,KAAK,EACL,SAAS,EACT,kBAAkB,GACnB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=require("react/jsx-runtime"),c=require("react");function x(d,l){const b=new Map(d.map(o=>[o.id,o]));if(l.questionIds&&l.questionIds.length===l.inputKeys.length)return l.questionIds.map(o=>{const u=b.get(o);if(!u)throw new Error(`Question not found: ${o}`);return u});const i=new Map;for(const o of d){const u=i.get(o.answerKey)??[];u.push(o),i.set(o.answerKey,u)}return l.inputKeys.map(o=>{const u=i.get(o);if(!u||u.length===0)throw new Error(`No question found for answerKey: ${o}`);return u[Math.floor(Math.random()*u.length)]})}const K={name:"the hero",goal:"a dream",problem:"doubts",place:"far away",food:"treasure",thing:"courage",pet:"a friend",father:"Dad",mum:"Mum",neighbour:"Mrs Brown",friend:"a friend",profession:"a hero",enemy:"the dragon",hobby:"dancing",book:"about adventures",subject:"nature",librarian:"the librarian",genre:"adventure",style:"ballet"},P=new Set(["name","father","mum","dad","friend","enemy","groom","pet","coach","teacher","librarian","neighbour"]);function G(d,l){const b=l.trim();return b.length===0?b:P.has(d)?b.charAt(0).toUpperCase()+b.slice(1).toLowerCase():b.toLowerCase()}function B(d,l){return d.replace(/\{\{(\w+)\}\}/g,(b,i)=>{const o=l[i];return o!=null&&o!==""?G(i,String(o)):K[i]??"something"})}const q="_root_j1p5w_1",Q="_container_j1p5w_76",H="_lobbyScreen_j1p5w_89",M="_lobbyContent_j1p5w_96",U="_storyList_j1p5w_103",O="_button_j1p5w_116",D="_storySelectBack_j1p5w_122",R="_questionsScreen_j1p5w_126",z="_headline1_j1p5w_134",X="_headline2_j1p5w_142",Z="_inputHint_j1p5w_150",Y="_buttonLarge_j1p5w_186",V="_inputGroup_j1p5w_192",W="_slideContainer_j1p5w_224",J="_slideFrame_j1p5w_234",E="_slideImage_j1p5w_242",ee="_slideLayers_j1p5w_252",se="_slideLayer_j1p5w_252",te="_slideLayerCharacter_j1p5w_272",ne="_slideSpeechBubble_j1p5w_287",ie="_slideSpeechBubbleCenter_j1p5w_305",oe="_slideSpeechBubbleTopSmall_j1p5w_316",le="_slideSpeechBubbleBottom_j1p5w_321",re="_slideNav_j1p5w_333",ae="_slideNavBtn_j1p5w_350",ce="_comprehensionSlide_j1p5w_386",de="_comprehensionSlideImage_j1p5w_393",ue="_comprehensionBubble_j1p5w_416",be="_comprehensionBubbleList_j1p5w_444",he="_comprehensionActionsOnly_j1p5w_469",pe="_endActions_j1p5w_531",e={root:q,container:Q,lobbyScreen:H,lobbyContent:M,storyList:U,button:O,storySelectBack:D,questionsScreen:R,headline1:z,headline2:X,inputHint:Z,buttonLarge:Y,inputGroup:V,slideContainer:W,slideFrame:J,slideImage:E,slideLayers:ee,slideLayer:se,slideLayerCharacter:te,slideSpeechBubble:ne,slideSpeechBubbleCenter:ie,slideSpeechBubbleTopSmall:oe,slideSpeechBubbleBottom:le,slideNav:re,slideNavBtn:ae,comprehensionSlide:ce,comprehensionSlideImage:de,comprehensionBubble:ue,comprehensionBubbleList:be,comprehensionActionsOnly:he,endActions:pe},A=40,me=/[^a-zA-Z ]/g;function _e(d){return d.replace(me,"").slice(0,A)}function ye(d){const l=d.trim();return l?l.charAt(0).toUpperCase()+l.slice(1):d}function je({config:d,questions:l,stories:b,onEvent:i}){var C,L;const[o,u]=c.useState("lobby"),[r,N]=c.useState(null),[y,S]=c.useState([]),[g,j]=c.useState({}),[p,_]=c.useState(0),a=c.useCallback(s=>{u(s),i==null||i({type:"STAGE_CHANGE",stage:s})},[i]),I=c.useCallback((s,n)=>{j(h=>({...h,[s]:n}));const m=y.find(h=>h.answerKey===s);m&&(i==null||i({type:"ANSWER_SUBMIT",questionId:m.id,value:n}))},[y,i]),$=c.useCallback(()=>{var n;if(!r)return;const s=r.slides.length;if(p<s-1){const m=p+1;_(m),i==null||i({type:"SLIDE_CHANGE",index:m})}else(n=r.comprehensionQuestions)!=null&&n.length?a("comprehension"):a("end")},[r,p,i,a]),v=c.useCallback(()=>{p>0&&(_(p-1),i==null||i({type:"SLIDE_CHANGE",index:p-1}))},[p,i]),T=c.useCallback(s=>{const n=x(l,s);N(s),S(n),j({}),_(0),a("questions")},[l,a]),k=c.useCallback(()=>{a("story")},[a]),w=c.useCallback(()=>{if(!r)return;const s=x(l,r);S(s),j({}),_(0),a("questions")},[r,l,a]),f=c.useCallback(()=>{N(null),S([]),j({}),_(0),a("lobby")},[a]);if(o==="lobby")return t.jsx("div",{className:`${e.root} ${e.lobbyScreen}`,children:t.jsx("div",{className:e.container,children:t.jsxs("div",{className:e.lobbyContent,children:[t.jsx("h1",{className:e.headline1,children:"Story Builder"}),t.jsx("p",{className:e.headline2,children:"Practice English with your own story"}),t.jsx("button",{type:"button",className:`${e.button} ${e.buttonLarge}`,onClick:()=>a("storySelect"),children:"Play"})]})})});if(o==="storySelect")return t.jsx("div",{className:`${e.root} ${e.questionsScreen}`,children:t.jsxs("div",{className:e.container,children:[t.jsx("h2",{className:e.headline2,children:"Choose a story"}),t.jsx("div",{className:e.storyList,children:b.map(s=>t.jsx("button",{type:"button",className:`${e.button} ${e.buttonLarge}`,onClick:()=>T(s),children:s.title??s.id},s.id))}),t.jsx("button",{type:"button",className:`${e.button} ${e.storySelectBack}`,onClick:()=>a("lobby"),children:"Back"})]})});if(o==="questions"){const s=y.every(n=>(g[n.answerKey]??"").trim().length>0);return t.jsx("div",{className:`${e.root} ${e.questionsScreen}`,children:t.jsxs("div",{className:e.container,children:[t.jsx("h2",{className:e.headline2,children:"Answer a few questions"}),t.jsx("p",{className:e.inputHint,children:"Only Latin letters (A–Z) and spaces, max 40 characters per field."}),y.map(n=>t.jsxs("div",{className:e.inputGroup,children:[t.jsx("label",{htmlFor:n.id,children:n.text}),t.jsx("input",{id:n.id,type:"text",maxLength:A,autoComplete:"off",placeholder:n.placeholder,value:g[n.answerKey]??"",onChange:m=>{let h=_e(m.target.value);n.answerKey==="subject"&&(h=ye(h)),I(n.answerKey,h)}})]},n.id)),t.jsx("button",{type:"button",className:`${e.button} ${e.buttonLarge}`,onClick:k,disabled:!s,children:"Continue"})]})})}if(o==="story"&&r){const s=r.slides[p];let n=e.slideSpeechBubble;return(s==null?void 0:s.speechBubblePosition)==="center"||(s==null?void 0:s.speechBubblePosition)===void 0&&(s==null?void 0:s.id)==="s6"&&r.id==="hero-1"?n=`${e.slideSpeechBubble} ${e.slideSpeechBubbleCenter}`:(s==null?void 0:s.speechBubblePosition)==="bottom"||(s==null?void 0:s.speechBubblePosition)===void 0&&(s==null?void 0:s.id)==="s7"&&r.id==="hero-1"?n=`${e.slideSpeechBubble} ${e.slideSpeechBubbleBottom}`:r.id==="hero-8"&&(s==null?void 0:s.id)==="s6"&&(n=`${e.slideSpeechBubble} ${e.slideSpeechBubbleTopSmall}`),t.jsx("div",{className:e.root,children:t.jsx("div",{className:e.slideContainer,children:t.jsxs("div",{className:e.slideFrame,children:[(C=s==null?void 0:s.imageLayers)!=null&&C.length?t.jsx("div",{className:e.slideLayers,children:s.imageLayers.map((m,h)=>{const F=h===s.imageLayers.length-1;return t.jsx("img",{src:m,alt:"",className:F?e.slideLayerCharacter:e.slideLayer},h)})}):s!=null&&s.imageUrl?t.jsx("img",{src:s.imageUrl,alt:"",className:e.slideImage}):t.jsx("div",{className:e.slideImage,"aria-hidden":!0}),t.jsx("div",{className:n,role:"dialog","aria-label":"Hero says",children:s?B(s.text,g):""}),t.jsxs("div",{className:e.slideNav,children:[t.jsx("button",{type:"button",className:e.slideNavBtn,onClick:v,disabled:p===0,"aria-label":"Previous slide",children:t.jsx("img",{src:"/slide-prev.png",alt:""})}),t.jsx("button",{type:"button",className:e.slideNavBtn,onClick:$,"aria-label":"Next slide",children:t.jsx("img",{src:"/slide-next.png",alt:""})})]})]})})})}if(o==="comprehension"&&((L=r==null?void 0:r.comprehensionQuestions)!=null&&L.length)){const s=r.comprehensionImageUrl;return t.jsx("div",{className:e.root,children:t.jsxs("div",{className:e.comprehensionSlide,children:[s?t.jsx("img",{src:s,alt:"",className:e.comprehensionSlideImage}):t.jsx("div",{className:e.comprehensionSlideImage,"aria-hidden":!0}),!r.comprehensionQuestionsOnImage&&t.jsx("div",{className:e.comprehensionBubble,role:"dialog","aria-label":"Comprehension questions",children:t.jsx("ul",{className:e.comprehensionBubbleList,children:r.comprehensionQuestions.map(n=>t.jsx("li",{children:n.text},n.id))})}),t.jsxs("div",{className:e.comprehensionActionsOnly,children:[t.jsx("button",{type:"button",className:`${e.button} ${e.buttonLarge}`,onClick:w,children:"PLAY AGAIN"}),t.jsx("button",{type:"button",className:`${e.button} ${e.buttonLarge}`,onClick:f,children:"EXIT"})]})]})})}return o==="end"?t.jsx("div",{className:e.root,children:t.jsxs("div",{className:e.container,children:[t.jsx("h1",{className:e.headline1,children:"The End"}),t.jsxs("div",{className:e.endActions,children:[t.jsx("button",{type:"button",className:`${e.button} ${e.buttonLarge}`,onClick:w,children:"Try again"}),t.jsx("button",{type:"button",className:`${e.button} ${e.buttonLarge}`,onClick:f,children:"Return to lobby"})]})]})}):null}exports.StoryBuilder=je;exports.getQuestionsForStory=x;exports.renderText=B;
2
+ //# sourceMappingURL=speakid-story-builder.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"speakid-story-builder.cjs.js","sources":["../src/utils/getQuestionsForStory.ts","../src/utils/renderText.ts","../src/components/StoryBuilderGame.tsx"],"sourcesContent":["import type { Question, Story } from \"../types\";\n\n/**\n * For each key in story.inputKeys, pick one question:\n * - If story.questionIds is set (same length as inputKeys), use those question ids from the bank.\n * - Otherwise pick one random question per answerKey.\n */\nexport function getQuestionsForStory(\n questionBank: Question[],\n story: Story\n): Question[] {\n const byId = new Map(questionBank.map((q) => [q.id, q]));\n\n if (story.questionIds && story.questionIds.length === story.inputKeys.length) {\n return story.questionIds.map((id) => {\n const q = byId.get(id);\n if (!q) throw new Error(`Question not found: ${id}`);\n return q;\n });\n }\n\n const byKey = new Map<string, Question[]>();\n for (const q of questionBank) {\n const list = byKey.get(q.answerKey) ?? [];\n list.push(q);\n byKey.set(q.answerKey, list);\n }\n\n return story.inputKeys.map((key) => {\n const list = byKey.get(key);\n if (!list || list.length === 0) {\n throw new Error(`No question found for answerKey: ${key}`);\n }\n return list[Math.floor(Math.random() * list.length)];\n });\n}\n","/** Нейтральные подстановки, чтобы пользователь никогда не видел «дырки» в тексте */\nconst DEFAULT_FALLBACKS: Record<string, string> = {\n name: \"the hero\",\n goal: \"a dream\",\n problem: \"doubts\",\n place: \"far away\",\n food: \"treasure\",\n thing: \"courage\",\n pet: \"a friend\",\n father: \"Dad\",\n mum: \"Mum\",\n neighbour: \"Mrs Brown\",\n friend: \"a friend\",\n profession: \"a hero\",\n enemy: \"the dragon\",\n hobby: \"dancing\",\n book: \"about adventures\",\n subject: \"nature\",\n librarian: \"the librarian\",\n genre: \"adventure\",\n style: \"ballet\",\n};\n\n/** Имена и обращения — всегда с большой буквы; профессия, еда, хобби — lower case */\nconst CAPITALIZE_KEYS = new Set([\n \"name\",\n \"father\",\n \"mum\",\n \"dad\",\n \"friend\",\n \"enemy\",\n \"groom\",\n \"pet\",\n \"coach\",\n \"teacher\",\n \"librarian\",\n \"neighbour\",\n]);\n\nfunction formatValue(key: string, raw: string): string {\n const s = raw.trim();\n if (s.length === 0) return s;\n if (CAPITALIZE_KEYS.has(key)) {\n return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();\n }\n return s.toLowerCase();\n}\n\n/**\n * Replaces placeholders {{key}} in text with values from answers.\n * name, father, friend, enemy → first letter uppercase; profession, food → lowercase.\n * Missing key → neutral fallback (never empty), so the sentence always reads well.\n */\nexport function renderText(text: string, answers: Record<string, string>): string {\n return text.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key: string) => {\n const value = answers[key];\n if (value != null && value !== \"\") return formatValue(key, String(value));\n return DEFAULT_FALLBACKS[key] ?? \"something\";\n });\n}\n","import React, { useState, useCallback } from \"react\";\nimport type { Question, Story, Answers, Stage, GameEvent, StoryBuilderConfig } from \"../types\";\nimport { getQuestionsForStory } from \"../utils/getQuestionsForStory\";\nimport { renderText } from \"../utils/renderText\";\nimport cssStyles from \"./StoryBuilder.module.css\";\n\nconst INPUT_MAX_LENGTH = 40;\nconst LATIN_AND_SPACE_REGEX = /[^a-zA-Z ]/g;\n\nfunction sanitizeInput(value: string): string {\n return value.replace(LATIN_AND_SPACE_REGEX, \"\").slice(0, INPUT_MAX_LENGTH);\n}\n\nfunction capitalizeFirstLetter(value: string): string {\n const t = value.trim();\n if (!t) return value;\n return t.charAt(0).toUpperCase() + t.slice(1);\n}\n\nexport interface StoryBuilderGameProps {\n config?: StoryBuilderConfig;\n questions: Question[];\n stories: Story[];\n onEvent?: (event: GameEvent) => void;\n}\n\nexport function StoryBuilder({\n config,\n questions: questionBank,\n stories,\n onEvent,\n}: StoryBuilderGameProps) {\n const [stage, setStageState] = useState<Stage>(\"lobby\");\n const [currentStory, setCurrentStory] = useState<Story | null>(null);\n const [currentQuestions, setCurrentQuestions] = useState<Question[]>([]);\n const [answers, setAnswersState] = useState<Answers>({});\n const [slideIndex, setSlideIndexState] = useState(0);\n\n const goToStage = useCallback(\n (next: Stage) => {\n setStageState(next);\n onEvent?.({ type: \"STAGE_CHANGE\", stage: next });\n },\n [onEvent]\n );\n\n const setAnswer = useCallback(\n (answerKey: string, value: string) => {\n setAnswersState((prev) => ({ ...prev, [answerKey]: value }));\n const q = currentQuestions.find((c) => c.answerKey === answerKey);\n if (q) onEvent?.({ type: \"ANSWER_SUBMIT\", questionId: q.id, value });\n },\n [currentQuestions, onEvent]\n );\n\n const nextSlide = useCallback(() => {\n if (!currentStory) return;\n const contentLength = currentStory.slides.length;\n if (slideIndex < contentLength - 1) {\n const next = slideIndex + 1;\n setSlideIndexState(next);\n onEvent?.({ type: \"SLIDE_CHANGE\", index: next });\n } else {\n if (currentStory.comprehensionQuestions?.length) {\n goToStage(\"comprehension\");\n } else {\n goToStage(\"end\");\n }\n }\n }, [currentStory, slideIndex, onEvent, goToStage]);\n\n const prevSlide = useCallback(() => {\n if (slideIndex > 0) {\n setSlideIndexState(slideIndex - 1);\n onEvent?.({ type: \"SLIDE_CHANGE\", index: slideIndex - 1 });\n }\n }, [slideIndex, onEvent]);\n\n const handleSelectStory = useCallback(\n (story: Story) => {\n const questionsForStory = getQuestionsForStory(questionBank, story);\n setCurrentStory(story);\n setCurrentQuestions(questionsForStory);\n setAnswersState({});\n setSlideIndexState(0);\n goToStage(\"questions\");\n },\n [questionBank, goToStage]\n );\n\n const handleQuestionsSubmit = useCallback(() => {\n goToStage(\"story\");\n }, [goToStage]);\n\n const handleTryAgain = useCallback(() => {\n if (!currentStory) return;\n const questionsForStory = getQuestionsForStory(questionBank, currentStory);\n setCurrentQuestions(questionsForStory);\n setAnswersState({});\n setSlideIndexState(0);\n goToStage(\"questions\");\n }, [currentStory, questionBank, goToStage]);\n\n const handleReturnToLobby = useCallback(() => {\n setCurrentStory(null);\n setCurrentQuestions([]);\n setAnswersState({});\n setSlideIndexState(0);\n goToStage(\"lobby\");\n }, [goToStage]);\n\n if (stage === \"lobby\") {\n return (\n <div className={`${cssStyles.root} ${cssStyles.lobbyScreen}`}>\n <div className={cssStyles.container}>\n <div className={cssStyles.lobbyContent}>\n <h1 className={cssStyles.headline1}>Story Builder</h1>\n <p className={cssStyles.headline2}>Practice English with your own story</p>\n <button\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.buttonLarge}`}\n onClick={() => goToStage(\"storySelect\")}\n >\n Play\n </button>\n </div>\n </div>\n </div>\n );\n }\n\n if (stage === \"storySelect\") {\n return (\n <div className={`${cssStyles.root} ${cssStyles.questionsScreen}`}>\n <div className={cssStyles.container}>\n <h2 className={cssStyles.headline2}>Choose a story</h2>\n <div className={cssStyles.storyList}>\n {stories.map((story) => (\n <button\n key={story.id}\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.buttonLarge}`}\n onClick={() => handleSelectStory(story)}\n >\n {story.title ?? story.id}\n </button>\n ))}\n </div>\n <button\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.storySelectBack}`}\n onClick={() => goToStage(\"lobby\")}\n >\n Back\n </button>\n </div>\n </div>\n );\n }\n\n if (stage === \"questions\") {\n const allFilled = currentQuestions.every(\n (q) => (answers[q.answerKey] ?? \"\").trim().length > 0\n );\n return (\n <div className={`${cssStyles.root} ${cssStyles.questionsScreen}`}>\n <div className={cssStyles.container}>\n <h2 className={cssStyles.headline2}>Answer a few questions</h2>\n <p className={cssStyles.inputHint}>Only Latin letters (A–Z) and spaces, max 40 characters per field.</p>\n {currentQuestions.map((q) => (\n <div key={q.id} className={cssStyles.inputGroup}>\n <label htmlFor={q.id}>{q.text}</label>\n <input\n id={q.id}\n type=\"text\"\n maxLength={INPUT_MAX_LENGTH}\n autoComplete=\"off\"\n placeholder={q.placeholder}\n value={answers[q.answerKey] ?? \"\"}\n onChange={(e) => {\n let v = sanitizeInput(e.target.value);\n if (q.answerKey === \"subject\") v = capitalizeFirstLetter(v);\n setAnswer(q.answerKey, v);\n }}\n />\n </div>\n ))}\n <button\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.buttonLarge}`}\n onClick={handleQuestionsSubmit}\n disabled={!allFilled}\n >\n Continue\n </button>\n </div>\n </div>\n );\n }\n\n if (stage === \"story\" && currentStory) {\n const slide = currentStory.slides[slideIndex];\n let speechBubbleClass = cssStyles.slideSpeechBubble;\n\n if (\n slide?.speechBubblePosition === \"center\" ||\n (slide?.speechBubblePosition === undefined &&\n slide?.id === \"s6\" &&\n currentStory.id === \"hero-1\")\n ) {\n speechBubbleClass = `${cssStyles.slideSpeechBubble} ${cssStyles.slideSpeechBubbleCenter}`;\n } else if (\n slide?.speechBubblePosition === \"bottom\" ||\n (slide?.speechBubblePosition === undefined &&\n slide?.id === \"s7\" &&\n currentStory.id === \"hero-1\")\n ) {\n speechBubbleClass = `${cssStyles.slideSpeechBubble} ${cssStyles.slideSpeechBubbleBottom}`;\n } else if (currentStory.id === \"hero-8\" && slide?.id === \"s6\") {\n // Финальный кадр School Concert: пузырь в верхнем левом углу, меньше по ширине\n speechBubbleClass = `${cssStyles.slideSpeechBubble} ${cssStyles.slideSpeechBubbleTopSmall}`;\n }\n return (\n <div className={cssStyles.root}>\n <div className={cssStyles.slideContainer}>\n <div className={cssStyles.slideFrame}>\n {/* Внутри кадра: фон + герой (единая сцена) */}\n {slide?.imageLayers?.length ? (\n <div className={cssStyles.slideLayers}>\n {slide.imageLayers.map((url, i) => {\n const isLast = i === slide.imageLayers!.length - 1;\n return (\n <img\n key={i}\n src={url}\n alt=\"\"\n className={isLast ? cssStyles.slideLayerCharacter : cssStyles.slideLayer}\n />\n );\n })}\n </div>\n ) : slide?.imageUrl ? (\n <img src={slide.imageUrl} alt=\"\" className={cssStyles.slideImage} />\n ) : (\n <div className={cssStyles.slideImage} aria-hidden />\n )}\n {/* Реплика: по slide.speechBubblePosition, с особыми случаями для hero-1 и финального кадра hero-8 */}\n <div className={speechBubbleClass} role=\"dialog\" aria-label=\"Hero says\">\n {slide ? renderText(slide.text, answers) : \"\"}\n </div>\n {/* Навигация внутри рамки, вторичная */}\n <div className={cssStyles.slideNav}>\n <button\n type=\"button\"\n className={cssStyles.slideNavBtn}\n onClick={prevSlide}\n disabled={slideIndex === 0}\n aria-label=\"Previous slide\"\n >\n <img src=\"/slide-prev.png\" alt=\"\" />\n </button>\n <button\n type=\"button\"\n className={cssStyles.slideNavBtn}\n onClick={nextSlide}\n aria-label=\"Next slide\"\n >\n <img src=\"/slide-next.png\" alt=\"\" />\n </button>\n </div>\n </div>\n </div>\n </div>\n );\n }\n\n if (stage === \"comprehension\" && currentStory?.comprehensionQuestions?.length) {\n const compImageUrl = currentStory.comprehensionImageUrl;\n return (\n <div className={cssStyles.root}>\n <div className={cssStyles.comprehensionSlide}>\n {compImageUrl ? (\n <img src={compImageUrl} alt=\"\" className={cssStyles.comprehensionSlideImage} />\n ) : (\n <div className={cssStyles.comprehensionSlideImage} aria-hidden />\n )}\n {!currentStory.comprehensionQuestionsOnImage && (\n <div className={cssStyles.comprehensionBubble} role=\"dialog\" aria-label=\"Comprehension questions\">\n <ul className={cssStyles.comprehensionBubbleList}>\n {currentStory.comprehensionQuestions.map((q) => (\n <li key={q.id}>{q.text}</li>\n ))}\n </ul>\n </div>\n )}\n <div className={cssStyles.comprehensionActionsOnly}>\n <button\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.buttonLarge}`}\n onClick={handleTryAgain}\n >\n PLAY AGAIN\n </button>\n <button\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.buttonLarge}`}\n onClick={handleReturnToLobby}\n >\n EXIT\n </button>\n </div>\n </div>\n </div>\n );\n }\n\n if (stage === \"end\") {\n return (\n <div className={cssStyles.root}>\n <div className={cssStyles.container}>\n <h1 className={cssStyles.headline1}>The End</h1>\n <div className={cssStyles.endActions}>\n <button\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.buttonLarge}`}\n onClick={handleTryAgain}\n >\n Try again\n </button>\n <button\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.buttonLarge}`}\n onClick={handleReturnToLobby}\n >\n Return to lobby\n </button>\n </div>\n </div>\n </div>\n );\n }\n\n return null;\n}\n"],"names":["getQuestionsForStory","questionBank","story","byId","q","id","byKey","list","key","DEFAULT_FALLBACKS","CAPITALIZE_KEYS","formatValue","raw","s","renderText","text","answers","_","value","INPUT_MAX_LENGTH","LATIN_AND_SPACE_REGEX","sanitizeInput","capitalizeFirstLetter","t","StoryBuilder","config","stories","onEvent","stage","setStageState","useState","currentStory","setCurrentStory","currentQuestions","setCurrentQuestions","setAnswersState","slideIndex","setSlideIndexState","goToStage","useCallback","next","setAnswer","answerKey","prev","c","nextSlide","contentLength","_a","prevSlide","handleSelectStory","questionsForStory","handleQuestionsSubmit","handleTryAgain","handleReturnToLobby","cssStyles","jsx","jsxs","allFilled","e","v","slide","speechBubbleClass","url","i","isLast","_b","compImageUrl"],"mappings":"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"}
@@ -0,0 +1,324 @@
1
+ import { jsx as n, jsxs as b } from "react/jsx-runtime";
2
+ import { useState as g, useCallback as _ } from "react";
3
+ function I(c, r) {
4
+ const u = new Map(c.map((o) => [o.id, o]));
5
+ if (r.questionIds && r.questionIds.length === r.inputKeys.length)
6
+ return r.questionIds.map((o) => {
7
+ const d = u.get(o);
8
+ if (!d) throw new Error(`Question not found: ${o}`);
9
+ return d;
10
+ });
11
+ const i = /* @__PURE__ */ new Map();
12
+ for (const o of c) {
13
+ const d = i.get(o.answerKey) ?? [];
14
+ d.push(o), i.set(o.answerKey, d);
15
+ }
16
+ return r.inputKeys.map((o) => {
17
+ const d = i.get(o);
18
+ if (!d || d.length === 0)
19
+ throw new Error(`No question found for answerKey: ${o}`);
20
+ return d[Math.floor(Math.random() * d.length)];
21
+ });
22
+ }
23
+ const P = {
24
+ name: "the hero",
25
+ goal: "a dream",
26
+ problem: "doubts",
27
+ place: "far away",
28
+ food: "treasure",
29
+ thing: "courage",
30
+ pet: "a friend",
31
+ father: "Dad",
32
+ mum: "Mum",
33
+ neighbour: "Mrs Brown",
34
+ friend: "a friend",
35
+ profession: "a hero",
36
+ enemy: "the dragon",
37
+ hobby: "dancing",
38
+ book: "about adventures",
39
+ subject: "nature",
40
+ librarian: "the librarian",
41
+ genre: "adventure",
42
+ style: "ballet"
43
+ }, k = /* @__PURE__ */ new Set([
44
+ "name",
45
+ "father",
46
+ "mum",
47
+ "dad",
48
+ "friend",
49
+ "enemy",
50
+ "groom",
51
+ "pet",
52
+ "coach",
53
+ "teacher",
54
+ "librarian",
55
+ "neighbour"
56
+ ]);
57
+ function H(c, r) {
58
+ const u = r.trim();
59
+ return u.length === 0 ? u : k.has(c) ? u.charAt(0).toUpperCase() + u.slice(1).toLowerCase() : u.toLowerCase();
60
+ }
61
+ function Q(c, r) {
62
+ return c.replace(/\{\{(\w+)\}\}/g, (u, i) => {
63
+ const o = r[i];
64
+ return o != null && o !== "" ? H(i, String(o)) : P[i] ?? "something";
65
+ });
66
+ }
67
+ const U = "_root_j1p5w_1", q = "_container_j1p5w_76", M = "_lobbyScreen_j1p5w_89", O = "_lobbyContent_j1p5w_96", D = "_storyList_j1p5w_103", R = "_button_j1p5w_116", z = "_storySelectBack_j1p5w_122", X = "_questionsScreen_j1p5w_126", Z = "_headline1_j1p5w_134", Y = "_headline2_j1p5w_142", V = "_inputHint_j1p5w_150", W = "_buttonLarge_j1p5w_186", J = "_inputGroup_j1p5w_192", E = "_slideContainer_j1p5w_224", ee = "_slideFrame_j1p5w_234", te = "_slideImage_j1p5w_242", ne = "_slideLayers_j1p5w_252", se = "_slideLayer_j1p5w_252", ie = "_slideLayerCharacter_j1p5w_272", oe = "_slideSpeechBubble_j1p5w_287", re = "_slideSpeechBubbleCenter_j1p5w_305", le = "_slideSpeechBubbleTopSmall_j1p5w_316", ae = "_slideSpeechBubbleBottom_j1p5w_321", ce = "_slideNav_j1p5w_333", de = "_slideNavBtn_j1p5w_350", ue = "_comprehensionSlide_j1p5w_386", he = "_comprehensionSlideImage_j1p5w_393", be = "_comprehensionBubble_j1p5w_416", pe = "_comprehensionBubbleList_j1p5w_444", me = "_comprehensionActionsOnly_j1p5w_469", _e = "_endActions_j1p5w_531", e = {
68
+ root: U,
69
+ container: q,
70
+ lobbyScreen: M,
71
+ lobbyContent: O,
72
+ storyList: D,
73
+ button: R,
74
+ storySelectBack: z,
75
+ questionsScreen: X,
76
+ headline1: Z,
77
+ headline2: Y,
78
+ inputHint: V,
79
+ buttonLarge: W,
80
+ inputGroup: J,
81
+ slideContainer: E,
82
+ slideFrame: ee,
83
+ slideImage: te,
84
+ slideLayers: ne,
85
+ slideLayer: se,
86
+ slideLayerCharacter: ie,
87
+ slideSpeechBubble: oe,
88
+ slideSpeechBubbleCenter: re,
89
+ slideSpeechBubbleTopSmall: le,
90
+ slideSpeechBubbleBottom: ae,
91
+ slideNav: ce,
92
+ slideNavBtn: de,
93
+ comprehensionSlide: ue,
94
+ comprehensionSlideImage: he,
95
+ comprehensionBubble: be,
96
+ comprehensionBubbleList: pe,
97
+ comprehensionActionsOnly: me,
98
+ endActions: _e
99
+ }, $ = 40, ye = /[^a-zA-Z ]/g;
100
+ function ge(c) {
101
+ return c.replace(ye, "").slice(0, $);
102
+ }
103
+ function Se(c) {
104
+ const r = c.trim();
105
+ return r ? r.charAt(0).toUpperCase() + r.slice(1) : c;
106
+ }
107
+ function fe({
108
+ config: c,
109
+ questions: r,
110
+ stories: u,
111
+ onEvent: i
112
+ }) {
113
+ var j, A;
114
+ const [o, d] = g("lobby"), [l, L] = g(null), [S, w] = g([]), [f, N] = g({}), [p, y] = g(0), a = _(
115
+ (t) => {
116
+ d(t), i == null || i({ type: "STAGE_CHANGE", stage: t });
117
+ },
118
+ [i]
119
+ ), v = _(
120
+ (t, s) => {
121
+ N((h) => ({ ...h, [t]: s }));
122
+ const m = S.find((h) => h.answerKey === t);
123
+ m && (i == null || i({ type: "ANSWER_SUBMIT", questionId: m.id, value: s }));
124
+ },
125
+ [S, i]
126
+ ), T = _(() => {
127
+ var s;
128
+ if (!l) return;
129
+ const t = l.slides.length;
130
+ if (p < t - 1) {
131
+ const m = p + 1;
132
+ y(m), i == null || i({ type: "SLIDE_CHANGE", index: m });
133
+ } else
134
+ (s = l.comprehensionQuestions) != null && s.length ? a("comprehension") : a("end");
135
+ }, [l, p, i, a]), x = _(() => {
136
+ p > 0 && (y(p - 1), i == null || i({ type: "SLIDE_CHANGE", index: p - 1 }));
137
+ }, [p, i]), F = _(
138
+ (t) => {
139
+ const s = I(r, t);
140
+ L(t), w(s), N({}), y(0), a("questions");
141
+ },
142
+ [r, a]
143
+ ), K = _(() => {
144
+ a("story");
145
+ }, [a]), B = _(() => {
146
+ if (!l) return;
147
+ const t = I(r, l);
148
+ w(t), N({}), y(0), a("questions");
149
+ }, [l, r, a]), C = _(() => {
150
+ L(null), w([]), N({}), y(0), a("lobby");
151
+ }, [a]);
152
+ if (o === "lobby")
153
+ return /* @__PURE__ */ n("div", { className: `${e.root} ${e.lobbyScreen}`, children: /* @__PURE__ */ n("div", { className: e.container, children: /* @__PURE__ */ b("div", { className: e.lobbyContent, children: [
154
+ /* @__PURE__ */ n("h1", { className: e.headline1, children: "Story Builder" }),
155
+ /* @__PURE__ */ n("p", { className: e.headline2, children: "Practice English with your own story" }),
156
+ /* @__PURE__ */ n(
157
+ "button",
158
+ {
159
+ type: "button",
160
+ className: `${e.button} ${e.buttonLarge}`,
161
+ onClick: () => a("storySelect"),
162
+ children: "Play"
163
+ }
164
+ )
165
+ ] }) }) });
166
+ if (o === "storySelect")
167
+ return /* @__PURE__ */ n("div", { className: `${e.root} ${e.questionsScreen}`, children: /* @__PURE__ */ b("div", { className: e.container, children: [
168
+ /* @__PURE__ */ n("h2", { className: e.headline2, children: "Choose a story" }),
169
+ /* @__PURE__ */ n("div", { className: e.storyList, children: u.map((t) => /* @__PURE__ */ n(
170
+ "button",
171
+ {
172
+ type: "button",
173
+ className: `${e.button} ${e.buttonLarge}`,
174
+ onClick: () => F(t),
175
+ children: t.title ?? t.id
176
+ },
177
+ t.id
178
+ )) }),
179
+ /* @__PURE__ */ n(
180
+ "button",
181
+ {
182
+ type: "button",
183
+ className: `${e.button} ${e.storySelectBack}`,
184
+ onClick: () => a("lobby"),
185
+ children: "Back"
186
+ }
187
+ )
188
+ ] }) });
189
+ if (o === "questions") {
190
+ const t = S.every(
191
+ (s) => (f[s.answerKey] ?? "").trim().length > 0
192
+ );
193
+ return /* @__PURE__ */ n("div", { className: `${e.root} ${e.questionsScreen}`, children: /* @__PURE__ */ b("div", { className: e.container, children: [
194
+ /* @__PURE__ */ n("h2", { className: e.headline2, children: "Answer a few questions" }),
195
+ /* @__PURE__ */ n("p", { className: e.inputHint, children: "Only Latin letters (A–Z) and spaces, max 40 characters per field." }),
196
+ S.map((s) => /* @__PURE__ */ b("div", { className: e.inputGroup, children: [
197
+ /* @__PURE__ */ n("label", { htmlFor: s.id, children: s.text }),
198
+ /* @__PURE__ */ n(
199
+ "input",
200
+ {
201
+ id: s.id,
202
+ type: "text",
203
+ maxLength: $,
204
+ autoComplete: "off",
205
+ placeholder: s.placeholder,
206
+ value: f[s.answerKey] ?? "",
207
+ onChange: (m) => {
208
+ let h = ge(m.target.value);
209
+ s.answerKey === "subject" && (h = Se(h)), v(s.answerKey, h);
210
+ }
211
+ }
212
+ )
213
+ ] }, s.id)),
214
+ /* @__PURE__ */ n(
215
+ "button",
216
+ {
217
+ type: "button",
218
+ className: `${e.button} ${e.buttonLarge}`,
219
+ onClick: K,
220
+ disabled: !t,
221
+ children: "Continue"
222
+ }
223
+ )
224
+ ] }) });
225
+ }
226
+ if (o === "story" && l) {
227
+ const t = l.slides[p];
228
+ let s = e.slideSpeechBubble;
229
+ return (t == null ? void 0 : t.speechBubblePosition) === "center" || (t == null ? void 0 : t.speechBubblePosition) === void 0 && (t == null ? void 0 : t.id) === "s6" && l.id === "hero-1" ? s = `${e.slideSpeechBubble} ${e.slideSpeechBubbleCenter}` : (t == null ? void 0 : t.speechBubblePosition) === "bottom" || (t == null ? void 0 : t.speechBubblePosition) === void 0 && (t == null ? void 0 : t.id) === "s7" && l.id === "hero-1" ? s = `${e.slideSpeechBubble} ${e.slideSpeechBubbleBottom}` : l.id === "hero-8" && (t == null ? void 0 : t.id) === "s6" && (s = `${e.slideSpeechBubble} ${e.slideSpeechBubbleTopSmall}`), /* @__PURE__ */ n("div", { className: e.root, children: /* @__PURE__ */ n("div", { className: e.slideContainer, children: /* @__PURE__ */ b("div", { className: e.slideFrame, children: [
230
+ (j = t == null ? void 0 : t.imageLayers) != null && j.length ? /* @__PURE__ */ n("div", { className: e.slideLayers, children: t.imageLayers.map((m, h) => {
231
+ const G = h === t.imageLayers.length - 1;
232
+ return /* @__PURE__ */ n(
233
+ "img",
234
+ {
235
+ src: m,
236
+ alt: "",
237
+ className: G ? e.slideLayerCharacter : e.slideLayer
238
+ },
239
+ h
240
+ );
241
+ }) }) : t != null && t.imageUrl ? /* @__PURE__ */ n("img", { src: t.imageUrl, alt: "", className: e.slideImage }) : /* @__PURE__ */ n("div", { className: e.slideImage, "aria-hidden": !0 }),
242
+ /* @__PURE__ */ n("div", { className: s, role: "dialog", "aria-label": "Hero says", children: t ? Q(t.text, f) : "" }),
243
+ /* @__PURE__ */ b("div", { className: e.slideNav, children: [
244
+ /* @__PURE__ */ n(
245
+ "button",
246
+ {
247
+ type: "button",
248
+ className: e.slideNavBtn,
249
+ onClick: x,
250
+ disabled: p === 0,
251
+ "aria-label": "Previous slide",
252
+ children: /* @__PURE__ */ n("img", { src: "/slide-prev.png", alt: "" })
253
+ }
254
+ ),
255
+ /* @__PURE__ */ n(
256
+ "button",
257
+ {
258
+ type: "button",
259
+ className: e.slideNavBtn,
260
+ onClick: T,
261
+ "aria-label": "Next slide",
262
+ children: /* @__PURE__ */ n("img", { src: "/slide-next.png", alt: "" })
263
+ }
264
+ )
265
+ ] })
266
+ ] }) }) });
267
+ }
268
+ if (o === "comprehension" && ((A = l == null ? void 0 : l.comprehensionQuestions) != null && A.length)) {
269
+ const t = l.comprehensionImageUrl;
270
+ return /* @__PURE__ */ n("div", { className: e.root, children: /* @__PURE__ */ b("div", { className: e.comprehensionSlide, children: [
271
+ t ? /* @__PURE__ */ n("img", { src: t, alt: "", className: e.comprehensionSlideImage }) : /* @__PURE__ */ n("div", { className: e.comprehensionSlideImage, "aria-hidden": !0 }),
272
+ !l.comprehensionQuestionsOnImage && /* @__PURE__ */ n("div", { className: e.comprehensionBubble, role: "dialog", "aria-label": "Comprehension questions", children: /* @__PURE__ */ n("ul", { className: e.comprehensionBubbleList, children: l.comprehensionQuestions.map((s) => /* @__PURE__ */ n("li", { children: s.text }, s.id)) }) }),
273
+ /* @__PURE__ */ b("div", { className: e.comprehensionActionsOnly, children: [
274
+ /* @__PURE__ */ n(
275
+ "button",
276
+ {
277
+ type: "button",
278
+ className: `${e.button} ${e.buttonLarge}`,
279
+ onClick: B,
280
+ children: "PLAY AGAIN"
281
+ }
282
+ ),
283
+ /* @__PURE__ */ n(
284
+ "button",
285
+ {
286
+ type: "button",
287
+ className: `${e.button} ${e.buttonLarge}`,
288
+ onClick: C,
289
+ children: "EXIT"
290
+ }
291
+ )
292
+ ] })
293
+ ] }) });
294
+ }
295
+ return o === "end" ? /* @__PURE__ */ n("div", { className: e.root, children: /* @__PURE__ */ b("div", { className: e.container, children: [
296
+ /* @__PURE__ */ n("h1", { className: e.headline1, children: "The End" }),
297
+ /* @__PURE__ */ b("div", { className: e.endActions, children: [
298
+ /* @__PURE__ */ n(
299
+ "button",
300
+ {
301
+ type: "button",
302
+ className: `${e.button} ${e.buttonLarge}`,
303
+ onClick: B,
304
+ children: "Try again"
305
+ }
306
+ ),
307
+ /* @__PURE__ */ n(
308
+ "button",
309
+ {
310
+ type: "button",
311
+ className: `${e.button} ${e.buttonLarge}`,
312
+ onClick: C,
313
+ children: "Return to lobby"
314
+ }
315
+ )
316
+ ] })
317
+ ] }) }) : null;
318
+ }
319
+ export {
320
+ fe as StoryBuilder,
321
+ I as getQuestionsForStory,
322
+ Q as renderText
323
+ };
324
+ //# sourceMappingURL=speakid-story-builder.es.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"speakid-story-builder.es.js","sources":["../src/utils/getQuestionsForStory.ts","../src/utils/renderText.ts","../src/components/StoryBuilderGame.tsx"],"sourcesContent":["import type { Question, Story } from \"../types\";\n\n/**\n * For each key in story.inputKeys, pick one question:\n * - If story.questionIds is set (same length as inputKeys), use those question ids from the bank.\n * - Otherwise pick one random question per answerKey.\n */\nexport function getQuestionsForStory(\n questionBank: Question[],\n story: Story\n): Question[] {\n const byId = new Map(questionBank.map((q) => [q.id, q]));\n\n if (story.questionIds && story.questionIds.length === story.inputKeys.length) {\n return story.questionIds.map((id) => {\n const q = byId.get(id);\n if (!q) throw new Error(`Question not found: ${id}`);\n return q;\n });\n }\n\n const byKey = new Map<string, Question[]>();\n for (const q of questionBank) {\n const list = byKey.get(q.answerKey) ?? [];\n list.push(q);\n byKey.set(q.answerKey, list);\n }\n\n return story.inputKeys.map((key) => {\n const list = byKey.get(key);\n if (!list || list.length === 0) {\n throw new Error(`No question found for answerKey: ${key}`);\n }\n return list[Math.floor(Math.random() * list.length)];\n });\n}\n","/** Нейтральные подстановки, чтобы пользователь никогда не видел «дырки» в тексте */\nconst DEFAULT_FALLBACKS: Record<string, string> = {\n name: \"the hero\",\n goal: \"a dream\",\n problem: \"doubts\",\n place: \"far away\",\n food: \"treasure\",\n thing: \"courage\",\n pet: \"a friend\",\n father: \"Dad\",\n mum: \"Mum\",\n neighbour: \"Mrs Brown\",\n friend: \"a friend\",\n profession: \"a hero\",\n enemy: \"the dragon\",\n hobby: \"dancing\",\n book: \"about adventures\",\n subject: \"nature\",\n librarian: \"the librarian\",\n genre: \"adventure\",\n style: \"ballet\",\n};\n\n/** Имена и обращения — всегда с большой буквы; профессия, еда, хобби — lower case */\nconst CAPITALIZE_KEYS = new Set([\n \"name\",\n \"father\",\n \"mum\",\n \"dad\",\n \"friend\",\n \"enemy\",\n \"groom\",\n \"pet\",\n \"coach\",\n \"teacher\",\n \"librarian\",\n \"neighbour\",\n]);\n\nfunction formatValue(key: string, raw: string): string {\n const s = raw.trim();\n if (s.length === 0) return s;\n if (CAPITALIZE_KEYS.has(key)) {\n return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();\n }\n return s.toLowerCase();\n}\n\n/**\n * Replaces placeholders {{key}} in text with values from answers.\n * name, father, friend, enemy → first letter uppercase; profession, food → lowercase.\n * Missing key → neutral fallback (never empty), so the sentence always reads well.\n */\nexport function renderText(text: string, answers: Record<string, string>): string {\n return text.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key: string) => {\n const value = answers[key];\n if (value != null && value !== \"\") return formatValue(key, String(value));\n return DEFAULT_FALLBACKS[key] ?? \"something\";\n });\n}\n","import React, { useState, useCallback } from \"react\";\nimport type { Question, Story, Answers, Stage, GameEvent, StoryBuilderConfig } from \"../types\";\nimport { getQuestionsForStory } from \"../utils/getQuestionsForStory\";\nimport { renderText } from \"../utils/renderText\";\nimport cssStyles from \"./StoryBuilder.module.css\";\n\nconst INPUT_MAX_LENGTH = 40;\nconst LATIN_AND_SPACE_REGEX = /[^a-zA-Z ]/g;\n\nfunction sanitizeInput(value: string): string {\n return value.replace(LATIN_AND_SPACE_REGEX, \"\").slice(0, INPUT_MAX_LENGTH);\n}\n\nfunction capitalizeFirstLetter(value: string): string {\n const t = value.trim();\n if (!t) return value;\n return t.charAt(0).toUpperCase() + t.slice(1);\n}\n\nexport interface StoryBuilderGameProps {\n config?: StoryBuilderConfig;\n questions: Question[];\n stories: Story[];\n onEvent?: (event: GameEvent) => void;\n}\n\nexport function StoryBuilder({\n config,\n questions: questionBank,\n stories,\n onEvent,\n}: StoryBuilderGameProps) {\n const [stage, setStageState] = useState<Stage>(\"lobby\");\n const [currentStory, setCurrentStory] = useState<Story | null>(null);\n const [currentQuestions, setCurrentQuestions] = useState<Question[]>([]);\n const [answers, setAnswersState] = useState<Answers>({});\n const [slideIndex, setSlideIndexState] = useState(0);\n\n const goToStage = useCallback(\n (next: Stage) => {\n setStageState(next);\n onEvent?.({ type: \"STAGE_CHANGE\", stage: next });\n },\n [onEvent]\n );\n\n const setAnswer = useCallback(\n (answerKey: string, value: string) => {\n setAnswersState((prev) => ({ ...prev, [answerKey]: value }));\n const q = currentQuestions.find((c) => c.answerKey === answerKey);\n if (q) onEvent?.({ type: \"ANSWER_SUBMIT\", questionId: q.id, value });\n },\n [currentQuestions, onEvent]\n );\n\n const nextSlide = useCallback(() => {\n if (!currentStory) return;\n const contentLength = currentStory.slides.length;\n if (slideIndex < contentLength - 1) {\n const next = slideIndex + 1;\n setSlideIndexState(next);\n onEvent?.({ type: \"SLIDE_CHANGE\", index: next });\n } else {\n if (currentStory.comprehensionQuestions?.length) {\n goToStage(\"comprehension\");\n } else {\n goToStage(\"end\");\n }\n }\n }, [currentStory, slideIndex, onEvent, goToStage]);\n\n const prevSlide = useCallback(() => {\n if (slideIndex > 0) {\n setSlideIndexState(slideIndex - 1);\n onEvent?.({ type: \"SLIDE_CHANGE\", index: slideIndex - 1 });\n }\n }, [slideIndex, onEvent]);\n\n const handleSelectStory = useCallback(\n (story: Story) => {\n const questionsForStory = getQuestionsForStory(questionBank, story);\n setCurrentStory(story);\n setCurrentQuestions(questionsForStory);\n setAnswersState({});\n setSlideIndexState(0);\n goToStage(\"questions\");\n },\n [questionBank, goToStage]\n );\n\n const handleQuestionsSubmit = useCallback(() => {\n goToStage(\"story\");\n }, [goToStage]);\n\n const handleTryAgain = useCallback(() => {\n if (!currentStory) return;\n const questionsForStory = getQuestionsForStory(questionBank, currentStory);\n setCurrentQuestions(questionsForStory);\n setAnswersState({});\n setSlideIndexState(0);\n goToStage(\"questions\");\n }, [currentStory, questionBank, goToStage]);\n\n const handleReturnToLobby = useCallback(() => {\n setCurrentStory(null);\n setCurrentQuestions([]);\n setAnswersState({});\n setSlideIndexState(0);\n goToStage(\"lobby\");\n }, [goToStage]);\n\n if (stage === \"lobby\") {\n return (\n <div className={`${cssStyles.root} ${cssStyles.lobbyScreen}`}>\n <div className={cssStyles.container}>\n <div className={cssStyles.lobbyContent}>\n <h1 className={cssStyles.headline1}>Story Builder</h1>\n <p className={cssStyles.headline2}>Practice English with your own story</p>\n <button\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.buttonLarge}`}\n onClick={() => goToStage(\"storySelect\")}\n >\n Play\n </button>\n </div>\n </div>\n </div>\n );\n }\n\n if (stage === \"storySelect\") {\n return (\n <div className={`${cssStyles.root} ${cssStyles.questionsScreen}`}>\n <div className={cssStyles.container}>\n <h2 className={cssStyles.headline2}>Choose a story</h2>\n <div className={cssStyles.storyList}>\n {stories.map((story) => (\n <button\n key={story.id}\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.buttonLarge}`}\n onClick={() => handleSelectStory(story)}\n >\n {story.title ?? story.id}\n </button>\n ))}\n </div>\n <button\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.storySelectBack}`}\n onClick={() => goToStage(\"lobby\")}\n >\n Back\n </button>\n </div>\n </div>\n );\n }\n\n if (stage === \"questions\") {\n const allFilled = currentQuestions.every(\n (q) => (answers[q.answerKey] ?? \"\").trim().length > 0\n );\n return (\n <div className={`${cssStyles.root} ${cssStyles.questionsScreen}`}>\n <div className={cssStyles.container}>\n <h2 className={cssStyles.headline2}>Answer a few questions</h2>\n <p className={cssStyles.inputHint}>Only Latin letters (A–Z) and spaces, max 40 characters per field.</p>\n {currentQuestions.map((q) => (\n <div key={q.id} className={cssStyles.inputGroup}>\n <label htmlFor={q.id}>{q.text}</label>\n <input\n id={q.id}\n type=\"text\"\n maxLength={INPUT_MAX_LENGTH}\n autoComplete=\"off\"\n placeholder={q.placeholder}\n value={answers[q.answerKey] ?? \"\"}\n onChange={(e) => {\n let v = sanitizeInput(e.target.value);\n if (q.answerKey === \"subject\") v = capitalizeFirstLetter(v);\n setAnswer(q.answerKey, v);\n }}\n />\n </div>\n ))}\n <button\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.buttonLarge}`}\n onClick={handleQuestionsSubmit}\n disabled={!allFilled}\n >\n Continue\n </button>\n </div>\n </div>\n );\n }\n\n if (stage === \"story\" && currentStory) {\n const slide = currentStory.slides[slideIndex];\n let speechBubbleClass = cssStyles.slideSpeechBubble;\n\n if (\n slide?.speechBubblePosition === \"center\" ||\n (slide?.speechBubblePosition === undefined &&\n slide?.id === \"s6\" &&\n currentStory.id === \"hero-1\")\n ) {\n speechBubbleClass = `${cssStyles.slideSpeechBubble} ${cssStyles.slideSpeechBubbleCenter}`;\n } else if (\n slide?.speechBubblePosition === \"bottom\" ||\n (slide?.speechBubblePosition === undefined &&\n slide?.id === \"s7\" &&\n currentStory.id === \"hero-1\")\n ) {\n speechBubbleClass = `${cssStyles.slideSpeechBubble} ${cssStyles.slideSpeechBubbleBottom}`;\n } else if (currentStory.id === \"hero-8\" && slide?.id === \"s6\") {\n // Финальный кадр School Concert: пузырь в верхнем левом углу, меньше по ширине\n speechBubbleClass = `${cssStyles.slideSpeechBubble} ${cssStyles.slideSpeechBubbleTopSmall}`;\n }\n return (\n <div className={cssStyles.root}>\n <div className={cssStyles.slideContainer}>\n <div className={cssStyles.slideFrame}>\n {/* Внутри кадра: фон + герой (единая сцена) */}\n {slide?.imageLayers?.length ? (\n <div className={cssStyles.slideLayers}>\n {slide.imageLayers.map((url, i) => {\n const isLast = i === slide.imageLayers!.length - 1;\n return (\n <img\n key={i}\n src={url}\n alt=\"\"\n className={isLast ? cssStyles.slideLayerCharacter : cssStyles.slideLayer}\n />\n );\n })}\n </div>\n ) : slide?.imageUrl ? (\n <img src={slide.imageUrl} alt=\"\" className={cssStyles.slideImage} />\n ) : (\n <div className={cssStyles.slideImage} aria-hidden />\n )}\n {/* Реплика: по slide.speechBubblePosition, с особыми случаями для hero-1 и финального кадра hero-8 */}\n <div className={speechBubbleClass} role=\"dialog\" aria-label=\"Hero says\">\n {slide ? renderText(slide.text, answers) : \"\"}\n </div>\n {/* Навигация внутри рамки, вторичная */}\n <div className={cssStyles.slideNav}>\n <button\n type=\"button\"\n className={cssStyles.slideNavBtn}\n onClick={prevSlide}\n disabled={slideIndex === 0}\n aria-label=\"Previous slide\"\n >\n <img src=\"/slide-prev.png\" alt=\"\" />\n </button>\n <button\n type=\"button\"\n className={cssStyles.slideNavBtn}\n onClick={nextSlide}\n aria-label=\"Next slide\"\n >\n <img src=\"/slide-next.png\" alt=\"\" />\n </button>\n </div>\n </div>\n </div>\n </div>\n );\n }\n\n if (stage === \"comprehension\" && currentStory?.comprehensionQuestions?.length) {\n const compImageUrl = currentStory.comprehensionImageUrl;\n return (\n <div className={cssStyles.root}>\n <div className={cssStyles.comprehensionSlide}>\n {compImageUrl ? (\n <img src={compImageUrl} alt=\"\" className={cssStyles.comprehensionSlideImage} />\n ) : (\n <div className={cssStyles.comprehensionSlideImage} aria-hidden />\n )}\n {!currentStory.comprehensionQuestionsOnImage && (\n <div className={cssStyles.comprehensionBubble} role=\"dialog\" aria-label=\"Comprehension questions\">\n <ul className={cssStyles.comprehensionBubbleList}>\n {currentStory.comprehensionQuestions.map((q) => (\n <li key={q.id}>{q.text}</li>\n ))}\n </ul>\n </div>\n )}\n <div className={cssStyles.comprehensionActionsOnly}>\n <button\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.buttonLarge}`}\n onClick={handleTryAgain}\n >\n PLAY AGAIN\n </button>\n <button\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.buttonLarge}`}\n onClick={handleReturnToLobby}\n >\n EXIT\n </button>\n </div>\n </div>\n </div>\n );\n }\n\n if (stage === \"end\") {\n return (\n <div className={cssStyles.root}>\n <div className={cssStyles.container}>\n <h1 className={cssStyles.headline1}>The End</h1>\n <div className={cssStyles.endActions}>\n <button\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.buttonLarge}`}\n onClick={handleTryAgain}\n >\n Try again\n </button>\n <button\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.buttonLarge}`}\n onClick={handleReturnToLobby}\n >\n Return to lobby\n </button>\n </div>\n </div>\n </div>\n );\n }\n\n return null;\n}\n"],"names":["getQuestionsForStory","questionBank","story","byId","q","id","byKey","list","key","DEFAULT_FALLBACKS","CAPITALIZE_KEYS","formatValue","raw","s","renderText","text","answers","_","value","INPUT_MAX_LENGTH","LATIN_AND_SPACE_REGEX","sanitizeInput","capitalizeFirstLetter","t","StoryBuilder","config","stories","onEvent","stage","setStageState","useState","currentStory","setCurrentStory","currentQuestions","setCurrentQuestions","setAnswersState","slideIndex","setSlideIndexState","goToStage","useCallback","next","setAnswer","answerKey","prev","c","nextSlide","contentLength","_a","prevSlide","handleSelectStory","questionsForStory","handleQuestionsSubmit","handleTryAgain","handleReturnToLobby","cssStyles","jsx","jsxs","allFilled","e","v","slide","speechBubbleClass","url","i","isLast","_b","compImageUrl"],"mappings":";;AAOO,SAASA,EACdC,GACAC,GACY;AACZ,QAAMC,IAAO,IAAI,IAAIF,EAAa,IAAI,CAACG,MAAM,CAACA,EAAE,IAAIA,CAAC,CAAC,CAAC;AAEvD,MAAIF,EAAM,eAAeA,EAAM,YAAY,WAAWA,EAAM,UAAU;AACpE,WAAOA,EAAM,YAAY,IAAI,CAACG,MAAO;AACnC,YAAMD,IAAID,EAAK,IAAIE,CAAE;AACrB,UAAI,CAACD,EAAG,OAAM,IAAI,MAAM,uBAAuBC,CAAE,EAAE;AACnD,aAAOD;AAAA,IACT,CAAC;AAGH,QAAME,wBAAY,IAAA;AAClB,aAAWF,KAAKH,GAAc;AAC5B,UAAMM,IAAOD,EAAM,IAAIF,EAAE,SAAS,KAAK,CAAA;AACvC,IAAAG,EAAK,KAAKH,CAAC,GACXE,EAAM,IAAIF,EAAE,WAAWG,CAAI;AAAA,EAC7B;AAEA,SAAOL,EAAM,UAAU,IAAI,CAACM,MAAQ;AAClC,UAAMD,IAAOD,EAAM,IAAIE,CAAG;AAC1B,QAAI,CAACD,KAAQA,EAAK,WAAW;AAC3B,YAAM,IAAI,MAAM,oCAAoCC,CAAG,EAAE;AAE3D,WAAOD,EAAK,KAAK,MAAM,KAAK,WAAWA,EAAK,MAAM,CAAC;AAAA,EACrD,CAAC;AACH;AClCA,MAAME,IAA4C;AAAA,EAChD,MAAM;AAAA,EACN,MAAM;AAAA,EACN,SAAS;AAAA,EACT,OAAO;AAAA,EACP,MAAM;AAAA,EACN,OAAO;AAAA,EACP,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,KAAK;AAAA,EACL,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,OAAO;AAAA,EACP,OAAO;AAAA,EACP,MAAM;AAAA,EACN,SAAS;AAAA,EACT,WAAW;AAAA,EACX,OAAO;AAAA,EACP,OAAO;AACT,GAGMC,wBAAsB,IAAI;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,SAASC,EAAYH,GAAaI,GAAqB;AACrD,QAAMC,IAAID,EAAI,KAAA;AACd,SAAIC,EAAE,WAAW,IAAUA,IACvBH,EAAgB,IAAIF,CAAG,IAClBK,EAAE,OAAO,CAAC,EAAE,gBAAgBA,EAAE,MAAM,CAAC,EAAE,YAAA,IAEzCA,EAAE,YAAA;AACX;AAOO,SAASC,EAAWC,GAAcC,GAAyC;AAChF,SAAOD,EAAK,QAAQ,kBAAkB,CAACE,GAAGT,MAAgB;AACxD,UAAMU,IAAQF,EAAQR,CAAG;AACzB,WAAIU,KAAS,QAAQA,MAAU,KAAWP,EAAYH,GAAK,OAAOU,CAAK,CAAC,IACjET,EAAkBD,CAAG,KAAK;AAAA,EACnC,CAAC;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GCrDMW,IAAmB,IACnBC,KAAwB;AAE9B,SAASC,GAAcH,GAAuB;AAC5C,SAAOA,EAAM,QAAQE,IAAuB,EAAE,EAAE,MAAM,GAAGD,CAAgB;AAC3E;AAEA,SAASG,GAAsBJ,GAAuB;AACpD,QAAMK,IAAIL,EAAM,KAAA;AAChB,SAAKK,IACEA,EAAE,OAAO,CAAC,EAAE,gBAAgBA,EAAE,MAAM,CAAC,IAD7BL;AAEjB;AASO,SAASM,GAAa;AAAA,EAC3B,QAAAC;AAAA,EACA,WAAWxB;AAAA,EACX,SAAAyB;AAAA,EACA,SAAAC;AACF,GAA0B;;AACxB,QAAM,CAACC,GAAOC,CAAa,IAAIC,EAAgB,OAAO,GAChD,CAACC,GAAcC,CAAe,IAAIF,EAAuB,IAAI,GAC7D,CAACG,GAAkBC,CAAmB,IAAIJ,EAAqB,CAAA,CAAE,GACjE,CAACd,GAASmB,CAAe,IAAIL,EAAkB,CAAA,CAAE,GACjD,CAACM,GAAYC,CAAkB,IAAIP,EAAS,CAAC,GAE7CQ,IAAYC;AAAA,IAChB,CAACC,MAAgB;AACf,MAAAX,EAAcW,CAAI,GAClBb,KAAA,QAAAA,EAAU,EAAE,MAAM,gBAAgB,OAAOa;IAC3C;AAAA,IACA,CAACb,CAAO;AAAA,EAAA,GAGJc,IAAYF;AAAA,IAChB,CAACG,GAAmBxB,MAAkB;AACpC,MAAAiB,EAAgB,CAACQ,OAAU,EAAE,GAAGA,GAAM,CAACD,CAAS,GAAGxB,EAAA,EAAQ;AAC3D,YAAMd,IAAI6B,EAAiB,KAAK,CAACW,MAAMA,EAAE,cAAcF,CAAS;AAChE,MAAItC,qBAAa,EAAE,MAAM,iBAAiB,YAAYA,EAAE,IAAI,OAAAc;IAC9D;AAAA,IACA,CAACe,GAAkBN,CAAO;AAAA,EAAA,GAGtBkB,IAAYN,EAAY,MAAM;;AAClC,QAAI,CAACR,EAAc;AACnB,UAAMe,IAAgBf,EAAa,OAAO;AAC1C,QAAIK,IAAaU,IAAgB,GAAG;AAClC,YAAMN,IAAOJ,IAAa;AAC1B,MAAAC,EAAmBG,CAAI,GACvBb,KAAA,QAAAA,EAAU,EAAE,MAAM,gBAAgB,OAAOa;IAC3C;AACE,OAAIO,IAAAhB,EAAa,2BAAb,QAAAgB,EAAqC,SACvCT,EAAU,eAAe,IAEzBA,EAAU,KAAK;AAAA,EAGrB,GAAG,CAACP,GAAcK,GAAYT,GAASW,CAAS,CAAC,GAE3CU,IAAYT,EAAY,MAAM;AAClC,IAAIH,IAAa,MACfC,EAAmBD,IAAa,CAAC,GACjCT,KAAA,QAAAA,EAAU,EAAE,MAAM,gBAAgB,OAAOS,IAAa;EAE1D,GAAG,CAACA,GAAYT,CAAO,CAAC,GAElBsB,IAAoBV;AAAA,IACxB,CAACrC,MAAiB;AAChB,YAAMgD,IAAoBlD,EAAqBC,GAAcC,CAAK;AAClE,MAAA8B,EAAgB9B,CAAK,GACrBgC,EAAoBgB,CAAiB,GACrCf,EAAgB,CAAA,CAAE,GAClBE,EAAmB,CAAC,GACpBC,EAAU,WAAW;AAAA,IACvB;AAAA,IACA,CAACrC,GAAcqC,CAAS;AAAA,EAAA,GAGpBa,IAAwBZ,EAAY,MAAM;AAC9C,IAAAD,EAAU,OAAO;AAAA,EACnB,GAAG,CAACA,CAAS,CAAC,GAERc,IAAiBb,EAAY,MAAM;AACvC,QAAI,CAACR,EAAc;AACnB,UAAMmB,IAAoBlD,EAAqBC,GAAc8B,CAAY;AACzE,IAAAG,EAAoBgB,CAAiB,GACrCf,EAAgB,CAAA,CAAE,GAClBE,EAAmB,CAAC,GACpBC,EAAU,WAAW;AAAA,EACvB,GAAG,CAACP,GAAc9B,GAAcqC,CAAS,CAAC,GAEpCe,IAAsBd,EAAY,MAAM;AAC5C,IAAAP,EAAgB,IAAI,GACpBE,EAAoB,CAAA,CAAE,GACtBC,EAAgB,CAAA,CAAE,GAClBE,EAAmB,CAAC,GACpBC,EAAU,OAAO;AAAA,EACnB,GAAG,CAACA,CAAS,CAAC;AAEd,MAAIV,MAAU;AACZ,6BACG,OAAA,EAAI,WAAW,GAAG0B,EAAU,IAAI,IAAIA,EAAU,WAAW,IACxD,UAAA,gBAAAC,EAAC,OAAA,EAAI,WAAWD,EAAU,WACxB,4BAAC,OAAA,EAAI,WAAWA,EAAU,cACxB,UAAA;AAAA,MAAA,gBAAAC,EAAC,MAAA,EAAG,WAAWD,EAAU,WAAW,UAAA,iBAAa;AAAA,MACjD,gBAAAC,EAAC,KAAA,EAAE,WAAWD,EAAU,WAAW,UAAA,wCAAoC;AAAA,MACvE,gBAAAC;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAK;AAAA,UACL,WAAW,GAAGD,EAAU,MAAM,IAAIA,EAAU,WAAW;AAAA,UACvD,SAAS,MAAMhB,EAAU,aAAa;AAAA,UACvC,UAAA;AAAA,QAAA;AAAA,MAAA;AAAA,IAED,EAAA,CACF,GACF,GACF;AAIJ,MAAIV,MAAU;AACZ,WACE,gBAAA2B,EAAC,OAAA,EAAI,WAAW,GAAGD,EAAU,IAAI,IAAIA,EAAU,eAAe,IAC5D,UAAA,gBAAAE,EAAC,OAAA,EAAI,WAAWF,EAAU,WACxB,UAAA;AAAA,MAAA,gBAAAC,EAAC,MAAA,EAAG,WAAWD,EAAU,WAAW,UAAA,kBAAc;AAAA,MAClD,gBAAAC,EAAC,SAAI,WAAWD,EAAU,WACvB,UAAA5B,EAAQ,IAAI,CAACxB,MACZ,gBAAAqD;AAAA,QAAC;AAAA,QAAA;AAAA,UAEC,MAAK;AAAA,UACL,WAAW,GAAGD,EAAU,MAAM,IAAIA,EAAU,WAAW;AAAA,UACvD,SAAS,MAAML,EAAkB/C,CAAK;AAAA,UAErC,UAAAA,EAAM,SAASA,EAAM;AAAA,QAAA;AAAA,QALjBA,EAAM;AAAA,MAAA,CAOd,GACH;AAAA,MACA,gBAAAqD;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAK;AAAA,UACL,WAAW,GAAGD,EAAU,MAAM,IAAIA,EAAU,eAAe;AAAA,UAC3D,SAAS,MAAMhB,EAAU,OAAO;AAAA,UACjC,UAAA;AAAA,QAAA;AAAA,MAAA;AAAA,IAED,EAAA,CACF,EAAA,CACF;AAIJ,MAAIV,MAAU,aAAa;AACzB,UAAM6B,IAAYxB,EAAiB;AAAA,MACjC,CAAC7B,OAAOY,EAAQZ,EAAE,SAAS,KAAK,IAAI,KAAA,EAAO,SAAS;AAAA,IAAA;AAEtD,WACE,gBAAAmD,EAAC,OAAA,EAAI,WAAW,GAAGD,EAAU,IAAI,IAAIA,EAAU,eAAe,IAC5D,UAAA,gBAAAE,EAAC,OAAA,EAAI,WAAWF,EAAU,WACxB,UAAA;AAAA,MAAA,gBAAAC,EAAC,MAAA,EAAG,WAAWD,EAAU,WAAW,UAAA,0BAAsB;AAAA,MAC1D,gBAAAC,EAAC,KAAA,EAAE,WAAWD,EAAU,WAAW,UAAA,qEAAiE;AAAA,MACnGrB,EAAiB,IAAI,CAAC7B,wBACpB,OAAA,EAAe,WAAWkD,EAAU,YACnC,UAAA;AAAA,QAAA,gBAAAC,EAAC,SAAA,EAAM,SAASnD,EAAE,IAAK,YAAE,MAAK;AAAA,QAC9B,gBAAAmD;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,IAAInD,EAAE;AAAA,YACN,MAAK;AAAA,YACL,WAAWe;AAAA,YACX,cAAa;AAAA,YACb,aAAaf,EAAE;AAAA,YACf,OAAOY,EAAQZ,EAAE,SAAS,KAAK;AAAA,YAC/B,UAAU,CAACsD,MAAM;AACjB,kBAAIC,IAAItC,GAAcqC,EAAE,OAAO,KAAK;AACpC,cAAItD,EAAE,cAAc,cAAWuD,IAAIrC,GAAsBqC,CAAC,IAC1DlB,EAAUrC,EAAE,WAAWuD,CAAC;AAAA,YAC1B;AAAA,UAAA;AAAA,QAAA;AAAA,MACA,KAdQvD,EAAE,EAeZ,CACD;AAAA,MACD,gBAAAmD;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAK;AAAA,UACL,WAAW,GAAGD,EAAU,MAAM,IAAIA,EAAU,WAAW;AAAA,UACvD,SAASH;AAAA,UACT,UAAU,CAACM;AAAA,UACZ,UAAA;AAAA,QAAA;AAAA,MAAA;AAAA,IAED,EAAA,CACF,EAAA,CACF;AAAA,EAEJ;AAEA,MAAI7B,MAAU,WAAWG,GAAc;AACrC,UAAM6B,IAAQ7B,EAAa,OAAOK,CAAU;AAC5C,QAAIyB,IAAoBP,EAAU;AAElC,YACEM,KAAA,gBAAAA,EAAO,0BAAyB,aAC/BA,KAAA,gBAAAA,EAAO,0BAAyB,WAC/BA,KAAA,gBAAAA,EAAO,QAAO,QACd7B,EAAa,OAAO,WAEtB8B,IAAoB,GAAGP,EAAU,iBAAiB,IAAIA,EAAU,uBAAuB,MAEvFM,KAAA,gBAAAA,EAAO,0BAAyB,aAC/BA,KAAA,gBAAAA,EAAO,0BAAyB,WAC/BA,KAAA,gBAAAA,EAAO,QAAO,QACd7B,EAAa,OAAO,WAEtB8B,IAAoB,GAAGP,EAAU,iBAAiB,IAAIA,EAAU,uBAAuB,KAC9EvB,EAAa,OAAO,aAAY6B,KAAA,gBAAAA,EAAO,QAAO,SAEvDC,IAAoB,GAAGP,EAAU,iBAAiB,IAAIA,EAAU,yBAAyB,KAGzF,gBAAAC,EAAC,OAAA,EAAI,WAAWD,EAAU,MACxB,UAAA,gBAAAC,EAAC,OAAA,EAAI,WAAWD,EAAU,gBACxB,UAAA,gBAAAE,EAAC,OAAA,EAAI,WAAWF,EAAU,YAEvB,UAAA;AAAA,OAAAP,IAAAa,KAAA,gBAAAA,EAAO,gBAAP,QAAAb,EAAoB,SACnB,gBAAAQ,EAAC,OAAA,EAAI,WAAWD,EAAU,aACvB,UAAAM,EAAM,YAAY,IAAI,CAACE,GAAKC,MAAM;AACjC,cAAMC,IAASD,MAAMH,EAAM,YAAa,SAAS;AACjD,eACE,gBAAAL;AAAA,UAAC;AAAA,UAAA;AAAA,YAEC,KAAKO;AAAA,YACL,KAAI;AAAA,YACJ,WAAWE,IAASV,EAAU,sBAAsBA,EAAU;AAAA,UAAA;AAAA,UAHzDS;AAAA,QAAA;AAAA,MAMX,CAAC,GACH,IACEH,KAAA,QAAAA,EAAO,WACT,gBAAAL,EAAC,OAAA,EAAI,KAAKK,EAAM,UAAU,KAAI,IAAG,WAAWN,EAAU,YAAY,IAElE,gBAAAC,EAAC,SAAI,WAAWD,EAAU,YAAY,eAAW,GAAA,CAAC;AAAA,MAGpD,gBAAAC,EAAC,OAAA,EAAI,WAAWM,GAAmB,MAAK,UAAS,cAAW,aACzD,UAAAD,IAAQ9C,EAAW8C,EAAM,MAAM5C,CAAO,IAAI,IAC7C;AAAA,MAEA,gBAAAwC,EAAC,OAAA,EAAI,WAAWF,EAAU,UACxB,UAAA;AAAA,QAAA,gBAAAC;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAK;AAAA,YACL,WAAWD,EAAU;AAAA,YACrB,SAASN;AAAA,YACT,UAAUZ,MAAe;AAAA,YACzB,cAAW;AAAA,YAEX,UAAA,gBAAAmB,EAAC,OAAA,EAAI,KAAI,mBAAkB,KAAI,GAAA,CAAG;AAAA,UAAA;AAAA,QAAA;AAAA,QAEpC,gBAAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAK;AAAA,YACL,WAAWD,EAAU;AAAA,YACrB,SAAST;AAAA,YACT,cAAW;AAAA,YAEX,UAAA,gBAAAU,EAAC,OAAA,EAAI,KAAI,mBAAkB,KAAI,GAAA,CAAG;AAAA,UAAA;AAAA,QAAA;AAAA,MACpC,EAAA,CACF;AAAA,IAAA,EAAA,CACF,GACF,GACF;AAAA,EAEJ;AAEA,MAAI3B,MAAU,qBAAmBqC,IAAAlC,KAAA,gBAAAA,EAAc,2BAAd,QAAAkC,EAAsC,SAAQ;AAC7E,UAAMC,IAAenC,EAAa;AAClC,WACE,gBAAAwB,EAAC,SAAI,WAAWD,EAAU,MACxB,UAAA,gBAAAE,EAAC,OAAA,EAAI,WAAWF,EAAU,oBACvB,UAAA;AAAA,MAAAY,sBACE,OAAA,EAAI,KAAKA,GAAc,KAAI,IAAG,WAAWZ,EAAU,wBAAA,CAAyB,sBAE5E,OAAA,EAAI,WAAWA,EAAU,yBAAyB,eAAW,IAAC;AAAA,MAEhE,CAACvB,EAAa,iCACb,gBAAAwB,EAAC,OAAA,EAAI,WAAWD,EAAU,qBAAqB,MAAK,UAAS,cAAW,2BACtE,4BAAC,MAAA,EAAG,WAAWA,EAAU,yBACtB,UAAAvB,EAAa,uBAAuB,IAAI,CAAC3B,MACxC,gBAAAmD,EAAC,MAAA,EAAe,UAAAnD,EAAE,QAATA,EAAE,EAAY,CACxB,GACH,GACF;AAAA,MAEF,gBAAAoD,EAAC,OAAA,EAAI,WAAWF,EAAU,0BACxB,UAAA;AAAA,QAAA,gBAAAC;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAK;AAAA,YACL,WAAW,GAAGD,EAAU,MAAM,IAAIA,EAAU,WAAW;AAAA,YACvD,SAASF;AAAA,YACV,UAAA;AAAA,UAAA;AAAA,QAAA;AAAA,QAGD,gBAAAG;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAK;AAAA,YACL,WAAW,GAAGD,EAAU,MAAM,IAAIA,EAAU,WAAW;AAAA,YACvD,SAASD;AAAA,YACV,UAAA;AAAA,UAAA;AAAA,QAAA;AAAA,MAED,EAAA,CACF;AAAA,IAAA,EAAA,CACF,EAAA,CACF;AAAA,EAEJ;AAEA,SAAIzB,MAAU,QAEV,gBAAA2B,EAAC,SAAI,WAAWD,EAAU,MACxB,UAAA,gBAAAE,EAAC,OAAA,EAAI,WAAWF,EAAU,WACxB,UAAA;AAAA,IAAA,gBAAAC,EAAC,MAAA,EAAG,WAAWD,EAAU,WAAW,UAAA,WAAO;AAAA,IAC3C,gBAAAE,EAAC,OAAA,EAAI,WAAWF,EAAU,YACxB,UAAA;AAAA,MAAA,gBAAAC;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAK;AAAA,UACL,WAAW,GAAGD,EAAU,MAAM,IAAIA,EAAU,WAAW;AAAA,UACvD,SAASF;AAAA,UACV,UAAA;AAAA,QAAA;AAAA,MAAA;AAAA,MAGD,gBAAAG;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAK;AAAA,UACL,WAAW,GAAGD,EAAU,MAAM,IAAIA,EAAU,WAAW;AAAA,UACvD,SAASD;AAAA,UACV,UAAA;AAAA,QAAA;AAAA,MAAA;AAAA,IAED,EAAA,CACF;AAAA,EAAA,EAAA,CACF,EAAA,CACF,IAIG;AACT;"}
package/dist/style.css ADDED
@@ -0,0 +1 @@
1
+ ._wrapperOuter_j1p5w_3{position:relative;width:100%;height:100%;overflow:hidden}._wrapperOuter_j1p5w_3 ._wrapperInner_j1p5w_10{position:absolute;left:50%;top:50%;box-sizing:border-box}._rotateScreen_j1p5w_18{width:100%;height:100vh;display:flex;justify-content:center;align-items:center;background-color:#fff7f5;flex-direction:column;text-align:center;padding:20px}._rotateScreenText_j1p5w_30{font-size:24px;font-weight:600;margin-bottom:12px;font-family:Onest,system-ui,sans-serif;color:#1f2937}._rotateScreenSubtext_j1p5w_38{font-size:16px;color:#6b7280;margin-bottom:24px}._rotateScreenIcon_j1p5w_44{font-size:48px;animation:_sbRotate_j1p5w_1 2s ease-in-out infinite}@keyframes _sbRotate_j1p5w_1{0%,to{transform:rotate(0)}50%{transform:rotate(90deg)}}._root_j1p5w_1{width:1280px;height:720px;position:relative;background-color:#fff7f5;overflow:hidden;font-family:Onest,system-ui,sans-serif;box-sizing:border-box}._root_j1p5w_1 *,._root_j1p5w_1 *:before,._root_j1p5w_1 *:after{box-sizing:border-box}._root_j1p5w_1 ._container_j1p5w_76{width:100%;height:100%;position:relative;display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center;padding:24px;overflow:hidden}._root_j1p5w_1._lobbyScreen_j1p5w_89{background-image:url(/lobby-hero-cave-dragon.png);background-size:cover;background-position:center;background-repeat:no-repeat}._root_j1p5w_1._lobbyScreen_j1p5w_89 ._lobbyContent_j1p5w_96{background:#fff7f5e0;padding:32px 48px;border-radius:16px;box-shadow:0 4px 24px #00000014}._root_j1p5w_1 ._storyList_j1p5w_103{display:grid;grid-template-columns:repeat(3,1fr);gap:12px 20px;align-items:center;justify-items:center;margin-top:16px;max-width:100%;max-height:420px;overflow-y:auto;padding:0 8px}._root_j1p5w_1 ._storyList_j1p5w_103 ._button_j1p5w_116{width:100%;max-width:320px;min-width:0}._root_j1p5w_1 ._storySelectBack_j1p5w_122{margin-top:24px}._root_j1p5w_1._questionsScreen_j1p5w_126{background-color:#fff7f5;background-image:url(/questions-bg.png);background-size:cover;background-position:center;background-repeat:no-repeat}._root_j1p5w_1 ._headline1_j1p5w_134{font-family:Onest,system-ui,sans-serif;font-size:32px;font-weight:600;color:#1f2937;margin-bottom:24px}._root_j1p5w_1 ._headline2_j1p5w_142{font-family:Onest,system-ui,sans-serif;font-size:24px;font-weight:500;color:#1f2937;margin-bottom:16px}._root_j1p5w_1 ._inputHint_j1p5w_150{font-size:14px;color:#6b7280;margin-bottom:16px}._root_j1p5w_1 ._headline3_j1p5w_156{font-family:Onest,system-ui,sans-serif;font-size:20px;font-weight:500;color:#1f2937;margin-bottom:12px}._root_j1p5w_1 ._button_j1p5w_116{font-family:Onest,system-ui,sans-serif;font-size:16px;padding:14px 36px;border-radius:10px;background:#f3f4f6;color:#1f2937;border:none;cursor:pointer;transition:all .2s ease}._root_j1p5w_1 ._button_j1p5w_116:hover{background:#ec4c44;color:#fff;transform:scale(1.05)}._root_j1p5w_1 ._button_j1p5w_116:active{transform:scale(.98)}._root_j1p5w_1 ._buttonLarge_j1p5w_186{padding:18px 24px;font-size:18px}._root_j1p5w_1 ._inputGroup_j1p5w_192{width:100%;max-width:560px;margin-bottom:16px;text-align:left}._root_j1p5w_1 ._inputGroup_j1p5w_192 label{display:block;font-size:14px;font-weight:500;color:#374151;margin-bottom:6px}._root_j1p5w_1 ._inputGroup_j1p5w_192 input{width:100%;font-family:Onest,system-ui,sans-serif;font-size:16px;padding:12px 16px;border:2px solid #e5e7eb;border-radius:10px;color:#1f2937;background:#fff}._root_j1p5w_1 ._inputGroup_j1p5w_192 input:focus{outline:none;border-color:#ec4c44}._root_j1p5w_1 ._slideContainer_j1p5w_224{width:100%;height:100%;display:flex;align-items:center;justify-content:center;padding:0}._root_j1p5w_1 ._slideFrame_j1p5w_234{position:relative;width:1280px;height:720px;overflow:hidden;border:2px solid #1f2937}._root_j1p5w_1 ._slideImage_j1p5w_242{position:absolute;top:0;left:0;width:100%;height:100%;background:#e5e7eb;object-fit:cover}._root_j1p5w_1 ._slideLayers_j1p5w_252{position:absolute;top:0;left:0;width:100%;height:100%;overflow:hidden;background:linear-gradient(to bottom,#87ceeb,#b0d8f0,#e8dcc4)}._root_j1p5w_1 ._slideLayer_j1p5w_252{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;object-fit:cover;display:block}._root_j1p5w_1 ._slideLayerCharacter_j1p5w_272{position:absolute;left:50%;bottom:-2%;transform:translate(-50%);width:auto;max-width:32%;height:88%;object-fit:contain;object-position:bottom center;display:block;filter:drop-shadow(0 6px 12px rgba(0,0,0,.35))}._root_j1p5w_1 ._slideSpeechBubble_j1p5w_287{position:absolute;left:20px;top:20px;z-index:2;max-width:55%;padding:12px 18px;background:#fff;border:2px solid #1f2937;border-radius:14px;font-size:17px;line-height:1.45;color:#1f2937;text-align:left;box-shadow:0 2px 6px #0000001a}._root_j1p5w_1 ._slideSpeechBubbleCenter_j1p5w_305{left:50%;top:50%;bottom:auto;transform:translate(-50%,-50%);max-width:70%;max-height:80%;overflow-y:auto;text-align:center}._root_j1p5w_1 ._slideSpeechBubbleTopSmall_j1p5w_316{max-width:42%}._root_j1p5w_1 ._slideSpeechBubbleBottom_j1p5w_321{left:50%;top:auto;bottom:64px;transform:translate(-50%);max-width:85%;max-height:35%;overflow-y:auto;text-align:center}._root_j1p5w_1 ._slideNav_j1p5w_333{position:absolute;bottom:24px;left:0;right:0;height:52px;display:flex;align-items:center;justify-content:space-between;padding:0 12px;pointer-events:none}._root_j1p5w_1 ._slideNav_j1p5w_333 button{pointer-events:auto}._root_j1p5w_1 ._slideNavBtn_j1p5w_350{padding:0;border:none;background:none;cursor:pointer;width:44px;height:44px;display:flex;align-items:center;justify-content:center;opacity:.85;transition:opacity .2s ease,transform .2s ease}._root_j1p5w_1 ._slideNavBtn_j1p5w_350:hover:not(:disabled){opacity:1;transform:scale(1.08)}._root_j1p5w_1 ._slideNavBtn_j1p5w_350:active:not(:disabled){transform:scale(.95)}._root_j1p5w_1 ._slideNavBtn_j1p5w_350:disabled{opacity:.35;cursor:not-allowed}._root_j1p5w_1 ._slideNavBtn_j1p5w_350 img{width:100%;height:100%;object-fit:contain;display:block}._root_j1p5w_1 ._comprehensionSlide_j1p5w_386{position:relative;width:100%;height:100%;overflow:hidden}._root_j1p5w_1 ._comprehensionSlideImage_j1p5w_393{position:absolute;top:0;left:0;width:100%;height:100%;background:linear-gradient(to bottom,#87ceeb,#b0d8f0,#e8dcc4);object-fit:cover}._root_j1p5w_1 ._comprehensionOverlay_j1p5w_403{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:48px 24px 100px;background:#fff7f5d1;text-align:center}._root_j1p5w_1 ._comprehensionBubble_j1p5w_416{position:absolute;left:50%;top:48px;transform:translate(-50%);z-index:2;max-width:min(520px,calc(100% - 48px));padding:24px 32px 28px;background:linear-gradient(165deg,#fffef9,#fdf8f0);border-radius:20px;box-shadow:0 4px 20px #00000014,0 0 0 1px #0000000f,inset 0 1px #ffffffe6;border:none}._root_j1p5w_1 ._comprehensionBubble_j1p5w_416:after{content:"";position:absolute;bottom:-12px;left:50%;transform:translate(-50%);border:14px solid transparent;border-top-color:#fdf8f0;filter:drop-shadow(0 2px 4px rgba(0,0,0,.06))}._root_j1p5w_1 ._comprehensionBubbleList_j1p5w_444{list-style:none;padding:0;margin:0;text-align:center}._root_j1p5w_1 ._comprehensionBubbleList_j1p5w_444 li{font-size:18px;font-weight:500;line-height:1.5;color:#2d3748;margin-bottom:12px;padding:0 8px}._root_j1p5w_1 ._comprehensionBubbleList_j1p5w_444 li:last-child{margin-bottom:0}._root_j1p5w_1 ._comprehensionBubbleList_j1p5w_444 li:before{content:none}._root_j1p5w_1 ._comprehensionActionsOnly_j1p5w_469{position:absolute;bottom:32px;left:50%;transform:translate(-50%);display:flex;gap:24px;z-index:2}._root_j1p5w_1 ._comprehensionTitle_j1p5w_479{font-family:Onest,system-ui,sans-serif;font-size:22px;font-weight:600;color:#1f2937;margin-bottom:24px}._root_j1p5w_1 ._comprehensionQuestions_j1p5w_487{list-style:none;padding:0;margin:0 0 32px;max-width:560px;text-align:left}._root_j1p5w_1 ._comprehensionQuestions_j1p5w_487 li{font-size:18px;line-height:1.5;color:#374151;margin-bottom:14px;padding-left:1.2em;position:relative}._root_j1p5w_1 ._comprehensionQuestions_j1p5w_487 li:before{content:"•";position:absolute;left:0;color:#ec4c44;font-weight:700}._root_j1p5w_1 ._comprehensionActions_j1p5w_469{display:flex;gap:24px;margin-top:auto}._root_j1p5w_1 ._comprehensionList_j1p5w_519{width:100%;max-width:560px;text-align:left;margin-bottom:24px}._root_j1p5w_1 ._comprehensionList_j1p5w_519 ._inputGroup_j1p5w_192{margin-bottom:20px}._root_j1p5w_1 ._endActions_j1p5w_531{display:flex;gap:24px;margin-top:24px}
@@ -0,0 +1,63 @@
1
+ /** Question from the bank: answerKey is used for substitution in stories */
2
+ export interface Question {
3
+ id: string;
4
+ answerKey: string;
5
+ text: string;
6
+ placeholder?: string;
7
+ }
8
+ /** Position of the speech bubble on the slide */
9
+ export type SpeechBubblePosition = "default" | "center" | "bottom";
10
+ export interface Slide {
11
+ id: string;
12
+ text: string;
13
+ imageUrl: string;
14
+ /** When set, slide image is composed from these layers (bottom to top). Takes precedence over imageUrl. */
15
+ imageLayers?: string[];
16
+ /** When set, overrides default (top-left) placement of the speech bubble. */
17
+ speechBubblePosition?: SpeechBubblePosition;
18
+ onEnter?: unknown[];
19
+ }
20
+ export interface ComprehensionQuestion {
21
+ id: string;
22
+ text: string;
23
+ }
24
+ /** Story is the main object: it declares inputKeys and comprehension */
25
+ export interface Story {
26
+ id: string;
27
+ /** Display name in lobby (optional; falls back to id) */
28
+ title?: string;
29
+ slides: Slide[];
30
+ inputKeys: string[];
31
+ /** If set, use these question ids in order (one per inputKey); otherwise pick at random by answerKey */
32
+ questionIds?: string[];
33
+ comprehensionQuestions?: ComprehensionQuestion[];
34
+ /** Optional background image for the comprehension screen (e.g. village with hero, no crowd) */
35
+ comprehensionImageUrl?: string;
36
+ /** When true, questions are drawn on the comprehension image; do not show the overlay bubble */
37
+ comprehensionQuestionsOnImage?: boolean;
38
+ }
39
+ /** Answers collected from user: answerKey → value */
40
+ export type Answers = Record<string, string>;
41
+ export type Stage = "lobby" | "storySelect" | "questions" | "story" | "comprehension" | "end";
42
+ export type GameEvent = {
43
+ type: "STAGE_CHANGE";
44
+ stage: Stage;
45
+ } | {
46
+ type: "ANSWER_SUBMIT";
47
+ questionId: string;
48
+ value: string;
49
+ } | {
50
+ type: "SLIDE_CHANGE";
51
+ index: number;
52
+ };
53
+ export interface StoryBuilderConfig {
54
+ /** Number of questions per round (default 5); first key is always name */
55
+ questionsPerRound?: number;
56
+ }
57
+ export interface StoryBuilderProps {
58
+ config?: StoryBuilderConfig;
59
+ questions: Question[];
60
+ stories: Story[];
61
+ onEvent?: (event: GameEvent) => void;
62
+ }
63
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,iDAAiD;AACjD,MAAM,MAAM,oBAAoB,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEnE,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,2GAA2G;IAC3G,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,6EAA6E;IAC7E,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IAC5C,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wEAAwE;AACxE,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,wGAAwG;IACxG,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,sBAAsB,CAAC,EAAE,qBAAqB,EAAE,CAAC;IACjD,gGAAgG;IAChG,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,gGAAgG;IAChG,6BAA6B,CAAC,EAAE,OAAO,CAAC;CACzC;AAED,qDAAqD;AACrD,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE7C,MAAM,MAAM,KAAK,GAAG,OAAO,GAAG,aAAa,GAAG,WAAW,GAAG,OAAO,GAAG,eAAe,GAAG,KAAK,CAAC;AAE9F,MAAM,MAAM,SAAS,GACjB;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,GACtC;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC5D;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5C,MAAM,WAAW,kBAAkB;IACjC,0EAA0E;IAC1E,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,OAAO,EAAE,KAAK,EAAE,CAAC;IACjB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;CACtC"}
@@ -0,0 +1,8 @@
1
+ import type { Question, Story } from "../types";
2
+ /**
3
+ * For each key in story.inputKeys, pick one question:
4
+ * - If story.questionIds is set (same length as inputKeys), use those question ids from the bank.
5
+ * - Otherwise pick one random question per answerKey.
6
+ */
7
+ export declare function getQuestionsForStory(questionBank: Question[], story: Story): Question[];
8
+ //# sourceMappingURL=getQuestionsForStory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getQuestionsForStory.d.ts","sourceRoot":"","sources":["../../src/utils/getQuestionsForStory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEhD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,QAAQ,EAAE,EACxB,KAAK,EAAE,KAAK,GACX,QAAQ,EAAE,CAyBZ"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Replaces placeholders {{key}} in text with values from answers.
3
+ * name, father, friend, enemy → first letter uppercase; profession, food → lowercase.
4
+ * Missing key → neutral fallback (never empty), so the sentence always reads well.
5
+ */
6
+ export declare function renderText(text: string, answers: Record<string, string>): string;
7
+ //# sourceMappingURL=renderText.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderText.d.ts","sourceRoot":"","sources":["../../src/utils/renderText.ts"],"names":[],"mappings":"AAgDA;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAMhF"}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "speakid-story-builder",
3
+ "version": "1.0.1",
4
+ "description": "Story Builder - Interactive story game for English practice (SPEAKID)",
5
+ "main": "dist/speakid-story-builder.cjs.js",
6
+ "module": "dist/speakid-story-builder.es.js",
7
+ "types": "dist/index.d.ts",
8
+ "sideEffects": false,
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/speakid-story-builder.es.js",
13
+ "require": "./dist/speakid-story-builder.cjs.js",
14
+ "default": "./dist/speakid-story-builder.es.js"
15
+ },
16
+ "./style.css": "./dist/style.css"
17
+ },
18
+ "style": "./dist/style.css",
19
+ "files": [
20
+ "dist",
21
+ "README.md"
22
+ ],
23
+ "scripts": {
24
+ "dev": "vite",
25
+ "build": "vite build && npm run build:types",
26
+ "build:types": "tsc --emitDeclarationOnly --outDir dist",
27
+ "prepublishOnly": "npm run build",
28
+ "preview": "vite preview"
29
+ },
30
+ "keywords": [
31
+ "speakid",
32
+ "game",
33
+ "education",
34
+ "story-builder",
35
+ "english",
36
+ "react",
37
+ "typescript"
38
+ ],
39
+ "author": "SPEAKID Team",
40
+ "license": "MIT",
41
+ "peerDependencies": {
42
+ "react": ">=18.2.0",
43
+ "react-dom": ">=18.2.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^24.9.1",
47
+ "@types/react": "^18.2.0",
48
+ "@types/react-dom": "^18.2.0",
49
+ "@vitejs/plugin-react": "^4.2.0",
50
+ "react": "^19.2.0",
51
+ "react-dom": "^19.2.0",
52
+ "typescript": "^5.2.0",
53
+ "vite": "^5.0.0"
54
+ }
55
+ }