@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.
@@ -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
- box-shadow: 0 0 30px var(--accent-red-glow), 0 0 60px var(--accent-red-glow) !important;
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="narrateBtn">🔊 NARRATE</button>
1669
- <button class="nav-btn" id="playBtn">AUTO PLAY</button>
1670
- <button class="nav-btn" id="resetBtn">RESTART</button>
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.5</div>
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
- const lightingTL = gsap.timeline({
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 BUTTON
2796
+ // UNIFIED AUTO-PLAY + AUDIO SYSTEM
2762
2797
  // ============================================
2763
- document.getElementById('playBtn').addEventListener('click', () => {
2764
- if (!isPlaying) {
2765
- isPlaying = true;
2766
- document.getElementById('playBtn').textContent = 'PLAYING...';
2767
- document.getElementById('playBtn').style.background = 'var(--white)';
2768
- document.getElementById('playBtn').style.color = 'var(--black)';
2769
-
2770
- let currentSection = 0;
2771
- const autoScroll = setInterval(() => {
2772
- if (currentSection < sections.length) {
2773
- document.getElementById(sections[currentSection]).scrollIntoView({
2774
- behavior: 'smooth'
2775
- });
2776
- currentSection++;
2777
- } else {
2778
- clearInterval(autoScroll);
2779
- isPlaying = false;
2780
- document.getElementById('playBtn').textContent = 'AUTO PLAY';
2781
- document.getElementById('playBtn').style.background = 'transparent';
2782
- document.getElementById('playBtn').style.color = 'var(--white)';
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
- }, 5000);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hustle-together/api-dev-tools",
3
- "version": "2.0.5",
3
+ "version": "2.0.7",
4
4
  "description": "Interview-driven API development workflow for Claude Code - Automates research, testing, and documentation",
5
5
  "main": "bin/cli.js",
6
6
  "bin": {