@zomako/elearning-components 2.0.6 → 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.
@@ -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';