@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.
- package/BranchingScenario/README.md +192 -0
- package/BranchingScenario/index.tsx +237 -0
- package/BranchingScenario/style.module.css +187 -0
- package/DragAndDropActivity/README.md +1 -1
- package/DragAndDropActivity/index.tsx +5 -5
- package/DragAndDropActivity/style.module.css +33 -13
- package/README.md +8 -0
- package/dist/elearning-components.css +1 -1
- package/dist/elearning-components.es.js +2796 -2595
- package/dist/elearning-components.umd.js +9 -9
- package/package.json +1 -1
- package/src/App.jsx +75 -1
- package/src/components/Accordion/README.md +164 -0
- package/src/components/Accordion/index.tsx +145 -0
- package/src/components/Accordion/style.module.css +123 -0
- package/src/components/ResponsiveWrapper/README.md +126 -0
- package/src/components/ResponsiveWrapper/index.tsx +84 -0
- package/src/components/ResponsiveWrapper/style.module.css +73 -0
- package/src/index.ts +12 -1
|
@@ -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';
|