@hustle-together/api-dev-tools 2.0.5 → 2.0.7
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/demo/workflow-demo.html +327 -257
- package/package.json +1 -1
package/demo/workflow-demo.html
CHANGED
|
@@ -506,6 +506,19 @@
|
|
|
506
506
|
border-color: var(--accent-red);
|
|
507
507
|
}
|
|
508
508
|
|
|
509
|
+
.nav-btn.disabled {
|
|
510
|
+
opacity: 0.5;
|
|
511
|
+
cursor: not-allowed;
|
|
512
|
+
text-decoration: line-through;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.nav-btn.disabled:hover {
|
|
516
|
+
background: transparent;
|
|
517
|
+
color: var(--grey);
|
|
518
|
+
border-color: var(--grey);
|
|
519
|
+
box-shadow: none;
|
|
520
|
+
}
|
|
521
|
+
|
|
509
522
|
/* ============================================
|
|
510
523
|
AUDIO NARRATION PLAYER
|
|
511
524
|
============================================ */
|
|
@@ -570,12 +583,32 @@
|
|
|
570
583
|
|
|
571
584
|
/* Audio highlight effect for elements being narrated */
|
|
572
585
|
.audio-highlighted {
|
|
573
|
-
|
|
586
|
+
outline: 2px solid var(--accent-red) !important;
|
|
587
|
+
outline-offset: 4px;
|
|
574
588
|
border-color: var(--accent-red) !important;
|
|
575
|
-
transform: scale(1.02);
|
|
576
589
|
transition: all 0.3s ease !important;
|
|
577
590
|
}
|
|
578
591
|
|
|
592
|
+
/* For container elements (cards, boxes), use glow effect */
|
|
593
|
+
.audio-highlighted.solution-card,
|
|
594
|
+
.audio-highlighted.phase-box,
|
|
595
|
+
.audio-highlighted.gap-item,
|
|
596
|
+
.audio-highlighted.phase-node {
|
|
597
|
+
box-shadow: 0 0 25px var(--accent-red-glow) !important;
|
|
598
|
+
outline: none !important;
|
|
599
|
+
transform: scale(1.02);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/* For text elements (headings, brand), use subtle underline/border */
|
|
603
|
+
.audio-highlighted h2,
|
|
604
|
+
h2.audio-highlighted,
|
|
605
|
+
.hustle-brand.audio-highlighted {
|
|
606
|
+
box-shadow: none !important;
|
|
607
|
+
outline: none !important;
|
|
608
|
+
border-bottom: 3px solid var(--accent-red) !important;
|
|
609
|
+
padding-bottom: 8px;
|
|
610
|
+
}
|
|
611
|
+
|
|
579
612
|
/* Progress indicator */
|
|
580
613
|
.progress-bar {
|
|
581
614
|
position: fixed;
|
|
@@ -1665,9 +1698,9 @@
|
|
|
1665
1698
|
|
|
1666
1699
|
<!-- Navigation -->
|
|
1667
1700
|
<nav class="nav">
|
|
1668
|
-
<button class="nav-btn" id="
|
|
1669
|
-
<button class="nav-btn" id="
|
|
1670
|
-
<button class="nav-btn" id="resetBtn"
|
|
1701
|
+
<button class="nav-btn" id="playBtn">▶ AUTO PLAY</button>
|
|
1702
|
+
<button class="nav-btn" id="audioToggleBtn" title="Toggle narration audio">🔊 WITH AUDIO</button>
|
|
1703
|
+
<button class="nav-btn" id="resetBtn">↺ RESTART</button>
|
|
1671
1704
|
</nav>
|
|
1672
1705
|
|
|
1673
1706
|
<!-- Audio Narration Player (hidden) -->
|
|
@@ -1700,7 +1733,7 @@
|
|
|
1700
1733
|
<span class="hustle-word hustle-highlight">API-DEV-TOOLS</span>
|
|
1701
1734
|
</div>
|
|
1702
1735
|
<div class="package-name" id="packageName">@hustle-together/api-dev-tools</div>
|
|
1703
|
-
<div class="version" id="versionText">v2.0.
|
|
1736
|
+
<div class="version" id="versionText">v2.0.7</div>
|
|
1704
1737
|
<p class="tagline">"Hustle together. Share resources. Build stronger."<span class="cursor"></span></p>
|
|
1705
1738
|
|
|
1706
1739
|
<div class="intro-text">
|
|
@@ -2533,11 +2566,13 @@
|
|
|
2533
2566
|
const phaseArrows = document.querySelectorAll('#introFlow .phase-connector-arrow');
|
|
2534
2567
|
|
|
2535
2568
|
// Create a separate timeline for the lighting sequence that loops
|
|
2536
|
-
|
|
2569
|
+
// Store globally so we can pause during narration
|
|
2570
|
+
window.introLightingTL = gsap.timeline({
|
|
2537
2571
|
repeat: -1,
|
|
2538
2572
|
repeatDelay: 1,
|
|
2539
2573
|
delay: 3 // Wait for intro animation to complete
|
|
2540
2574
|
});
|
|
2575
|
+
const lightingTL = window.introLightingTL;
|
|
2541
2576
|
|
|
2542
2577
|
phaseNodes.forEach((node, i) => {
|
|
2543
2578
|
lightingTL
|
|
@@ -2758,32 +2793,295 @@
|
|
|
2758
2793
|
.to('.made-with', { opacity: 1, duration: 0.5 });
|
|
2759
2794
|
|
|
2760
2795
|
// ============================================
|
|
2761
|
-
// AUTO-PLAY
|
|
2796
|
+
// UNIFIED AUTO-PLAY + AUDIO SYSTEM
|
|
2762
2797
|
// ============================================
|
|
2763
|
-
document.getElementById('playBtn')
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2798
|
+
const playBtn = document.getElementById('playBtn');
|
|
2799
|
+
const audioToggleBtn = document.getElementById('audioToggleBtn');
|
|
2800
|
+
const narrationAudio = document.getElementById('narrationAudio');
|
|
2801
|
+
const audioProgressContainer = document.getElementById('audioProgressContainer');
|
|
2802
|
+
const audioProgressBar = document.getElementById('audioProgressBar');
|
|
2803
|
+
const audioProgressFill = document.getElementById('audioProgressFill');
|
|
2804
|
+
const audioCurrentTime = document.getElementById('audioCurrentTime');
|
|
2805
|
+
const audioTotalTime = document.getElementById('audioTotalTime');
|
|
2806
|
+
const audioSectionMarkers = document.getElementById('audioSectionMarkers');
|
|
2807
|
+
|
|
2808
|
+
let audioEnabled = true; // Audio on by default
|
|
2809
|
+
let timingData = null;
|
|
2810
|
+
let currentHighlight = null;
|
|
2811
|
+
let highlightIndex = 0;
|
|
2812
|
+
let currentSectionIdx = 0;
|
|
2813
|
+
let autoPlayInterval = null;
|
|
2814
|
+
|
|
2815
|
+
// Format seconds to MM:SS
|
|
2816
|
+
function formatTime(seconds) {
|
|
2817
|
+
const mins = Math.floor(seconds / 60);
|
|
2818
|
+
const secs = Math.floor(seconds % 60);
|
|
2819
|
+
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
// Load timing data
|
|
2823
|
+
async function loadTimingData() {
|
|
2824
|
+
if (timingData) return timingData;
|
|
2825
|
+
try {
|
|
2826
|
+
const response = await fetch('audio/narration-timing.json');
|
|
2827
|
+
if (!response.ok) throw new Error('Failed to load timing data');
|
|
2828
|
+
timingData = await response.json();
|
|
2829
|
+
|
|
2830
|
+
// Update total time display
|
|
2831
|
+
if (audioTotalTime) {
|
|
2832
|
+
audioTotalTime.textContent = formatTime(timingData.duration);
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
// Create section markers on progress bar
|
|
2836
|
+
if (audioSectionMarkers && timingData.sections) {
|
|
2837
|
+
audioSectionMarkers.innerHTML = '';
|
|
2838
|
+
timingData.sections.forEach((section, i) => {
|
|
2839
|
+
if (i === 0) return;
|
|
2840
|
+
const marker = document.createElement('div');
|
|
2841
|
+
marker.className = 'audio-section-marker';
|
|
2842
|
+
marker.style.left = `${(section.timestamp / timingData.duration) * 100}%`;
|
|
2843
|
+
marker.title = section.id.toUpperCase();
|
|
2844
|
+
audioSectionMarkers.appendChild(marker);
|
|
2845
|
+
});
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
console.log('Timing data loaded:', timingData.sections.length, 'sections,', timingData.highlights.length, 'highlights');
|
|
2849
|
+
return timingData;
|
|
2850
|
+
} catch (error) {
|
|
2851
|
+
console.error('Failed to load timing data:', error);
|
|
2852
|
+
return null;
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
// Apply highlight to an element
|
|
2857
|
+
function applyHighlight(selector) {
|
|
2858
|
+
removeHighlight();
|
|
2859
|
+
try {
|
|
2860
|
+
const el = document.querySelector(selector);
|
|
2861
|
+
if (el) {
|
|
2862
|
+
currentHighlight = el;
|
|
2863
|
+
el.classList.add('audio-highlighted');
|
|
2864
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
2865
|
+
|
|
2866
|
+
// Only apply box-shadow GSAP animation to container elements, not text
|
|
2867
|
+
const isContainer = el.classList.contains('solution-card') ||
|
|
2868
|
+
el.classList.contains('phase-box') ||
|
|
2869
|
+
el.classList.contains('gap-item') ||
|
|
2870
|
+
el.classList.contains('phase-node') ||
|
|
2871
|
+
el.classList.contains('install-command');
|
|
2872
|
+
|
|
2873
|
+
if (isContainer) {
|
|
2874
|
+
gsap.fromTo(el,
|
|
2875
|
+
{ boxShadow: '0 0 0px var(--accent-red-glow)' },
|
|
2876
|
+
{ boxShadow: '0 0 30px var(--accent-red-glow)', duration: 0.4, ease: 'power2.out' }
|
|
2877
|
+
);
|
|
2783
2878
|
}
|
|
2784
|
-
}
|
|
2879
|
+
}
|
|
2880
|
+
} catch (e) {
|
|
2881
|
+
console.warn('Could not highlight:', selector, e);
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
|
|
2885
|
+
// Remove current highlight
|
|
2886
|
+
function removeHighlight() {
|
|
2887
|
+
if (currentHighlight) {
|
|
2888
|
+
currentHighlight.classList.remove('audio-highlighted');
|
|
2889
|
+
gsap.to(currentHighlight, { boxShadow: 'none', duration: 0.3 });
|
|
2890
|
+
currentHighlight = null;
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
// Handle audio time update (syncs highlights with narration)
|
|
2895
|
+
function onAudioTimeUpdate() {
|
|
2896
|
+
if (!timingData || !audioEnabled) return;
|
|
2897
|
+
|
|
2898
|
+
const currentTime = narrationAudio.currentTime;
|
|
2899
|
+
|
|
2900
|
+
// Update progress bar
|
|
2901
|
+
const progress = (currentTime / timingData.duration) * 100;
|
|
2902
|
+
audioProgressFill.style.width = `${progress}%`;
|
|
2903
|
+
|
|
2904
|
+
// Update time display
|
|
2905
|
+
if (audioCurrentTime) {
|
|
2906
|
+
audioCurrentTime.textContent = formatTime(currentTime);
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
// Check for highlight triggers
|
|
2910
|
+
for (let i = highlightIndex; i < timingData.highlights.length; i++) {
|
|
2911
|
+
const highlight = timingData.highlights[i];
|
|
2912
|
+
if (currentTime >= highlight.timestamp) {
|
|
2913
|
+
if (i !== highlightIndex || !currentHighlight) {
|
|
2914
|
+
highlightIndex = i;
|
|
2915
|
+
applyHighlight(highlight.selector);
|
|
2916
|
+
}
|
|
2917
|
+
} else {
|
|
2918
|
+
break;
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
// Handle seek on progress bar click
|
|
2924
|
+
function onProgressBarClick(e) {
|
|
2925
|
+
if (!timingData || !narrationAudio.duration) return;
|
|
2926
|
+
|
|
2927
|
+
const rect = audioProgressBar.getBoundingClientRect();
|
|
2928
|
+
const clickX = e.clientX - rect.left;
|
|
2929
|
+
const percentage = clickX / rect.width;
|
|
2930
|
+
const seekTime = percentage * timingData.duration;
|
|
2931
|
+
|
|
2932
|
+
narrationAudio.currentTime = seekTime;
|
|
2933
|
+
|
|
2934
|
+
// Reset highlight tracking
|
|
2935
|
+
highlightIndex = 0;
|
|
2936
|
+
for (let i = 0; i < timingData.highlights.length; i++) {
|
|
2937
|
+
if (timingData.highlights[i].timestamp <= seekTime) {
|
|
2938
|
+
highlightIndex = i;
|
|
2939
|
+
} else {
|
|
2940
|
+
break;
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2945
|
+
// Silent auto-play (no audio, timed sections)
|
|
2946
|
+
function startSilentAutoPlay() {
|
|
2947
|
+
let sectionIdx = 0;
|
|
2948
|
+
autoPlayInterval = setInterval(() => {
|
|
2949
|
+
if (sectionIdx < sections.length) {
|
|
2950
|
+
document.getElementById(sections[sectionIdx]).scrollIntoView({ behavior: 'smooth' });
|
|
2951
|
+
sectionIdx++;
|
|
2952
|
+
} else {
|
|
2953
|
+
stopAutoPlay();
|
|
2954
|
+
}
|
|
2955
|
+
}, 5000);
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
// Start auto-play
|
|
2959
|
+
async function startAutoPlay() {
|
|
2960
|
+
if (isPlaying) return;
|
|
2961
|
+
|
|
2962
|
+
isPlaying = true;
|
|
2963
|
+
playBtn.classList.add('active');
|
|
2964
|
+
playBtn.textContent = '⏸ STOP';
|
|
2965
|
+
|
|
2966
|
+
// Pause the intro lighting animation to avoid visual conflict
|
|
2967
|
+
if (window.introLightingTL) {
|
|
2968
|
+
window.introLightingTL.pause();
|
|
2969
|
+
// Reset the phase nodes to clean state
|
|
2970
|
+
document.querySelectorAll('#introFlow .phase-node').forEach(n => n.classList.remove('active'));
|
|
2971
|
+
document.querySelectorAll('#introFlow .phase-connector-arrow').forEach(a => a.classList.remove('active'));
|
|
2972
|
+
}
|
|
2973
|
+
|
|
2974
|
+
// Scroll to top first
|
|
2975
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
2976
|
+
|
|
2977
|
+
await loadTimingData();
|
|
2978
|
+
|
|
2979
|
+
if (audioEnabled && timingData) {
|
|
2980
|
+
// Try to play audio
|
|
2981
|
+
audioProgressContainer.style.display = 'block';
|
|
2982
|
+
|
|
2983
|
+
try {
|
|
2984
|
+
await narrationAudio.play();
|
|
2985
|
+
console.log('Audio playing');
|
|
2986
|
+
} catch (err) {
|
|
2987
|
+
console.error('Audio playback failed:', err);
|
|
2988
|
+
// Fall back to silent mode
|
|
2989
|
+
audioEnabled = false;
|
|
2990
|
+
audioToggleBtn.textContent = '🔇 NO AUDIO';
|
|
2991
|
+
audioToggleBtn.classList.add('disabled');
|
|
2992
|
+
audioProgressContainer.style.display = 'none';
|
|
2993
|
+
startSilentAutoPlay();
|
|
2994
|
+
}
|
|
2995
|
+
} else {
|
|
2996
|
+
// Silent auto-play
|
|
2997
|
+
startSilentAutoPlay();
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
|
|
3001
|
+
// Stop auto-play
|
|
3002
|
+
function stopAutoPlay() {
|
|
3003
|
+
isPlaying = false;
|
|
3004
|
+
playBtn.classList.remove('active');
|
|
3005
|
+
playBtn.textContent = '▶ AUTO PLAY';
|
|
3006
|
+
|
|
3007
|
+
if (autoPlayInterval) {
|
|
3008
|
+
clearInterval(autoPlayInterval);
|
|
3009
|
+
autoPlayInterval = null;
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
narrationAudio.pause();
|
|
3013
|
+
narrationAudio.currentTime = 0;
|
|
3014
|
+
audioProgressContainer.style.display = 'none';
|
|
3015
|
+
removeHighlight();
|
|
3016
|
+
highlightIndex = 0;
|
|
3017
|
+
currentSectionIdx = 0;
|
|
3018
|
+
|
|
3019
|
+
// Resume the intro lighting animation
|
|
3020
|
+
if (window.introLightingTL) {
|
|
3021
|
+
window.introLightingTL.restart();
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
// Audio ended
|
|
3026
|
+
function onAudioEnded() {
|
|
3027
|
+
stopAutoPlay();
|
|
3028
|
+
setTimeout(() => {
|
|
3029
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
3030
|
+
}, 500);
|
|
3031
|
+
}
|
|
3032
|
+
|
|
3033
|
+
// Toggle audio on/off
|
|
3034
|
+
function toggleAudio() {
|
|
3035
|
+
audioEnabled = !audioEnabled;
|
|
3036
|
+
if (audioEnabled) {
|
|
3037
|
+
audioToggleBtn.textContent = '🔊 WITH AUDIO';
|
|
3038
|
+
audioToggleBtn.classList.remove('disabled');
|
|
3039
|
+
} else {
|
|
3040
|
+
audioToggleBtn.textContent = '🔇 SILENT';
|
|
3041
|
+
audioToggleBtn.classList.add('disabled');
|
|
3042
|
+
}
|
|
3043
|
+
|
|
3044
|
+
// If currently playing, adjust
|
|
3045
|
+
if (isPlaying) {
|
|
3046
|
+
if (audioEnabled) {
|
|
3047
|
+
narrationAudio.play().catch(() => {});
|
|
3048
|
+
audioProgressContainer.style.display = 'block';
|
|
3049
|
+
if (autoPlayInterval) {
|
|
3050
|
+
clearInterval(autoPlayInterval);
|
|
3051
|
+
autoPlayInterval = null;
|
|
3052
|
+
}
|
|
3053
|
+
} else {
|
|
3054
|
+
narrationAudio.pause();
|
|
3055
|
+
audioProgressContainer.style.display = 'none';
|
|
3056
|
+
startSilentAutoPlay();
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
// Event listeners
|
|
3062
|
+
playBtn.addEventListener('click', () => {
|
|
3063
|
+
if (isPlaying) {
|
|
3064
|
+
stopAutoPlay();
|
|
3065
|
+
} else {
|
|
3066
|
+
startAutoPlay();
|
|
2785
3067
|
}
|
|
2786
3068
|
});
|
|
3069
|
+
audioToggleBtn.addEventListener('click', toggleAudio);
|
|
3070
|
+
narrationAudio.addEventListener('timeupdate', onAudioTimeUpdate);
|
|
3071
|
+
narrationAudio.addEventListener('ended', onAudioEnded);
|
|
3072
|
+
audioProgressBar.addEventListener('click', onProgressBarClick);
|
|
3073
|
+
|
|
3074
|
+
// Check if audio file is accessible
|
|
3075
|
+
narrationAudio.addEventListener('error', (e) => {
|
|
3076
|
+
console.error('Audio error:', e);
|
|
3077
|
+
audioEnabled = false;
|
|
3078
|
+
audioToggleBtn.textContent = '🔇 NO AUDIO';
|
|
3079
|
+
audioToggleBtn.classList.add('disabled');
|
|
3080
|
+
audioToggleBtn.title = 'Audio file not accessible (run from a web server)';
|
|
3081
|
+
});
|
|
3082
|
+
|
|
3083
|
+
// Preload timing data
|
|
3084
|
+
loadTimingData();
|
|
2787
3085
|
|
|
2788
3086
|
// ============================================
|
|
2789
3087
|
// RESET BUTTON
|
|
@@ -2984,234 +3282,6 @@
|
|
|
2984
3282
|
});
|
|
2985
3283
|
});
|
|
2986
3284
|
|
|
2987
|
-
// ============================================
|
|
2988
|
-
// AUDIO NARRATION CONTROLLER
|
|
2989
|
-
// ============================================
|
|
2990
|
-
const narrateBtn = document.getElementById('narrateBtn');
|
|
2991
|
-
const narrationAudio = document.getElementById('narrationAudio');
|
|
2992
|
-
const audioProgressContainer = document.getElementById('audioProgressContainer');
|
|
2993
|
-
const audioProgressBar = document.getElementById('audioProgressBar');
|
|
2994
|
-
const audioProgressFill = document.getElementById('audioProgressFill');
|
|
2995
|
-
const audioCurrentTime = document.getElementById('audioCurrentTime');
|
|
2996
|
-
const audioTotalTime = document.getElementById('audioTotalTime');
|
|
2997
|
-
const audioSectionMarkers = document.getElementById('audioSectionMarkers');
|
|
2998
|
-
|
|
2999
|
-
let isNarrating = false;
|
|
3000
|
-
let timingData = null;
|
|
3001
|
-
let currentHighlight = null;
|
|
3002
|
-
let highlightIndex = 0;
|
|
3003
|
-
let sectionIndex = 0;
|
|
3004
|
-
|
|
3005
|
-
// Format seconds to MM:SS
|
|
3006
|
-
function formatTime(seconds) {
|
|
3007
|
-
const mins = Math.floor(seconds / 60);
|
|
3008
|
-
const secs = Math.floor(seconds % 60);
|
|
3009
|
-
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
3010
|
-
}
|
|
3011
|
-
|
|
3012
|
-
// Load timing data
|
|
3013
|
-
async function loadTimingData() {
|
|
3014
|
-
if (timingData) return timingData;
|
|
3015
|
-
try {
|
|
3016
|
-
const response = await fetch('audio/narration-timing.json');
|
|
3017
|
-
timingData = await response.json();
|
|
3018
|
-
|
|
3019
|
-
// Update total time display
|
|
3020
|
-
if (audioTotalTime) {
|
|
3021
|
-
audioTotalTime.textContent = formatTime(timingData.duration);
|
|
3022
|
-
}
|
|
3023
|
-
|
|
3024
|
-
// Create section markers on progress bar
|
|
3025
|
-
if (audioSectionMarkers && timingData.sections) {
|
|
3026
|
-
timingData.sections.forEach((section, i) => {
|
|
3027
|
-
if (i === 0) return; // Skip first section (intro at 0)
|
|
3028
|
-
const marker = document.createElement('div');
|
|
3029
|
-
marker.className = 'audio-section-marker';
|
|
3030
|
-
marker.style.left = `${(section.timestamp / timingData.duration) * 100}%`;
|
|
3031
|
-
marker.title = section.id.toUpperCase();
|
|
3032
|
-
audioSectionMarkers.appendChild(marker);
|
|
3033
|
-
});
|
|
3034
|
-
}
|
|
3035
|
-
|
|
3036
|
-
console.log('Timing data loaded:', timingData.sections.length, 'sections,', timingData.highlights.length, 'highlights');
|
|
3037
|
-
return timingData;
|
|
3038
|
-
} catch (error) {
|
|
3039
|
-
console.error('Failed to load timing data:', error);
|
|
3040
|
-
return null;
|
|
3041
|
-
}
|
|
3042
|
-
}
|
|
3043
|
-
|
|
3044
|
-
// Apply highlight to an element
|
|
3045
|
-
function applyHighlight(selector) {
|
|
3046
|
-
// Remove previous highlight
|
|
3047
|
-
removeHighlight();
|
|
3048
|
-
|
|
3049
|
-
try {
|
|
3050
|
-
const el = document.querySelector(selector);
|
|
3051
|
-
if (el) {
|
|
3052
|
-
currentHighlight = el;
|
|
3053
|
-
el.classList.add('audio-highlighted');
|
|
3054
|
-
|
|
3055
|
-
// Scroll element into view
|
|
3056
|
-
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
3057
|
-
|
|
3058
|
-
// GSAP enhancement
|
|
3059
|
-
gsap.fromTo(el,
|
|
3060
|
-
{ boxShadow: '0 0 0px var(--accent-red-glow)' },
|
|
3061
|
-
{
|
|
3062
|
-
boxShadow: '0 0 40px var(--accent-red-glow), 0 0 80px var(--accent-red-glow)',
|
|
3063
|
-
duration: 0.4,
|
|
3064
|
-
ease: 'power2.out'
|
|
3065
|
-
}
|
|
3066
|
-
);
|
|
3067
|
-
}
|
|
3068
|
-
} catch (e) {
|
|
3069
|
-
console.warn('Could not highlight:', selector, e);
|
|
3070
|
-
}
|
|
3071
|
-
}
|
|
3072
|
-
|
|
3073
|
-
// Remove current highlight
|
|
3074
|
-
function removeHighlight() {
|
|
3075
|
-
if (currentHighlight) {
|
|
3076
|
-
currentHighlight.classList.remove('audio-highlighted');
|
|
3077
|
-
gsap.to(currentHighlight, {
|
|
3078
|
-
boxShadow: 'none',
|
|
3079
|
-
duration: 0.3
|
|
3080
|
-
});
|
|
3081
|
-
currentHighlight = null;
|
|
3082
|
-
}
|
|
3083
|
-
}
|
|
3084
|
-
|
|
3085
|
-
// Scroll to section
|
|
3086
|
-
function scrollToSection(sectionId) {
|
|
3087
|
-
const el = document.getElementById(sectionId);
|
|
3088
|
-
if (el) {
|
|
3089
|
-
el.scrollIntoView({ behavior: 'smooth' });
|
|
3090
|
-
}
|
|
3091
|
-
}
|
|
3092
|
-
|
|
3093
|
-
// Handle audio time update
|
|
3094
|
-
function onTimeUpdate() {
|
|
3095
|
-
if (!timingData) return;
|
|
3096
|
-
|
|
3097
|
-
const currentTime = narrationAudio.currentTime;
|
|
3098
|
-
|
|
3099
|
-
// Update progress bar
|
|
3100
|
-
const progress = (currentTime / timingData.duration) * 100;
|
|
3101
|
-
audioProgressFill.style.width = `${progress}%`;
|
|
3102
|
-
|
|
3103
|
-
// Update time display
|
|
3104
|
-
if (audioCurrentTime) {
|
|
3105
|
-
audioCurrentTime.textContent = formatTime(currentTime);
|
|
3106
|
-
}
|
|
3107
|
-
|
|
3108
|
-
// Check for section changes
|
|
3109
|
-
for (let i = timingData.sections.length - 1; i >= 0; i--) {
|
|
3110
|
-
if (currentTime >= timingData.sections[i].timestamp) {
|
|
3111
|
-
if (i !== sectionIndex) {
|
|
3112
|
-
sectionIndex = i;
|
|
3113
|
-
const section = timingData.sections[i];
|
|
3114
|
-
console.log('Section:', section.id);
|
|
3115
|
-
// Optionally scroll to section (only on major section changes)
|
|
3116
|
-
// scrollToSection(section.id);
|
|
3117
|
-
}
|
|
3118
|
-
break;
|
|
3119
|
-
}
|
|
3120
|
-
}
|
|
3121
|
-
|
|
3122
|
-
// Check for highlight triggers
|
|
3123
|
-
for (let i = highlightIndex; i < timingData.highlights.length; i++) {
|
|
3124
|
-
const highlight = timingData.highlights[i];
|
|
3125
|
-
if (currentTime >= highlight.timestamp) {
|
|
3126
|
-
if (i !== highlightIndex || !currentHighlight) {
|
|
3127
|
-
highlightIndex = i;
|
|
3128
|
-
applyHighlight(highlight.selector);
|
|
3129
|
-
}
|
|
3130
|
-
} else {
|
|
3131
|
-
break;
|
|
3132
|
-
}
|
|
3133
|
-
}
|
|
3134
|
-
}
|
|
3135
|
-
|
|
3136
|
-
// Handle seek on progress bar click
|
|
3137
|
-
function onProgressBarClick(e) {
|
|
3138
|
-
if (!timingData || !narrationAudio.duration) return;
|
|
3139
|
-
|
|
3140
|
-
const rect = audioProgressBar.getBoundingClientRect();
|
|
3141
|
-
const clickX = e.clientX - rect.left;
|
|
3142
|
-
const percentage = clickX / rect.width;
|
|
3143
|
-
const seekTime = percentage * timingData.duration;
|
|
3144
|
-
|
|
3145
|
-
narrationAudio.currentTime = seekTime;
|
|
3146
|
-
|
|
3147
|
-
// Reset highlight tracking
|
|
3148
|
-
highlightIndex = 0;
|
|
3149
|
-
for (let i = 0; i < timingData.highlights.length; i++) {
|
|
3150
|
-
if (timingData.highlights[i].timestamp <= seekTime) {
|
|
3151
|
-
highlightIndex = i;
|
|
3152
|
-
} else {
|
|
3153
|
-
break;
|
|
3154
|
-
}
|
|
3155
|
-
}
|
|
3156
|
-
}
|
|
3157
|
-
|
|
3158
|
-
// Start/Stop narration
|
|
3159
|
-
async function toggleNarration() {
|
|
3160
|
-
if (!isNarrating) {
|
|
3161
|
-
// Start narration
|
|
3162
|
-
await loadTimingData();
|
|
3163
|
-
|
|
3164
|
-
narrationAudio.play().then(() => {
|
|
3165
|
-
isNarrating = true;
|
|
3166
|
-
narrateBtn.classList.add('active');
|
|
3167
|
-
narrateBtn.textContent = '⏸ STOP';
|
|
3168
|
-
audioProgressContainer.style.display = 'block';
|
|
3169
|
-
|
|
3170
|
-
// Scroll to top for intro
|
|
3171
|
-
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
3172
|
-
}).catch(err => {
|
|
3173
|
-
console.error('Failed to play audio:', err);
|
|
3174
|
-
alert('Could not play audio. Make sure narration.mp3 exists in the audio folder.');
|
|
3175
|
-
});
|
|
3176
|
-
} else {
|
|
3177
|
-
// Stop narration
|
|
3178
|
-
narrationAudio.pause();
|
|
3179
|
-
narrationAudio.currentTime = 0;
|
|
3180
|
-
isNarrating = false;
|
|
3181
|
-
narrateBtn.classList.remove('active');
|
|
3182
|
-
narrateBtn.textContent = '🔊 NARRATE';
|
|
3183
|
-
audioProgressContainer.style.display = 'none';
|
|
3184
|
-
removeHighlight();
|
|
3185
|
-
highlightIndex = 0;
|
|
3186
|
-
sectionIndex = 0;
|
|
3187
|
-
}
|
|
3188
|
-
}
|
|
3189
|
-
|
|
3190
|
-
// Audio ended
|
|
3191
|
-
function onAudioEnded() {
|
|
3192
|
-
isNarrating = false;
|
|
3193
|
-
narrateBtn.classList.remove('active');
|
|
3194
|
-
narrateBtn.textContent = '🔊 NARRATE';
|
|
3195
|
-
audioProgressContainer.style.display = 'none';
|
|
3196
|
-
removeHighlight();
|
|
3197
|
-
highlightIndex = 0;
|
|
3198
|
-
sectionIndex = 0;
|
|
3199
|
-
|
|
3200
|
-
// Scroll back to top
|
|
3201
|
-
setTimeout(() => {
|
|
3202
|
-
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
3203
|
-
}, 500);
|
|
3204
|
-
}
|
|
3205
|
-
|
|
3206
|
-
// Event listeners
|
|
3207
|
-
narrateBtn.addEventListener('click', toggleNarration);
|
|
3208
|
-
narrationAudio.addEventListener('timeupdate', onTimeUpdate);
|
|
3209
|
-
narrationAudio.addEventListener('ended', onAudioEnded);
|
|
3210
|
-
audioProgressBar.addEventListener('click', onProgressBarClick);
|
|
3211
|
-
|
|
3212
|
-
// Preload timing data on page load
|
|
3213
|
-
loadTimingData();
|
|
3214
|
-
|
|
3215
3285
|
</script>
|
|
3216
3286
|
|
|
3217
3287
|
</body>
|
package/package.json
CHANGED