@hustle-together/api-dev-tools 2.0.5 → 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.
@@ -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
  ============================================ */
@@ -1665,9 +1678,9 @@
1665
1678
 
1666
1679
  <!-- Navigation -->
1667
1680
  <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>
1681
+ <button class="nav-btn" id="playBtn">▶ AUTO PLAY</button>
1682
+ <button class="nav-btn" id="audioToggleBtn" title="Toggle narration audio">🔊 WITH AUDIO</button>
1683
+ <button class="nav-btn" id="resetBtn">↺ RESTART</button>
1671
1684
  </nav>
1672
1685
 
1673
1686
  <!-- Audio Narration Player (hidden) -->
@@ -1700,7 +1713,7 @@
1700
1713
  <span class="hustle-word hustle-highlight">API-DEV-TOOLS</span>
1701
1714
  </div>
1702
1715
  <div class="package-name" id="packageName">@hustle-together/api-dev-tools</div>
1703
- <div class="version" id="versionText">v2.0.5</div>
1716
+ <div class="version" id="versionText">v2.0.6</div>
1704
1717
  <p class="tagline">"Hustle together. Share resources. Build stronger."<span class="cursor"></span></p>
1705
1718
 
1706
1719
  <div class="intro-text">
@@ -2758,32 +2771,272 @@
2758
2771
  .to('.made-with', { opacity: 1, duration: 0.5 });
2759
2772
 
2760
2773
  // ============================================
2761
- // AUTO-PLAY BUTTON
2774
+ // UNIFIED AUTO-PLAY + AUDIO SYSTEM
2762
2775
  // ============================================
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)';
2776
+ const playBtn = document.getElementById('playBtn');
2777
+ const audioToggleBtn = document.getElementById('audioToggleBtn');
2778
+ const narrationAudio = document.getElementById('narrationAudio');
2779
+ const audioProgressContainer = document.getElementById('audioProgressContainer');
2780
+ const audioProgressBar = document.getElementById('audioProgressBar');
2781
+ const audioProgressFill = document.getElementById('audioProgressFill');
2782
+ const audioCurrentTime = document.getElementById('audioCurrentTime');
2783
+ const audioTotalTime = document.getElementById('audioTotalTime');
2784
+ const audioSectionMarkers = document.getElementById('audioSectionMarkers');
2785
+
2786
+ let audioEnabled = true; // Audio on by default
2787
+ let timingData = null;
2788
+ let currentHighlight = null;
2789
+ let highlightIndex = 0;
2790
+ let currentSectionIdx = 0;
2791
+ let autoPlayInterval = null;
2792
+
2793
+ // Format seconds to MM:SS
2794
+ function formatTime(seconds) {
2795
+ const mins = Math.floor(seconds / 60);
2796
+ const secs = Math.floor(seconds % 60);
2797
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
2798
+ }
2799
+
2800
+ // Load timing data
2801
+ async function loadTimingData() {
2802
+ if (timingData) return timingData;
2803
+ try {
2804
+ const response = await fetch('audio/narration-timing.json');
2805
+ if (!response.ok) throw new Error('Failed to load timing data');
2806
+ timingData = await response.json();
2807
+
2808
+ // Update total time display
2809
+ if (audioTotalTime) {
2810
+ audioTotalTime.textContent = formatTime(timingData.duration);
2811
+ }
2812
+
2813
+ // Create section markers on progress bar
2814
+ if (audioSectionMarkers && timingData.sections) {
2815
+ audioSectionMarkers.innerHTML = '';
2816
+ timingData.sections.forEach((section, i) => {
2817
+ if (i === 0) return;
2818
+ const marker = document.createElement('div');
2819
+ marker.className = 'audio-section-marker';
2820
+ marker.style.left = `${(section.timestamp / timingData.duration) * 100}%`;
2821
+ marker.title = section.id.toUpperCase();
2822
+ audioSectionMarkers.appendChild(marker);
2823
+ });
2824
+ }
2825
+
2826
+ console.log('Timing data loaded:', timingData.sections.length, 'sections,', timingData.highlights.length, 'highlights');
2827
+ return timingData;
2828
+ } catch (error) {
2829
+ console.error('Failed to load timing data:', error);
2830
+ return null;
2831
+ }
2832
+ }
2833
+
2834
+ // Apply highlight to an element
2835
+ function applyHighlight(selector) {
2836
+ removeHighlight();
2837
+ try {
2838
+ const el = document.querySelector(selector);
2839
+ if (el) {
2840
+ currentHighlight = el;
2841
+ el.classList.add('audio-highlighted');
2842
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' });
2843
+ gsap.fromTo(el,
2844
+ { boxShadow: '0 0 0px var(--accent-red-glow)' },
2845
+ { boxShadow: '0 0 40px var(--accent-red-glow), 0 0 80px var(--accent-red-glow)', duration: 0.4, ease: 'power2.out' }
2846
+ );
2847
+ }
2848
+ } catch (e) {
2849
+ console.warn('Could not highlight:', selector, e);
2850
+ }
2851
+ }
2852
+
2853
+ // Remove current highlight
2854
+ function removeHighlight() {
2855
+ if (currentHighlight) {
2856
+ currentHighlight.classList.remove('audio-highlighted');
2857
+ gsap.to(currentHighlight, { boxShadow: 'none', duration: 0.3 });
2858
+ currentHighlight = null;
2859
+ }
2860
+ }
2861
+
2862
+ // Handle audio time update (syncs highlights with narration)
2863
+ function onAudioTimeUpdate() {
2864
+ if (!timingData || !audioEnabled) return;
2865
+
2866
+ const currentTime = narrationAudio.currentTime;
2867
+
2868
+ // Update progress bar
2869
+ const progress = (currentTime / timingData.duration) * 100;
2870
+ audioProgressFill.style.width = `${progress}%`;
2871
+
2872
+ // Update time display
2873
+ if (audioCurrentTime) {
2874
+ audioCurrentTime.textContent = formatTime(currentTime);
2875
+ }
2876
+
2877
+ // Check for highlight triggers
2878
+ for (let i = highlightIndex; i < timingData.highlights.length; i++) {
2879
+ const highlight = timingData.highlights[i];
2880
+ if (currentTime >= highlight.timestamp) {
2881
+ if (i !== highlightIndex || !currentHighlight) {
2882
+ highlightIndex = i;
2883
+ applyHighlight(highlight.selector);
2884
+ }
2885
+ } else {
2886
+ break;
2887
+ }
2888
+ }
2889
+ }
2890
+
2891
+ // Handle seek on progress bar click
2892
+ function onProgressBarClick(e) {
2893
+ if (!timingData || !narrationAudio.duration) return;
2894
+
2895
+ const rect = audioProgressBar.getBoundingClientRect();
2896
+ const clickX = e.clientX - rect.left;
2897
+ const percentage = clickX / rect.width;
2898
+ const seekTime = percentage * timingData.duration;
2899
+
2900
+ narrationAudio.currentTime = seekTime;
2901
+
2902
+ // Reset highlight tracking
2903
+ highlightIndex = 0;
2904
+ for (let i = 0; i < timingData.highlights.length; i++) {
2905
+ if (timingData.highlights[i].timestamp <= seekTime) {
2906
+ highlightIndex = i;
2907
+ } else {
2908
+ break;
2909
+ }
2910
+ }
2911
+ }
2912
+
2913
+ // Silent auto-play (no audio, timed sections)
2914
+ function startSilentAutoPlay() {
2915
+ let sectionIdx = 0;
2916
+ autoPlayInterval = setInterval(() => {
2917
+ if (sectionIdx < sections.length) {
2918
+ document.getElementById(sections[sectionIdx]).scrollIntoView({ behavior: 'smooth' });
2919
+ sectionIdx++;
2920
+ } else {
2921
+ stopAutoPlay();
2922
+ }
2923
+ }, 5000);
2924
+ }
2925
+
2926
+ // Start auto-play
2927
+ async function startAutoPlay() {
2928
+ if (isPlaying) return;
2929
+
2930
+ isPlaying = true;
2931
+ playBtn.classList.add('active');
2932
+ playBtn.textContent = '⏸ STOP';
2933
+
2934
+ // Scroll to top first
2935
+ window.scrollTo({ top: 0, behavior: 'smooth' });
2936
+
2937
+ await loadTimingData();
2938
+
2939
+ if (audioEnabled && timingData) {
2940
+ // Try to play audio
2941
+ audioProgressContainer.style.display = 'block';
2942
+
2943
+ try {
2944
+ await narrationAudio.play();
2945
+ console.log('Audio playing');
2946
+ } catch (err) {
2947
+ console.error('Audio playback failed:', err);
2948
+ // Fall back to silent mode
2949
+ audioEnabled = false;
2950
+ audioToggleBtn.textContent = '🔇 NO AUDIO';
2951
+ audioToggleBtn.classList.add('disabled');
2952
+ audioProgressContainer.style.display = 'none';
2953
+ startSilentAutoPlay();
2954
+ }
2955
+ } else {
2956
+ // Silent auto-play
2957
+ startSilentAutoPlay();
2958
+ }
2959
+ }
2960
+
2961
+ // Stop auto-play
2962
+ function stopAutoPlay() {
2963
+ isPlaying = false;
2964
+ playBtn.classList.remove('active');
2965
+ playBtn.textContent = '▶ AUTO PLAY';
2966
+
2967
+ if (autoPlayInterval) {
2968
+ clearInterval(autoPlayInterval);
2969
+ autoPlayInterval = null;
2970
+ }
2971
+
2972
+ narrationAudio.pause();
2973
+ narrationAudio.currentTime = 0;
2974
+ audioProgressContainer.style.display = 'none';
2975
+ removeHighlight();
2976
+ highlightIndex = 0;
2977
+ currentSectionIdx = 0;
2978
+ }
2979
+
2980
+ // Audio ended
2981
+ function onAudioEnded() {
2982
+ stopAutoPlay();
2983
+ setTimeout(() => {
2984
+ window.scrollTo({ top: 0, behavior: 'smooth' });
2985
+ }, 500);
2986
+ }
2987
+
2988
+ // Toggle audio on/off
2989
+ function toggleAudio() {
2990
+ audioEnabled = !audioEnabled;
2991
+ if (audioEnabled) {
2992
+ audioToggleBtn.textContent = '🔊 WITH AUDIO';
2993
+ audioToggleBtn.classList.remove('disabled');
2994
+ } else {
2995
+ audioToggleBtn.textContent = '🔇 SILENT';
2996
+ audioToggleBtn.classList.add('disabled');
2997
+ }
2998
+
2999
+ // If currently playing, adjust
3000
+ if (isPlaying) {
3001
+ if (audioEnabled) {
3002
+ narrationAudio.play().catch(() => {});
3003
+ audioProgressContainer.style.display = 'block';
3004
+ if (autoPlayInterval) {
3005
+ clearInterval(autoPlayInterval);
3006
+ autoPlayInterval = null;
2783
3007
  }
2784
- }, 5000);
3008
+ } else {
3009
+ narrationAudio.pause();
3010
+ audioProgressContainer.style.display = 'none';
3011
+ startSilentAutoPlay();
3012
+ }
3013
+ }
3014
+ }
3015
+
3016
+ // Event listeners
3017
+ playBtn.addEventListener('click', () => {
3018
+ if (isPlaying) {
3019
+ stopAutoPlay();
3020
+ } else {
3021
+ startAutoPlay();
2785
3022
  }
2786
3023
  });
3024
+ audioToggleBtn.addEventListener('click', toggleAudio);
3025
+ narrationAudio.addEventListener('timeupdate', onAudioTimeUpdate);
3026
+ narrationAudio.addEventListener('ended', onAudioEnded);
3027
+ audioProgressBar.addEventListener('click', onProgressBarClick);
3028
+
3029
+ // Check if audio file is accessible
3030
+ narrationAudio.addEventListener('error', (e) => {
3031
+ console.error('Audio error:', e);
3032
+ audioEnabled = false;
3033
+ audioToggleBtn.textContent = '🔇 NO AUDIO';
3034
+ audioToggleBtn.classList.add('disabled');
3035
+ audioToggleBtn.title = 'Audio file not accessible (run from a web server)';
3036
+ });
3037
+
3038
+ // Preload timing data
3039
+ loadTimingData();
2787
3040
 
2788
3041
  // ============================================
2789
3042
  // RESET BUTTON
@@ -2984,234 +3237,6 @@
2984
3237
  });
2985
3238
  });
2986
3239
 
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
3240
  </script>
3216
3241
 
3217
3242
  </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.6",
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": {