@tpitre/story-ui 3.7.0 → 3.9.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpitre/story-ui",
3
- "version": "3.7.0",
3
+ "version": "3.9.0",
4
4
  "description": "AI-powered Storybook story generator with dynamic component discovery",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1656,6 +1656,8 @@ function StoryUIPanel() {
1656
1656
  const [attachedImages, setAttachedImages] = useState<AttachedImage[]>([]);
1657
1657
  const [considerations, setConsiderations] = useState<string>('');
1658
1658
  const [orphanStories, setOrphanStories] = useState<OrphanStory[]>([]);
1659
+ const [selectedStoryIds, setSelectedStoryIds] = useState<Set<string>>(new Set());
1660
+ const [isBulkDeleting, setIsBulkDeleting] = useState(false);
1659
1661
  const chatEndRef = useRef<HTMLDivElement | null>(null);
1660
1662
  const inputRef = useRef<HTMLInputElement | null>(null);
1661
1663
  const fileInputRef = useRef<HTMLInputElement | null>(null);
@@ -2462,6 +2464,92 @@ function StoryUIPanel() {
2462
2464
  }
2463
2465
  };
2464
2466
 
2467
+ // Toggle story selection for bulk operations
2468
+ const toggleStorySelection = (storyId: string) => {
2469
+ setSelectedStoryIds(prev => {
2470
+ const newSet = new Set(prev);
2471
+ if (newSet.has(storyId)) {
2472
+ newSet.delete(storyId);
2473
+ } else {
2474
+ newSet.add(storyId);
2475
+ }
2476
+ return newSet;
2477
+ });
2478
+ };
2479
+
2480
+ // Select/deselect all stories
2481
+ const toggleSelectAll = () => {
2482
+ if (selectedStoryIds.size === orphanStories.length) {
2483
+ setSelectedStoryIds(new Set());
2484
+ } else {
2485
+ setSelectedStoryIds(new Set(orphanStories.map(s => s.id)));
2486
+ }
2487
+ };
2488
+
2489
+ // Bulk delete selected stories
2490
+ const handleBulkDelete = async () => {
2491
+ if (selectedStoryIds.size === 0) return;
2492
+
2493
+ const count = selectedStoryIds.size;
2494
+ if (!confirm(`Delete ${count} selected ${count === 1 ? 'story' : 'stories'}? This action cannot be undone.`)) {
2495
+ return;
2496
+ }
2497
+
2498
+ setIsBulkDeleting(true);
2499
+ try {
2500
+ const response = await fetch(`${STORIES_API}/delete-bulk`, {
2501
+ method: 'POST',
2502
+ headers: { 'Content-Type': 'application/json' },
2503
+ body: JSON.stringify({ ids: Array.from(selectedStoryIds) }),
2504
+ });
2505
+
2506
+ if (response.ok) {
2507
+ const result = await response.json();
2508
+ // Remove deleted stories from state
2509
+ setOrphanStories(prev => prev.filter(s => !selectedStoryIds.has(s.id)));
2510
+ setSelectedStoryIds(new Set());
2511
+ console.log(`Deleted ${result.deleted?.length || count} stories`);
2512
+ } else {
2513
+ alert('Failed to delete some stories. Please try again.');
2514
+ }
2515
+ } catch (err) {
2516
+ console.error('Error bulk deleting stories:', err);
2517
+ alert('Failed to delete stories. Please try again.');
2518
+ } finally {
2519
+ setIsBulkDeleting(false);
2520
+ }
2521
+ };
2522
+
2523
+ // Clear all generated stories
2524
+ const handleClearAll = async () => {
2525
+ if (orphanStories.length === 0) return;
2526
+
2527
+ if (!confirm(`Delete ALL ${orphanStories.length} generated stories? This action cannot be undone.`)) {
2528
+ return;
2529
+ }
2530
+
2531
+ setIsBulkDeleting(true);
2532
+ try {
2533
+ const response = await fetch(STORIES_API, {
2534
+ method: 'DELETE',
2535
+ });
2536
+
2537
+ if (response.ok) {
2538
+ const result = await response.json();
2539
+ setOrphanStories([]);
2540
+ setSelectedStoryIds(new Set());
2541
+ console.log(`Cleared ${result.deleted || 'all'} stories`);
2542
+ } else {
2543
+ alert('Failed to clear stories. Please try again.');
2544
+ }
2545
+ } catch (err) {
2546
+ console.error('Error clearing stories:', err);
2547
+ alert('Failed to clear stories. Please try again.');
2548
+ } finally {
2549
+ setIsBulkDeleting(false);
2550
+ }
2551
+ };
2552
+
2465
2553
  return (
2466
2554
  <div className="story-ui-panel" style={STYLES.container}>
2467
2555
  {/* Sidebar */}
@@ -2558,40 +2646,173 @@ function StoryUIPanel() {
2558
2646
  {/* Generated Files Section - orphan stories without chat history */}
2559
2647
  {orphanStories.length > 0 && (
2560
2648
  <>
2649
+ {/* Header with Select All and Count */}
2561
2650
  <div style={{
2562
- color: '#64748b',
2563
- fontSize: '12px',
2651
+ display: 'flex',
2652
+ alignItems: 'center',
2653
+ justifyContent: 'space-between',
2564
2654
  marginTop: '16px',
2565
2655
  marginBottom: '8px',
2566
- fontWeight: '500',
2567
- textTransform: 'uppercase',
2568
- letterSpacing: '0.05em',
2569
2656
  }}>
2570
- Generated Files
2657
+ <div style={{
2658
+ display: 'flex',
2659
+ alignItems: 'center',
2660
+ gap: '8px',
2661
+ }}>
2662
+ <input
2663
+ type="checkbox"
2664
+ checked={selectedStoryIds.size === orphanStories.length && orphanStories.length > 0}
2665
+ onChange={toggleSelectAll}
2666
+ style={{
2667
+ width: '14px',
2668
+ height: '14px',
2669
+ cursor: 'pointer',
2670
+ accentColor: '#3b82f6',
2671
+ }}
2672
+ title={selectedStoryIds.size === orphanStories.length ? 'Deselect all' : 'Select all'}
2673
+ />
2674
+ <span style={{
2675
+ color: '#64748b',
2676
+ fontSize: '12px',
2677
+ fontWeight: '500',
2678
+ textTransform: 'uppercase',
2679
+ letterSpacing: '0.05em',
2680
+ }}>
2681
+ Generated Files ({orphanStories.length})
2682
+ </span>
2683
+ </div>
2684
+ </div>
2685
+
2686
+ {/* Bulk Action Buttons */}
2687
+ {selectedStoryIds.size > 0 && (
2688
+ <div style={{
2689
+ display: 'flex',
2690
+ gap: '8px',
2691
+ marginBottom: '12px',
2692
+ }}>
2693
+ <button
2694
+ onClick={handleBulkDelete}
2695
+ disabled={isBulkDeleting}
2696
+ style={{
2697
+ flex: 1,
2698
+ padding: '6px 10px',
2699
+ fontSize: '11px',
2700
+ fontWeight: '500',
2701
+ background: 'rgba(239, 68, 68, 0.15)',
2702
+ color: '#f87171',
2703
+ border: '1px solid rgba(239, 68, 68, 0.3)',
2704
+ borderRadius: '6px',
2705
+ cursor: isBulkDeleting ? 'not-allowed' : 'pointer',
2706
+ opacity: isBulkDeleting ? 0.6 : 1,
2707
+ transition: 'all 0.15s ease',
2708
+ }}
2709
+ onMouseEnter={(e) => {
2710
+ if (!isBulkDeleting) {
2711
+ e.currentTarget.style.background = 'rgba(239, 68, 68, 0.25)';
2712
+ }
2713
+ }}
2714
+ onMouseLeave={(e) => {
2715
+ e.currentTarget.style.background = 'rgba(239, 68, 68, 0.15)';
2716
+ }}
2717
+ >
2718
+ {isBulkDeleting ? 'Deleting...' : `Delete Selected (${selectedStoryIds.size})`}
2719
+ </button>
2720
+ </div>
2721
+ )}
2722
+
2723
+ {/* Clear All Button (always visible) */}
2724
+ <div style={{
2725
+ display: 'flex',
2726
+ gap: '8px',
2727
+ marginBottom: '12px',
2728
+ }}>
2729
+ <button
2730
+ onClick={handleClearAll}
2731
+ disabled={isBulkDeleting || orphanStories.length === 0}
2732
+ style={{
2733
+ flex: 1,
2734
+ padding: '6px 10px',
2735
+ fontSize: '11px',
2736
+ fontWeight: '500',
2737
+ background: 'rgba(100, 116, 139, 0.15)',
2738
+ color: '#94a3b8',
2739
+ border: '1px solid rgba(100, 116, 139, 0.3)',
2740
+ borderRadius: '6px',
2741
+ cursor: (isBulkDeleting || orphanStories.length === 0) ? 'not-allowed' : 'pointer',
2742
+ opacity: (isBulkDeleting || orphanStories.length === 0) ? 0.6 : 1,
2743
+ transition: 'all 0.15s ease',
2744
+ }}
2745
+ onMouseEnter={(e) => {
2746
+ if (!isBulkDeleting && orphanStories.length > 0) {
2747
+ e.currentTarget.style.background = 'rgba(239, 68, 68, 0.15)';
2748
+ e.currentTarget.style.color = '#f87171';
2749
+ e.currentTarget.style.borderColor = 'rgba(239, 68, 68, 0.3)';
2750
+ }
2751
+ }}
2752
+ onMouseLeave={(e) => {
2753
+ e.currentTarget.style.background = 'rgba(100, 116, 139, 0.15)';
2754
+ e.currentTarget.style.color = '#94a3b8';
2755
+ e.currentTarget.style.borderColor = 'rgba(100, 116, 139, 0.3)';
2756
+ }}
2757
+ >
2758
+ Clear All Stories
2759
+ </button>
2571
2760
  </div>
2761
+
2762
+ {/* Story List */}
2572
2763
  {orphanStories.map(story => (
2573
2764
  <div
2574
2765
  key={story.id}
2575
2766
  style={{
2576
2767
  ...STYLES.chatItem,
2577
- background: 'rgba(251, 191, 36, 0.1)',
2578
- borderLeft: '3px solid rgba(251, 191, 36, 0.5)',
2768
+ background: selectedStoryIds.has(story.id)
2769
+ ? 'rgba(59, 130, 246, 0.15)'
2770
+ : 'rgba(251, 191, 36, 0.1)',
2771
+ borderLeft: selectedStoryIds.has(story.id)
2772
+ ? '3px solid rgba(59, 130, 246, 0.5)'
2773
+ : '3px solid rgba(251, 191, 36, 0.5)',
2774
+ display: 'flex',
2775
+ alignItems: 'flex-start',
2776
+ gap: '8px',
2579
2777
  }}
2580
2778
  onMouseEnter={(e) => {
2581
- e.currentTarget.style.background = 'rgba(251, 191, 36, 0.15)';
2779
+ if (!selectedStoryIds.has(story.id)) {
2780
+ e.currentTarget.style.background = 'rgba(251, 191, 36, 0.15)';
2781
+ }
2582
2782
  const deleteBtn = e.currentTarget.querySelector('.delete-orphan-btn') as HTMLElement;
2583
2783
  if (deleteBtn) deleteBtn.style.opacity = '1';
2584
2784
  }}
2585
2785
  onMouseLeave={(e) => {
2586
- e.currentTarget.style.background = 'rgba(251, 191, 36, 0.1)';
2786
+ if (!selectedStoryIds.has(story.id)) {
2787
+ e.currentTarget.style.background = 'rgba(251, 191, 36, 0.1)';
2788
+ }
2587
2789
  const deleteBtn = e.currentTarget.querySelector('.delete-orphan-btn') as HTMLElement;
2588
2790
  if (deleteBtn) deleteBtn.style.opacity = '0';
2589
2791
  }}
2590
2792
  >
2591
- <div style={STYLES.chatItemTitle}>{story.title}</div>
2592
- <div style={{ ...STYLES.chatItemTime, fontSize: '11px' }}>
2593
- {story.fileName}
2793
+ {/* Checkbox */}
2794
+ <input
2795
+ type="checkbox"
2796
+ checked={selectedStoryIds.has(story.id)}
2797
+ onChange={() => toggleStorySelection(story.id)}
2798
+ onClick={(e) => e.stopPropagation()}
2799
+ style={{
2800
+ width: '14px',
2801
+ height: '14px',
2802
+ cursor: 'pointer',
2803
+ accentColor: '#3b82f6',
2804
+ marginTop: '2px',
2805
+ flexShrink: 0,
2806
+ }}
2807
+ />
2808
+ {/* Story Info */}
2809
+ <div style={{ flex: 1, minWidth: 0 }}>
2810
+ <div style={STYLES.chatItemTitle}>{story.title}</div>
2811
+ <div style={{ ...STYLES.chatItemTime, fontSize: '11px' }}>
2812
+ {story.fileName}
2813
+ </div>
2594
2814
  </div>
2815
+ {/* Delete Button */}
2595
2816
  <button
2596
2817
  className="delete-orphan-btn"
2597
2818
  onClick={async (e) => {
@@ -2602,6 +2823,11 @@ function StoryUIPanel() {
2602
2823
  });
2603
2824
  if (response.ok) {
2604
2825
  setOrphanStories(prev => prev.filter(s => s.id !== story.id));
2826
+ setSelectedStoryIds(prev => {
2827
+ const newSet = new Set(prev);
2828
+ newSet.delete(story.id);
2829
+ return newSet;
2830
+ });
2605
2831
  } else {
2606
2832
  console.error('Failed to delete orphan story');
2607
2833
  }