@zomako/elearning-components 2.0.4 → 2.0.6

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/src/App.jsx CHANGED
@@ -1,9 +1,16 @@
1
1
  import React from 'react';
2
- import { Accordion, FlashcardDeck } from './index';
2
+ import {
3
+ Accordion,
4
+ FlashcardDeck,
5
+ InteractiveTimeline,
6
+ MatchingActivity,
7
+ SortingActivity,
8
+ DragAndDropActivity,
9
+ } from './index';
3
10
 
4
11
  const accordionItems = [
5
12
  { id: 'getting-started', title: 'Getting started', content: 'Install the package and import the components you need.' },
6
- { id: 'components', title: 'Components', content: 'FlashcardDeck and Accordion are available. See each component README for usage.' },
13
+ { id: 'components', title: 'Components', content: 'Accordion, FlashcardDeck, InteractiveTimeline, MatchingActivity, SortingActivity, and DragAndDropActivity are available.' },
7
14
  { id: 'support', title: 'Support', content: 'Check the repository for documentation and issues.' },
8
15
  ];
9
16
 
@@ -12,18 +19,91 @@ const sampleCards = [
12
19
  { front: 'Front 2', back: 'Back 2' },
13
20
  ];
14
21
 
22
+ const timelineEvents = [
23
+ { id: '1', year: '2020', title: 'Project kickoff', content: <p>The project started with a small team and a clear roadmap.</p> },
24
+ { id: '2', year: '2021', title: 'First release', content: <p>We shipped the first public version and gathered feedback.</p> },
25
+ { id: '3', year: '2022', title: 'Scale and grow', content: <p>Focus shifted to performance and scaling for enterprise.</p> },
26
+ ];
27
+
28
+ const matchingItems = [
29
+ { id: 'a', content: 'Paris', matchId: 'capital-france' },
30
+ { id: 'b', content: 'Berlin', matchId: 'capital-germany' },
31
+ { id: 'c', content: 'Madrid', matchId: 'capital-spain' },
32
+ ];
33
+
34
+ const matchingTargets = [
35
+ { id: 'capital-france', content: 'France' },
36
+ { id: 'capital-germany', content: 'Germany' },
37
+ { id: 'capital-spain', content: 'Spain' },
38
+ ];
39
+
40
+ const sortableItems = [
41
+ { id: '1', content: 'First step: Plan the project' },
42
+ { id: '2', content: 'Second step: Build the prototype' },
43
+ { id: '3', content: 'Third step: Test and iterate' },
44
+ { id: '4', content: 'Fourth step: Launch' },
45
+ ];
46
+
47
+ const correctOrder = ['1', '2', '3', '4'];
48
+
49
+ const dragDropItems = [
50
+ { id: 'paris', content: 'Paris' },
51
+ { id: 'berlin', content: 'Berlin' },
52
+ { id: 'madrid', content: 'Madrid' },
53
+ ];
54
+
55
+ const dragDropTargets = [
56
+ { id: 'target-fr', accepts: ['paris'], label: 'Capital of France' },
57
+ { id: 'target-de', accepts: ['berlin'], label: 'Capital of Germany' },
58
+ { id: 'target-es', accepts: ['madrid'], label: 'Capital of Spain' },
59
+ ];
60
+
15
61
  export default function App() {
16
62
  return (
17
- <div style={{ padding: '2rem', maxWidth: 640, margin: '0 auto' }}>
63
+ <div style={{ padding: '2rem', maxWidth: 800, margin: '0 auto' }}>
18
64
  <h1>E-learning Components</h1>
19
- <section style={{ marginBottom: '2rem' }}>
65
+
66
+ <section style={{ marginBottom: '2.5rem' }}>
20
67
  <h2>Accordion</h2>
21
68
  <Accordion items={accordionItems} />
22
69
  </section>
23
- <section>
70
+
71
+ <section style={{ marginBottom: '2.5rem' }}>
24
72
  <h2>FlashcardDeck</h2>
25
73
  <FlashcardDeck cards={sampleCards} />
26
74
  </section>
75
+
76
+ <section style={{ marginBottom: '2.5rem' }}>
77
+ <h2>InteractiveTimeline</h2>
78
+ <InteractiveTimeline events={timelineEvents} />
79
+ </section>
80
+
81
+ <section style={{ marginBottom: '2.5rem' }}>
82
+ <h2>MatchingActivity</h2>
83
+ <MatchingActivity
84
+ items={matchingItems}
85
+ targets={matchingTargets}
86
+ onComplete={(result) => console.log('Matching result:', result)}
87
+ />
88
+ </section>
89
+
90
+ <section style={{ marginBottom: '2.5rem' }}>
91
+ <h2>SortingActivity</h2>
92
+ <SortingActivity
93
+ items={sortableItems}
94
+ correctOrder={correctOrder}
95
+ onComplete={(result) => console.log('Sorting result:', result)}
96
+ />
97
+ </section>
98
+
99
+ <section>
100
+ <h2>DragAndDropActivity</h2>
101
+ <DragAndDropActivity
102
+ items={dragDropItems}
103
+ targets={dragDropTargets}
104
+ onComplete={(result) => console.log('Drag and drop result:', result)}
105
+ />
106
+ </section>
27
107
  </div>
28
108
  );
29
109
  }
@@ -0,0 +1,191 @@
1
+ import React, { useRef, useState, useCallback, useEffect } from 'react';
2
+ import styles from './style.module.css';
3
+
4
+ export interface TimelineEvent {
5
+ id: string;
6
+ year: string;
7
+ title: string;
8
+ content: React.ReactNode;
9
+ }
10
+
11
+ export interface InteractiveTimelineProps {
12
+ events: TimelineEvent[];
13
+ defaultActiveId?: string;
14
+ }
15
+
16
+ const InteractiveTimeline: React.FC<InteractiveTimelineProps> = ({
17
+ events,
18
+ defaultActiveId,
19
+ }) => {
20
+ const scrollRef = useRef<HTMLDivElement>(null);
21
+ const [activeId, setActiveId] = useState<string | null>(() => {
22
+ if (defaultActiveId && events.some((e) => e.id === defaultActiveId)) {
23
+ return defaultActiveId;
24
+ }
25
+ return events.length > 0 ? events[0].id : null;
26
+ });
27
+ const [isDragging, setIsDragging] = useState(false);
28
+ const dragState = useRef({ startX: 0, scrollLeft: 0 });
29
+
30
+ const activeEvent = events.find((e) => e.id === activeId) ?? events[0];
31
+ const activeIndex = events.findIndex((e) => e.id === activeId);
32
+
33
+ const handleMarkerClick = useCallback(
34
+ (id: string) => {
35
+ setActiveId(id);
36
+ },
37
+ []
38
+ );
39
+
40
+ const goPrevious = useCallback(() => {
41
+ if (activeIndex > 0) {
42
+ setActiveId(events[activeIndex - 1].id);
43
+ }
44
+ }, [activeIndex, events]);
45
+
46
+ const goNext = useCallback(() => {
47
+ if (activeIndex < events.length - 1 && activeIndex >= 0) {
48
+ setActiveId(events[activeIndex + 1].id);
49
+ }
50
+ }, [activeIndex, events]);
51
+
52
+ const handlePointerDown = useCallback(
53
+ (e: React.MouseEvent | React.TouchEvent) => {
54
+ if (!scrollRef.current) return;
55
+ const target = e.target as HTMLElement;
56
+ if (target.closest(`.${styles.marker}`)) return;
57
+ const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
58
+ setIsDragging(true);
59
+ dragState.current = {
60
+ startX: clientX,
61
+ scrollLeft: scrollRef.current.scrollLeft,
62
+ };
63
+ },
64
+ []
65
+ );
66
+
67
+ const handlePointerMove = useCallback(
68
+ (e: MouseEvent | TouchEvent) => {
69
+ if (!isDragging || !scrollRef.current) return;
70
+ e.preventDefault();
71
+ const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
72
+ const dx = dragState.current.startX - clientX;
73
+ scrollRef.current.scrollLeft = dragState.current.scrollLeft + dx;
74
+ },
75
+ [isDragging]
76
+ );
77
+
78
+ const handlePointerUp = useCallback(() => {
79
+ setIsDragging(false);
80
+ }, []);
81
+
82
+ useEffect(() => {
83
+ if (!isDragging) return;
84
+ window.addEventListener('mousemove', handlePointerMove, { passive: false });
85
+ window.addEventListener('mouseup', handlePointerUp);
86
+ window.addEventListener('touchmove', handlePointerMove, { passive: false });
87
+ window.addEventListener('touchend', handlePointerUp);
88
+ return () => {
89
+ window.removeEventListener('mousemove', handlePointerMove);
90
+ window.removeEventListener('mouseup', handlePointerUp);
91
+ window.removeEventListener('touchmove', handlePointerMove);
92
+ window.removeEventListener('touchend', handlePointerUp);
93
+ };
94
+ }, [isDragging, handlePointerMove, handlePointerUp]);
95
+
96
+ useEffect(() => {
97
+ if (!scrollRef.current || !activeId) return;
98
+ const marker = scrollRef.current.querySelector(
99
+ `[data-event-id="${activeId}"]`
100
+ ) as HTMLElement | null;
101
+ if (marker) {
102
+ const container = scrollRef.current;
103
+ const half = container.clientWidth / 2;
104
+ const target =
105
+ marker.offsetLeft - half + marker.offsetWidth / 2;
106
+ container.scrollTo({
107
+ left: Math.max(0, target),
108
+ behavior: 'smooth',
109
+ });
110
+ }
111
+ }, [activeId]);
112
+
113
+ if (!events || events.length === 0) {
114
+ return (
115
+ <div className={styles.wrapper}>
116
+ <p className={styles.empty}>No events to display.</p>
117
+ </div>
118
+ );
119
+ }
120
+
121
+ const canGoPrevious = activeIndex > 0;
122
+ const canGoNext = activeIndex >= 0 && activeIndex < events.length - 1;
123
+
124
+ return (
125
+ <div className={styles.wrapper}>
126
+ <div className={styles.contentPanel}>
127
+ <div key={activeId} className={styles.contentSlide}>
128
+ {activeEvent && (
129
+ <>
130
+ <span className={styles.contentYear}>{activeEvent.year}</span>
131
+ <h2 className={styles.contentTitle}>{activeEvent.title}</h2>
132
+ <div className={styles.contentBody}>{activeEvent.content}</div>
133
+ </>
134
+ )}
135
+ </div>
136
+ </div>
137
+
138
+ <div className={styles.timelineSection}>
139
+ <button
140
+ type="button"
141
+ className={styles.navButton}
142
+ onClick={goPrevious}
143
+ disabled={!canGoPrevious}
144
+ aria-label="Previous event"
145
+ >
146
+
147
+ </button>
148
+
149
+ <div
150
+ ref={scrollRef}
151
+ className={`${styles.track} ${isDragging ? styles.dragging : ''}`}
152
+ onMouseDown={handlePointerDown}
153
+ onTouchStart={handlePointerDown}
154
+ role="list"
155
+ aria-label="Timeline events"
156
+ >
157
+ <div className={styles.trackInner}>
158
+ {events.map((event) => (
159
+ <button
160
+ type="button"
161
+ key={event.id}
162
+ data-event-id={event.id}
163
+ className={`${styles.marker} ${
164
+ event.id === activeId ? styles.markerActive : ''
165
+ }`}
166
+ onClick={() => handleMarkerClick(event.id)}
167
+ aria-pressed={event.id === activeId}
168
+ aria-label={`${event.year}: ${event.title}`}
169
+ >
170
+ <span className={styles.markerYear}>{event.year}</span>
171
+ <span className={styles.markerTitle}>{event.title}</span>
172
+ </button>
173
+ ))}
174
+ </div>
175
+ </div>
176
+
177
+ <button
178
+ type="button"
179
+ className={styles.navButton}
180
+ onClick={goNext}
181
+ disabled={!canGoNext}
182
+ aria-label="Next event"
183
+ >
184
+
185
+ </button>
186
+ </div>
187
+ </div>
188
+ );
189
+ };
190
+
191
+ export default InteractiveTimeline;
@@ -0,0 +1,5 @@
1
+ export {
2
+ default,
3
+ type TimelineEvent,
4
+ type InteractiveTimelineProps,
5
+ } from './InteractiveTimeline';
@@ -0,0 +1,211 @@
1
+ /* ---- Wrapper ---- */
2
+ .wrapper {
3
+ --timeline-accent: #2563eb;
4
+ --timeline-accent-hover: #1d4ed8;
5
+ --timeline-bg: #f8fafc;
6
+ --timeline-line: #e2e8f0;
7
+ --timeline-text: #1e293b;
8
+ --timeline-muted: #64748b;
9
+ --timeline-radius: 12px;
10
+ --timeline-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
11
+ font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
12
+ color: var(--timeline-text);
13
+ max-width: 100%;
14
+ display: flex;
15
+ flex-direction: column;
16
+ gap: 1.5rem;
17
+ }
18
+
19
+ /* ---- Content panel ---- */
20
+ .contentPanel {
21
+ background: var(--timeline-bg);
22
+ border-radius: var(--timeline-radius);
23
+ padding: 1.75rem 2rem;
24
+ box-shadow: var(--timeline-shadow);
25
+ min-height: 140px;
26
+ overflow: hidden;
27
+ }
28
+
29
+ .contentSlide {
30
+ animation: contentIn 0.35s ease-out forwards;
31
+ }
32
+
33
+ @keyframes contentIn {
34
+ from {
35
+ opacity: 0;
36
+ transform: translateY(8px);
37
+ }
38
+ to {
39
+ opacity: 1;
40
+ transform: translateY(0);
41
+ }
42
+ }
43
+
44
+ .contentYear {
45
+ display: inline-block;
46
+ font-size: 0.8125rem;
47
+ font-weight: 600;
48
+ letter-spacing: 0.05em;
49
+ text-transform: uppercase;
50
+ color: var(--timeline-accent);
51
+ margin-bottom: 0.35rem;
52
+ }
53
+
54
+ .contentTitle {
55
+ font-size: 1.375rem;
56
+ font-weight: 700;
57
+ line-height: 1.3;
58
+ margin: 0 0 0.75rem 0;
59
+ color: var(--timeline-text);
60
+ }
61
+
62
+ .contentBody {
63
+ font-size: 1rem;
64
+ line-height: 1.6;
65
+ color: var(--timeline-muted);
66
+ }
67
+
68
+ .contentBody p {
69
+ margin: 0 0 0.5rem 0;
70
+ }
71
+
72
+ .contentBody p:last-child {
73
+ margin-bottom: 0;
74
+ }
75
+
76
+ /* ---- Timeline section ---- */
77
+ .timelineSection {
78
+ display: flex;
79
+ align-items: center;
80
+ gap: 0.5rem;
81
+ }
82
+
83
+ .navButton {
84
+ flex-shrink: 0;
85
+ width: 2.5rem;
86
+ height: 2.5rem;
87
+ border-radius: 50%;
88
+ border: 2px solid var(--timeline-line);
89
+ background: #fff;
90
+ color: var(--timeline-text);
91
+ font-size: 1.5rem;
92
+ line-height: 1;
93
+ cursor: pointer;
94
+ display: flex;
95
+ align-items: center;
96
+ justify-content: center;
97
+ transition: border-color 0.2s ease, background 0.2s ease, color 0.2s ease;
98
+ }
99
+
100
+ .navButton:hover:not(:disabled) {
101
+ border-color: var(--timeline-accent);
102
+ background: var(--timeline-accent);
103
+ color: #fff;
104
+ }
105
+
106
+ .navButton:disabled {
107
+ opacity: 0.4;
108
+ cursor: not-allowed;
109
+ }
110
+
111
+ /* ---- Track (draggable scroll area) ---- */
112
+ .track {
113
+ flex: 1;
114
+ overflow-x: auto;
115
+ overflow-y: hidden;
116
+ scroll-behavior: smooth;
117
+ scrollbar-width: thin;
118
+ scrollbar-color: var(--timeline-line) transparent;
119
+ cursor: grab;
120
+ padding: 0.5rem 0;
121
+ margin: 0 -0.5rem;
122
+ }
123
+
124
+ .track.dragging {
125
+ cursor: grabbing;
126
+ scroll-behavior: auto;
127
+ }
128
+
129
+ .track::-webkit-scrollbar {
130
+ height: 6px;
131
+ }
132
+
133
+ .track::-webkit-scrollbar-track {
134
+ background: transparent;
135
+ }
136
+
137
+ .track::-webkit-scrollbar-thumb {
138
+ background: var(--timeline-line);
139
+ border-radius: 3px;
140
+ }
141
+
142
+ .trackInner {
143
+ display: flex;
144
+ gap: 0.75rem;
145
+ padding: 0 0.25rem;
146
+ min-width: min-content;
147
+ }
148
+
149
+ /* ---- Event markers ---- */
150
+ .marker {
151
+ flex-shrink: 0;
152
+ display: flex;
153
+ flex-direction: column;
154
+ align-items: center;
155
+ gap: 0.25rem;
156
+ padding: 0.75rem 1rem;
157
+ min-width: 100px;
158
+ border: 2px solid var(--timeline-line);
159
+ border-radius: var(--timeline-radius);
160
+ background: #fff;
161
+ color: var(--timeline-muted);
162
+ font: inherit;
163
+ cursor: pointer;
164
+ text-align: center;
165
+ transition: transform 0.25s ease, border-color 0.25s ease, background 0.25s ease,
166
+ color 0.25s ease, box-shadow 0.25s ease;
167
+ }
168
+
169
+ .marker:hover {
170
+ border-color: var(--timeline-accent);
171
+ color: var(--timeline-accent);
172
+ }
173
+
174
+ .markerActive {
175
+ border-color: var(--timeline-accent);
176
+ background: var(--timeline-accent);
177
+ color: #fff;
178
+ transform: scale(1.08);
179
+ box-shadow: var(--timeline-shadow);
180
+ }
181
+
182
+ .markerActive:hover {
183
+ background: var(--timeline-accent-hover);
184
+ border-color: var(--timeline-accent-hover);
185
+ color: #fff;
186
+ }
187
+
188
+ .markerYear {
189
+ font-size: 0.8125rem;
190
+ font-weight: 700;
191
+ letter-spacing: 0.02em;
192
+ }
193
+
194
+ .markerTitle {
195
+ font-size: 0.8125rem;
196
+ font-weight: 500;
197
+ line-height: 1.25;
198
+ display: -webkit-box;
199
+ -webkit-line-clamp: 2;
200
+ -webkit-box-orient: vertical;
201
+ overflow: hidden;
202
+ }
203
+
204
+ /* ---- Empty state ---- */
205
+ .empty {
206
+ margin: 0;
207
+ padding: 2rem;
208
+ text-align: center;
209
+ color: var(--timeline-muted);
210
+ font-size: 1rem;
211
+ }
package/src/index.ts CHANGED
@@ -1,3 +1,25 @@
1
1
  // src/index.ts
2
2
  export { default as FlashcardDeck } from './components/FlashcardDeck';
3
- export { default as Accordion, type AccordionProps, type AccordionItem } from './components/Accordion';
3
+ export { default as Accordion, type AccordionProps, type AccordionItem } from '../Accordion';
4
+ export {
5
+ default as InteractiveTimeline,
6
+ type TimelineEvent,
7
+ type InteractiveTimelineProps,
8
+ } from './components/InteractiveTimeline';
9
+ export {
10
+ default as MatchingActivity,
11
+ type MatchItem,
12
+ type MatchTarget,
13
+ type MatchingActivityProps,
14
+ } from '../MatchingActivity';
15
+ export {
16
+ default as SortingActivity,
17
+ type SortableItem,
18
+ type SortingActivityProps,
19
+ } from '../SortingActivity';
20
+ export {
21
+ default as DragAndDropActivity,
22
+ type DraggableItem,
23
+ type DropTarget,
24
+ type DragAndDropProps,
25
+ } from '../DragAndDropActivity';