@zomako/elearning-components 2.0.7 → 2.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zomako/elearning-components",
3
- "version": "2.0.7",
3
+ "version": "2.0.8",
4
4
  "description": "A library of interactive and SCORM-compliant eLearning components.",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
package/src/App.jsx CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  MatchingActivity,
7
7
  SortingActivity,
8
8
  DragAndDropActivity,
9
+ BranchingScenario,
9
10
  } from './index';
10
11
 
11
12
  const accordionItems = [
@@ -58,6 +59,70 @@ const dragDropTargets = [
58
59
  { id: 'target-es', accepts: ['madrid'], label: 'Capital of Spain' },
59
60
  ];
60
61
 
62
+ const branchingNodes = {
63
+ start: {
64
+ id: 'start',
65
+ type: 'scenario',
66
+ title: 'The Crossroads',
67
+ content:
68
+ 'You stand at a crossroads. The left path leads into a dark forest. The right path follows a river.',
69
+ choices: [
70
+ { id: 'left', text: 'Take the left path into the forest', nextNodeId: 'forest' },
71
+ { id: 'right', text: 'Follow the river', nextNodeId: 'river' },
72
+ ],
73
+ },
74
+ forest: {
75
+ id: 'forest',
76
+ type: 'scenario',
77
+ title: 'In the Forest',
78
+ content: 'You enter the forest. It gets darker. You hear a noise ahead.',
79
+ choices: [
80
+ {
81
+ id: 'investigate',
82
+ text: 'Investigate the noise',
83
+ nextNodeId: 'forest',
84
+ outcomes: [
85
+ {
86
+ id: 'friend',
87
+ text: 'You find a lost traveler. You gain a companion.',
88
+ nextNodeId: 'ending-friend',
89
+ scoreModifier: 10,
90
+ variableUpdates: { companion: true },
91
+ },
92
+ {
93
+ id: 'wolf',
94
+ text: 'A wolf appears. You retreat safely.',
95
+ nextNodeId: 'ending-forest',
96
+ scoreModifier: 0,
97
+ },
98
+ ],
99
+ },
100
+ { id: 'back', text: 'Go back to the crossroads', nextNodeId: 'start' },
101
+ ],
102
+ },
103
+ river: {
104
+ id: 'river',
105
+ type: 'ending',
106
+ title: 'By the River',
107
+ content: 'You follow the river and find a village. Your journey ends here.',
108
+ choices: [],
109
+ },
110
+ 'ending-friend': {
111
+ id: 'ending-friend',
112
+ type: 'ending',
113
+ title: 'Good Ending',
114
+ content: 'You and your companion reach safety. Well done!',
115
+ choices: [],
116
+ },
117
+ 'ending-forest': {
118
+ id: 'ending-forest',
119
+ type: 'ending',
120
+ title: 'Forest End',
121
+ content: 'You leave the forest and return to the road.',
122
+ choices: [],
123
+ },
124
+ };
125
+
61
126
  export default function App() {
62
127
  return (
63
128
  <div style={{ padding: '2rem', maxWidth: 800, margin: '0 auto' }}>
@@ -96,7 +161,7 @@ export default function App() {
96
161
  />
97
162
  </section>
98
163
 
99
- <section>
164
+ <section style={{ marginBottom: '2.5rem' }}>
100
165
  <h2>DragAndDropActivity</h2>
101
166
  <DragAndDropActivity
102
167
  items={dragDropItems}
@@ -104,6 +169,15 @@ export default function App() {
104
169
  onComplete={(result) => console.log('Drag and drop result:', result)}
105
170
  />
106
171
  </section>
172
+
173
+ <section>
174
+ <h2>BranchingScenario</h2>
175
+ <BranchingScenario
176
+ nodes={branchingNodes}
177
+ startNodeId="start"
178
+ onComplete={(result) => console.log('Branching scenario complete:', result)}
179
+ />
180
+ </section>
107
181
  </div>
108
182
  );
109
183
  }
@@ -0,0 +1,126 @@
1
+ # ResponsiveWrapper
2
+
3
+ A responsive layout container component that uses **CSS Grid** to provide a three-row structure (header, body, footer) with a scrollable body section. It uses **ResizeObserver** for dimension-based styling and **100dvh** for reliable viewport height on mobile browsers.
4
+
5
+ ## Purpose
6
+
7
+ ResponsiveWrapper is designed to:
8
+
9
+ - Provide a consistent, responsive layout for eLearning and web content that works across screen sizes.
10
+ - Keep the main content area scrollable when it exceeds the viewport, while preserving a fixed header/footer structure.
11
+ - Adapt styling for mobile (&lt; 600px) vs desktop (≥ 600px) using both **media queries** and **ResizeObserver** (so behavior is correct when the component is inside a constrained container, e.g. in an iframe or split view).
12
+ - Use **fluid typography and spacing** via `clamp()` so text and padding scale smoothly without separate breakpoints.
13
+ - Use **100dvh** (dynamic viewport height) so layout behaves correctly when mobile browser chrome (e.g. address bar) shows or hides.
14
+
15
+ ## Props
16
+
17
+ | Prop | Type | Default | Description |
18
+ | ---------- | ---------------- | --------- | ---------------------------------------------------------- |
19
+ | `children` | `React.ReactNode`| *required*| Content rendered inside the scrollable body section. |
20
+ | `maxWidth` | `string` | `"100%"` | Optional max-width for the wrapper container. |
21
+ | `padding` | `string` | `"1rem"` | Optional padding applied to the wrapper. |
22
+ | `gap` | `string` | `"1rem"` | Optional gap between grid rows (header, body, footer). |
23
+
24
+ ### TypeScript interface
25
+
26
+ ```ts
27
+ interface ResponsiveWrapperProps {
28
+ children: React.ReactNode;
29
+ maxWidth?: string; // default: "100%"
30
+ padding?: string; // default: "1rem"
31
+ gap?: string; // default: "1rem"
32
+ }
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ ### Basic
38
+
39
+ Wrap any content in the scrollable body:
40
+
41
+ ```tsx
42
+ import { ResponsiveWrapper } from '@zomako/elearning-components';
43
+
44
+ function App() {
45
+ return (
46
+ <ResponsiveWrapper>
47
+ <p>Your main content here. This area will scroll if it exceeds the viewport.</p>
48
+ </ResponsiveWrapper>
49
+ );
50
+ }
51
+ ```
52
+
53
+ ### With custom layout options
54
+
55
+ ```tsx
56
+ <ResponsiveWrapper
57
+ maxWidth="720px"
58
+ padding="1.5rem"
59
+ gap="1.25rem"
60
+ >
61
+ <article>
62
+ <h1>Lesson title</h1>
63
+ <p>Long content…</p>
64
+ </article>
65
+ </ResponsiveWrapper>
66
+ ```
67
+
68
+ ### Wrapping other components
69
+
70
+ ```tsx
71
+ <ResponsiveWrapper maxWidth="min(90vw, 800px)">
72
+ <Accordion items={items} />
73
+ </ResponsiveWrapper>
74
+ ```
75
+
76
+ ## Responsive behavior
77
+
78
+ - **Viewport height**: The wrapper uses `height: 100dvh` and `max-height: 100dvh` so it fills the dynamic viewport and adapts when the mobile browser UI (e.g. address bar) appears or disappears.
79
+ - **Width-based styling**: A **ResizeObserver** watches the wrapper’s width. When width is **&lt; 600px**, the component gets mobile-optimized styles (e.g. larger touch targets, adjusted typography). When width is **≥ 600px**, desktop styles apply. This works even when the component is inside a constrained parent (e.g. iframe or responsive design tools), not only the window.
80
+ - **Media queries**: CSS also uses `@media (max-width: 599px)` and `@media (min-width: 600px)` so that layout and typography remain correct if JavaScript is slow or ResizeObserver is unavailable.
81
+
82
+ ## CSS Grid layout structure
83
+
84
+ The layout is a single grid with three rows:
85
+
86
+ - **Row 1 (header)**: `grid-template-rows: auto` — height by content (currently empty; structure is in place for future header content).
87
+ - **Row 2 (body)**: `1fr` — takes all remaining space; this is the scrollable area where `children` are rendered.
88
+ - **Row 3 (footer)**: `auto` — height by content (currently empty).
89
+
90
+ The body cell uses `min-height: 0` so the grid allows it to shrink, and `overflow-y: auto` so content scrolls when it exceeds the available height.
91
+
92
+ ## Fluid sizing with `clamp()`
93
+
94
+ - **Padding (body)**: `padding: clamp(0.5rem, 2vw, 1.5rem)` — scales between 0.5rem and 1.5rem with viewport.
95
+ - **Font size**: `font-size: clamp(14px, 2.5vw, 18px)` (desktop) and a slightly smaller max on mobile so text stays readable at all sizes.
96
+
97
+ This reduces the need for many breakpoint-specific values and keeps scaling smooth.
98
+
99
+ ## Mobile-first and accessibility
100
+
101
+ - **Mobile-first**: Base styles work for small screens; desktop styles enhance layout and typography at 600px and up. ResizeObserver aligns behavior with the actual container width.
102
+ - **Semantic HTML**: Uses `<section>`, `<header>`, `<main>`, and `<footer>` for structure and assistive tech.
103
+ - **Keyboard**: The main content area is focusable (`tabIndex={0}`) and has a visible `:focus-visible` outline.
104
+ - **ARIA**: The wrapper has `role="region"` and `aria-label="Content wrapper"`; the main area has `aria-label="Main content"`. Header and footer are `aria-hidden` when empty.
105
+ - **Touch targets**: On mobile, buttons and link-like elements inside the body get a minimum size of 44×44px where possible to improve tap usability and align with WCAG 2.5.5 (Target Size).
106
+ - **Contrast**: Text and background use high-contrast defaults; override via your own styles if needed.
107
+
108
+ ## Dependencies
109
+
110
+ - **React** (with hooks: `useRef`, `useState`, `useEffect`).
111
+ - **ResizeObserver** (built-in in modern browsers; no extra library).
112
+ - **CSS**: Grid, Flexbox, media queries, and `clamp()` — no JS-based styling required.
113
+
114
+ ## File structure
115
+
116
+ ```
117
+ ResponsiveWrapper/
118
+ index.tsx – Main component and ResizeObserver logic
119
+ style.module.css – Scoped styles (Grid, clamp, media queries)
120
+ README.md – This documentation
121
+ ```
122
+
123
+ ## Browser support
124
+
125
+ - Modern browsers that support CSS Grid, `clamp()`, and `ResizeObserver`.
126
+ - `100dvh` is supported in current Chrome, Safari, and Firefox; consider a fallback (e.g. `height: 100vh`) for very old browsers if needed.
@@ -0,0 +1,84 @@
1
+ import React, { useRef, useState, useEffect } from 'react';
2
+ import styles from './style.module.css';
3
+
4
+ export interface ResponsiveWrapperProps {
5
+ children: React.ReactNode;
6
+ /** Optional max-width for the container (default: "100%") */
7
+ maxWidth?: string;
8
+ /** Optional padding (default: "1rem") */
9
+ padding?: string;
10
+ /** Optional gap between grid rows (default: "1rem") */
11
+ gap?: string;
12
+ }
13
+
14
+ const MOBILE_BREAKPOINT_PX = 600;
15
+
16
+ /**
17
+ * ResponsiveWrapper – A responsive layout container using CSS Grid with three rows
18
+ * (header, body, footer). The body section is scrollable when content exceeds the
19
+ * viewport. Uses ResizeObserver for dimension-based styling and 100dvh for
20
+ * mobile viewport handling.
21
+ */
22
+ const ResponsiveWrapper: React.FC<ResponsiveWrapperProps> = ({
23
+ children,
24
+ maxWidth = '100%',
25
+ padding = '1rem',
26
+ gap = '1rem',
27
+ }) => {
28
+ const containerRef = useRef<HTMLSectionElement>(null);
29
+ const [isMobile, setIsMobile] = useState(false);
30
+
31
+ useEffect(() => {
32
+ const el = containerRef.current;
33
+ if (!el) return;
34
+
35
+ // ResizeObserver tracks the container's width so we can apply mobile vs desktop
36
+ // styles based on actual rendered size (e.g. in responsive design mode or
37
+ // when the container is constrained by a parent), not just the viewport.
38
+ const resizeObserver = new ResizeObserver((entries) => {
39
+ for (const entry of entries) {
40
+ const width = entry.contentRect.width;
41
+ setIsMobile(width < MOBILE_BREAKPOINT_PX);
42
+ }
43
+ });
44
+
45
+ resizeObserver.observe(el);
46
+
47
+ // Set initial value in case ResizeObserver fires after first paint
48
+ setIsMobile(el.getBoundingClientRect().width < MOBILE_BREAKPOINT_PX);
49
+
50
+ return () => {
51
+ resizeObserver.disconnect();
52
+ };
53
+ }, []);
54
+
55
+ return (
56
+ <section
57
+ ref={containerRef}
58
+ className={`${styles.wrapper} ${isMobile ? styles.mobile : styles.desktop}`}
59
+ style={{
60
+ maxWidth,
61
+ padding,
62
+ gap,
63
+ }}
64
+ role="region"
65
+ aria-label="Content wrapper"
66
+ >
67
+ <header className={styles.header} aria-hidden>
68
+ {/* Optional header slot; empty by default. Structure supports future extension. */}
69
+ </header>
70
+ <main
71
+ className={styles.body}
72
+ tabIndex={0}
73
+ aria-label="Main content"
74
+ >
75
+ {children}
76
+ </main>
77
+ <footer className={styles.footer} aria-hidden>
78
+ {/* Optional footer slot; empty by default. */}
79
+ </footer>
80
+ </section>
81
+ );
82
+ };
83
+
84
+ export default ResponsiveWrapper;
@@ -0,0 +1,73 @@
1
+ /* Three-row grid: header (auto), body (1fr), footer (auto). Body scrolls when content overflows. */
2
+ .wrapper {
3
+ display: grid;
4
+ grid-template-rows: auto 1fr auto;
5
+ min-height: 0;
6
+ /* 100dvh: dynamic viewport height – accounts for mobile browser UI (address bar) showing/hiding */
7
+ height: 100dvh;
8
+ max-height: 100dvh;
9
+ box-sizing: border-box;
10
+ font-size: clamp(14px, 2.5vw, 18px);
11
+ line-height: 1.5;
12
+ color: #1a1a1a;
13
+ background-color: #ffffff;
14
+ }
15
+
16
+ .header,
17
+ .footer {
18
+ min-height: 0;
19
+ flex-shrink: 0;
20
+ }
21
+
22
+ /* Scrollable body: takes remaining space and scrolls when content exceeds viewport */
23
+ .body {
24
+ min-height: 0;
25
+ overflow-y: auto;
26
+ overflow-x: hidden;
27
+ padding: clamp(0.5rem, 2vw, 1.5rem);
28
+ -webkit-overflow-scrolling: touch;
29
+ }
30
+
31
+ .body:focus {
32
+ outline: none;
33
+ }
34
+
35
+ .body:focus-visible {
36
+ outline: 2px solid #2563eb;
37
+ outline-offset: 2px;
38
+ }
39
+
40
+ /* Ensure readable text and sufficient touch targets on mobile */
41
+ .mobile .body {
42
+ padding: clamp(0.5rem, 2vw, 1.5rem);
43
+ font-size: clamp(14px, 2.5vw, 16px);
44
+ }
45
+
46
+ /* Minimum 44px touch targets for mobile accessibility (WCAG 2.5.5) */
47
+ .mobile .body button,
48
+ .mobile .body a[href],
49
+ .mobile .body [role="button"] {
50
+ min-height: 44px;
51
+ min-width: 44px;
52
+ display: inline-flex;
53
+ align-items: center;
54
+ justify-content: center;
55
+ }
56
+
57
+ /* Desktop: more spacing and typography */
58
+ .desktop .body {
59
+ padding: clamp(0.5rem, 2vw, 1.5rem);
60
+ }
61
+
62
+ /* Media query fallback for environments where ResizeObserver might not drive class */
63
+ @media (max-width: 599px) {
64
+ .wrapper {
65
+ font-size: clamp(14px, 2.5vw, 16px);
66
+ }
67
+ }
68
+
69
+ @media (min-width: 600px) {
70
+ .wrapper {
71
+ font-size: clamp(14px, 2.5vw, 18px);
72
+ }
73
+ }
package/src/index.ts CHANGED
@@ -22,4 +22,15 @@ export {
22
22
  type DraggableItem,
23
23
  type DropTarget,
24
24
  type DragAndDropProps,
25
- } from '../DragAndDropActivity';
25
+ } from '../DragAndDropActivity';
26
+ export {
27
+ default as BranchingScenario,
28
+ type BranchingScenarioProps,
29
+ type ScenarioNode,
30
+ type Choice,
31
+ type Outcome,
32
+ } from '../BranchingScenario';
33
+ export {
34
+ default as ResponsiveWrapper,
35
+ type ResponsiveWrapperProps,
36
+ } from './components/ResponsiveWrapper';