@hustle-together/api-dev-tools 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.
@@ -0,0 +1,48 @@
1
+ {
2
+ "generated": "2025-12-08T00:00:00.000Z",
3
+ "duration": 300,
4
+ "wordCount": 850,
5
+ "sections": [
6
+ { "id": "intro", "timestamp": 0 },
7
+ { "id": "problems", "timestamp": 35 },
8
+ { "id": "solution", "timestamp": 95 },
9
+ { "id": "workflow", "timestamp": 140 },
10
+ { "id": "installation", "timestamp": 220 },
11
+ { "id": "credits", "timestamp": 260 },
12
+ { "id": "outro", "timestamp": 285 }
13
+ ],
14
+ "highlights": [
15
+ { "selector": "#hustleBrand", "section": "intro", "timestamp": 2 },
16
+ { "selector": "[data-phase='research']", "section": "intro", "timestamp": 8 },
17
+ { "selector": "[data-phase='interview']", "section": "intro", "timestamp": 14 },
18
+ { "selector": "[data-phase='test']", "section": "intro", "timestamp": 20 },
19
+ { "selector": "[data-phase='code']", "section": "intro", "timestamp": 26 },
20
+ { "selector": "[data-phase='docs']", "section": "intro", "timestamp": 32 },
21
+ { "selector": "#problems h2", "section": "problems", "timestamp": 36 },
22
+ { "selector": ".gap-item:nth-child(1)", "section": "problems", "timestamp": 42 },
23
+ { "selector": ".gap-item:nth-child(2)", "section": "problems", "timestamp": 54 },
24
+ { "selector": ".gap-item:nth-child(3)", "section": "problems", "timestamp": 66 },
25
+ { "selector": ".gap-item:nth-child(4)", "section": "problems", "timestamp": 78 },
26
+ { "selector": ".gap-item:nth-child(5)", "section": "problems", "timestamp": 88 },
27
+ { "selector": "#solution h2", "section": "solution", "timestamp": 96 },
28
+ { "selector": ".hook-box:nth-child(1)", "section": "solution", "timestamp": 104 },
29
+ { "selector": ".hook-box:nth-child(2)", "section": "solution", "timestamp": 118 },
30
+ { "selector": ".hook-box:nth-child(3)", "section": "solution", "timestamp": 130 },
31
+ { "selector": "#workflow h2", "section": "workflow", "timestamp": 141 },
32
+ { "selector": ".workflow-phase:nth-child(1)", "section": "workflow", "timestamp": 148 },
33
+ { "selector": ".workflow-phase:nth-child(2)", "section": "workflow", "timestamp": 155 },
34
+ { "selector": ".workflow-phase:nth-child(3)", "section": "workflow", "timestamp": 162 },
35
+ { "selector": ".workflow-phase:nth-child(4)", "section": "workflow", "timestamp": 172 },
36
+ { "selector": ".workflow-phase:nth-child(5)", "section": "workflow", "timestamp": 180 },
37
+ { "selector": ".workflow-phase:nth-child(6)", "section": "workflow", "timestamp": 188 },
38
+ { "selector": ".workflow-phase:nth-child(7)", "section": "workflow", "timestamp": 196 },
39
+ { "selector": ".workflow-phase:nth-child(8)", "section": "workflow", "timestamp": 204 },
40
+ { "selector": ".workflow-phase:nth-child(9)", "section": "workflow", "timestamp": 211 },
41
+ { "selector": ".workflow-phase:nth-child(10)", "section": "workflow", "timestamp": 218 },
42
+ { "selector": "#installation h2", "section": "installation", "timestamp": 221 },
43
+ { "selector": ".install-command", "section": "installation", "timestamp": 228 },
44
+ { "selector": "#credits h2", "section": "credits", "timestamp": 261 },
45
+ { "selector": "#intro", "section": "outro", "timestamp": 286 }
46
+ ],
47
+ "words": []
48
+ }
Binary file
@@ -500,6 +500,95 @@
500
500
  box-shadow: 0 0 15px var(--accent-red-glow);
501
501
  }
502
502
 
503
+ .nav-btn.active {
504
+ background: var(--accent-red);
505
+ color: #fff;
506
+ border-color: var(--accent-red);
507
+ }
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
+
522
+ /* ============================================
523
+ AUDIO NARRATION PLAYER
524
+ ============================================ */
525
+ .audio-progress-container {
526
+ position: fixed;
527
+ bottom: 60px;
528
+ left: 50%;
529
+ transform: translateX(-50%);
530
+ width: 80%;
531
+ max-width: 600px;
532
+ background: rgba(0, 0, 0, 0.9);
533
+ border: 1px dashed var(--grey);
534
+ padding: 15px 20px;
535
+ z-index: 1001;
536
+ border-radius: 0;
537
+ }
538
+
539
+ .audio-time-display {
540
+ display: flex;
541
+ justify-content: space-between;
542
+ font-size: 0.75rem;
543
+ color: var(--grey);
544
+ margin-bottom: 10px;
545
+ font-family: 'Courier New', monospace;
546
+ }
547
+
548
+ .audio-progress-bar {
549
+ position: relative;
550
+ height: 6px;
551
+ background: var(--dark-grey);
552
+ cursor: pointer;
553
+ }
554
+
555
+ .audio-progress-fill {
556
+ height: 100%;
557
+ background: var(--accent-red);
558
+ width: 0%;
559
+ transition: width 0.1s linear;
560
+ }
561
+
562
+ .audio-section-markers {
563
+ position: absolute;
564
+ top: 0;
565
+ left: 0;
566
+ width: 100%;
567
+ height: 100%;
568
+ pointer-events: none;
569
+ }
570
+
571
+ .audio-section-marker {
572
+ position: absolute;
573
+ top: -3px;
574
+ width: 2px;
575
+ height: 12px;
576
+ background: var(--white);
577
+ opacity: 0.5;
578
+ }
579
+
580
+ .audio-section-marker:hover {
581
+ opacity: 1;
582
+ }
583
+
584
+ /* Audio highlight effect for elements being narrated */
585
+ .audio-highlighted {
586
+ box-shadow: 0 0 30px var(--accent-red-glow), 0 0 60px var(--accent-red-glow) !important;
587
+ border-color: var(--accent-red) !important;
588
+ transform: scale(1.02);
589
+ transition: all 0.3s ease !important;
590
+ }
591
+
503
592
  /* Progress indicator */
504
593
  .progress-bar {
505
594
  position: fixed;
@@ -1589,10 +1678,28 @@
1589
1678
 
1590
1679
  <!-- Navigation -->
1591
1680
  <nav class="nav">
1592
- <button class="nav-btn" id="playBtn">AUTO PLAY</button>
1593
- <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>
1594
1684
  </nav>
1595
1685
 
1686
+ <!-- Audio Narration Player (hidden) -->
1687
+ <audio id="narrationAudio" preload="auto">
1688
+ <source src="audio/narration.mp3" type="audio/mpeg">
1689
+ </audio>
1690
+
1691
+ <!-- Audio Progress Bar -->
1692
+ <div class="audio-progress-container" id="audioProgressContainer" style="display: none;">
1693
+ <div class="audio-time-display">
1694
+ <span id="audioCurrentTime">0:00</span>
1695
+ <span id="audioTotalTime">0:00</span>
1696
+ </div>
1697
+ <div class="audio-progress-bar" id="audioProgressBar">
1698
+ <div class="audio-progress-fill" id="audioProgressFill"></div>
1699
+ <div class="audio-section-markers" id="audioSectionMarkers"></div>
1700
+ </div>
1701
+ </div>
1702
+
1596
1703
  <!-- Section Indicators -->
1597
1704
  <div class="section-indicator" id="sectionIndicator"></div>
1598
1705
 
@@ -1606,7 +1713,7 @@
1606
1713
  <span class="hustle-word hustle-highlight">API-DEV-TOOLS</span>
1607
1714
  </div>
1608
1715
  <div class="package-name" id="packageName">@hustle-together/api-dev-tools</div>
1609
- <div class="version" id="versionText">v2.0.4</div>
1716
+ <div class="version" id="versionText">v2.0.6</div>
1610
1717
  <p class="tagline">"Hustle together. Share resources. Build stronger."<span class="cursor"></span></p>
1611
1718
 
1612
1719
  <div class="intro-text">
@@ -2664,32 +2771,272 @@
2664
2771
  .to('.made-with', { opacity: 1, duration: 0.5 });
2665
2772
 
2666
2773
  // ============================================
2667
- // AUTO-PLAY BUTTON
2774
+ // UNIFIED AUTO-PLAY + AUDIO SYSTEM
2668
2775
  // ============================================
2669
- document.getElementById('playBtn').addEventListener('click', () => {
2670
- if (!isPlaying) {
2671
- isPlaying = true;
2672
- document.getElementById('playBtn').textContent = 'PLAYING...';
2673
- document.getElementById('playBtn').style.background = 'var(--white)';
2674
- document.getElementById('playBtn').style.color = 'var(--black)';
2675
-
2676
- let currentSection = 0;
2677
- const autoScroll = setInterval(() => {
2678
- if (currentSection < sections.length) {
2679
- document.getElementById(sections[currentSection]).scrollIntoView({
2680
- behavior: 'smooth'
2681
- });
2682
- currentSection++;
2683
- } else {
2684
- clearInterval(autoScroll);
2685
- isPlaying = false;
2686
- document.getElementById('playBtn').textContent = 'AUTO PLAY';
2687
- document.getElementById('playBtn').style.background = 'transparent';
2688
- 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);
2689
2884
  }
2690
- }, 5000);
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;
3007
+ }
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();
2691
3022
  }
2692
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();
2693
3040
 
2694
3041
  // ============================================
2695
3042
  // RESET BUTTON
@@ -2889,6 +3236,7 @@
2889
3236
  }
2890
3237
  });
2891
3238
  });
3239
+
2892
3240
  </script>
2893
3241
 
2894
3242
  </body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hustle-together/api-dev-tools",
3
- "version": "2.0.4",
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": {