krz/brand-bench
A dynamic brand documentation generator with Ollama integration.
clone: git clone https://gitbay.org/krz/brand-bench.git
main: src/components/PackageSwitcher.tsx · raw
1import { useState, useEffect } from 'react';
2import { createPortal } from 'react-dom';
3import type { PackageSlot } from '../hooks/usePackages';
4
5interface Props {
6 slots: PackageSlot[];
7 activeId: string;
8 onSwitch: (id: string) => void;
9 onCreate: () => void;
10 onRemove: (id: string) => void;
11 onRename: (id: string, name: string) => void;
12 onDuplicate: (id: string) => void;
13}
14
15interface MenuState {
16 id: string;
17 top: number;
18 left: number;
19}
20
21export function PackageSwitcher({ slots, activeId, onSwitch, onCreate, onRemove, onRename, onDuplicate }: Props) {
22 const [editingId, setEditingId] = useState<string | null>(null);
23 const [draft, setDraft] = useState('');
24 const [menu, setMenu] = useState<MenuState | null>(null);
25
26 // Close dropdown on any outside click
27 useEffect(() => {
28 if (!menu) return;
29 const handler = () => setMenu(null);
30 document.addEventListener('click', handler);
31 return () => document.removeEventListener('click', handler);
32 }, [menu]);
33
34 const openMenu = (id: string, e: React.MouseEvent<HTMLButtonElement>) => {
35 e.stopPropagation();
36 if (menu?.id === id) { setMenu(null); return; }
37 const rect = e.currentTarget.getBoundingClientRect();
38 setMenu({ id, top: rect.bottom + 4, left: rect.left });
39 };
40
41 const commitRename = (id: string) => {
42 const name = draft.trim();
43 if (name) onRename(id, name);
44 setEditingId(null);
45 };
46
47 return (
48 <div className="pkg-switcher">
49 {slots.map(slot => (
50 <div
51 key={slot.id}
52 className={`pkg-tab${slot.id === activeId ? ' active' : ''}`}
53 onClick={() => slot.id !== activeId && onSwitch(slot.id)}
54 >
55 {editingId === slot.id ? (
56 <input
57 autoFocus
58 className="pkg-tab-input"
59 value={draft}
60 onChange={e => setDraft(e.target.value)}
61 onBlur={() => commitRename(slot.id)}
62 onKeyDown={e => {
63 if (e.key === 'Enter') commitRename(slot.id);
64 if (e.key === 'Escape') setEditingId(null);
65 }}
66 onClick={e => e.stopPropagation()}
67 />
68 ) : (
69 <span
70 className="pkg-tab-name"
71 onDoubleClick={e => {
72 e.stopPropagation();
73 setDraft(slot.name);
74 setEditingId(slot.id);
75 }}
76 >
77 {slot.name}
78 </span>
79 )}
80
81 <div className="pkg-tab-actions">
82 <button
83 className="pkg-tab-menu-btn"
84 title="Options"
85 onClick={e => openMenu(slot.id, e)}
86 >
87 ···
88 </button>
89 {slots.length > 1 && (
90 <button
91 className="pkg-tab-close"
92 title="Remove"
93 onClick={e => { e.stopPropagation(); onRemove(slot.id); }}
94 >
95 ×
96 </button>
97 )}
98 </div>
99 </div>
100 ))}
101
102 <button className="pkg-new-btn" onClick={onCreate} title="New package">+</button>
103
104 {/* Dropdown rendered in a portal to escape overflow clipping */}
105 {menu && createPortal(
106 <div
107 className="pkg-tab-dropdown"
108 style={{ position: 'fixed', top: menu.top, left: menu.left }}
109 onClick={e => e.stopPropagation()}
110 >
111 <button onClick={() => {
112 const slot = slots.find(s => s.id === menu.id);
113 if (slot) { setDraft(slot.name); setEditingId(slot.id); }
114 setMenu(null);
115 }}>Rename</button>
116 <button onClick={() => { onDuplicate(menu.id); setMenu(null); }}>Duplicate</button>
117 {slots.length > 1 && (
118 <button className="danger" onClick={() => { onRemove(menu.id); setMenu(null); }}>Delete</button>
119 )}
120 </div>,
121 document.body
122 )}
123 </div>
124 );
125}