/* detail.jsx — Detail side panel + New Task modal. Exported to window. */ function TagSelect({ kind, value, onChange }) { const { Ic, Store } = window; const [open, setOpen] = React.useState(false); const ref = React.useRef(null); const opts = kind === "cat" ? Store.CATEGORIES : Store.PRIORITIES; const meta = opts[value]; React.useEffect(() => { const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; document.addEventListener("mousedown", h); return () => document.removeEventListener("mousedown", h); }, []); const I = meta && meta.icon ? Ic[meta.icon] : null; return ( {open && (
{Object.entries(opts).map(([k, m]) => { const MI = m.icon ? Ic[m.icon] : null; return ( ); })}
)}
); } function Lightbox({ images, index, onClose, onNav, onDelete }) { const { Ic, FX } = window; const n = images.length; const go = (d) => { onNav((index + d + n) % n); FX.sfx.play("click"); }; React.useEffect(() => { const h = (e) => { if (e.key === "Escape") onClose(); else if (e.key === "ArrowRight") go(1); else if (e.key === "ArrowLeft") go(-1); }; window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h); }, [index, n]); return (
{index + 1} / {n}
{n > 1 && ( )} {"Imagem e.stopPropagation()} /> {n > 1 && ( )}
e.stopPropagation()}> {images.map((u, i) => (
); } function VideoItem({ video, index, onDel }) { const { Ic } = window; const [playing, setPlaying] = React.useState(false); const m = (video.url || "").match(/(?:youtu\.be\/|youtube\.com\/(?:watch\?v=|embed\/|shorts\/|live\/|v\/))([\w-]{11})/); const ytid = m ? m[1] : null; return (
{playing && ytid ? ( ) : ( )} Abrir no YouTube ↗
); } function DetailPanel({ task, onClose, onUpdate, onMoveStatus, onDelete, dispatch }) { const { Ic, Store, FX } = window; const [newCheck, setNewCheck] = React.useState(""); const [lightbox, setLightbox] = React.useState(null); // image index | null const [dragOver, setDragOver] = React.useState(false); const [replaceIdx, setReplaceIdx] = React.useState(null); const [vidUrl, setVidUrl] = React.useState(""); const [vidErr, setVidErr] = React.useState(""); const titleRef = React.useRef(null); const descRef = React.useRef(null); const fileRef = React.useRef(null); const replaceRef = React.useRef(null); if (!task) return null; const pct = Store.taskProgress(task); const cat = Store.CATEGORIES[task.category]; const commitTitle = () => { const v = titleRef.current.innerText.trim(); if (v && v !== task.title) onUpdate(task.id, { title: v }); else titleRef.current.innerText = task.title; }; const commitDesc = () => { const v = descRef.current.innerText.trim(); if (v !== task.desc) onUpdate(task.id, { desc: v }); }; const toggleCheck = (cid, done) => { dispatch({ type: "TOGGLE_CHECK", id: task.id, cid }); FX.sfx.play(done ? "uncheck" : "check"); }; const addCheck = () => { const v = newCheck.trim(); if (!v) return; dispatch({ type: "ADD_CHECK", id: task.id, text: v }); setNewCheck(""); }; const remaining = 12 - task.images.length; const addFiles = (fileList) => { const files = [...(fileList || [])].slice(0, remaining); if (!files.length) return; let added = 0; files.forEach(f => FX.readImageFile(f, 1280, (url) => { if (url) { dispatch({ type: "ADD_IMAGE", id: task.id, url }); added++; if (added === 1) FX.sfx.play("click"); } })); }; const onDrop = (e) => { e.preventDefault(); setDragOver(false); if (e.dataTransfer && e.dataTransfer.files) addFiles(e.dataTransfer.files); }; const delImage = (i) => { dispatch({ type: "DEL_IMAGE", id: task.id, index: i }); FX.sfx.play("uncheck"); setLightbox(lb => lb == null ? lb : (i < lb ? lb - 1 : (i === lb ? null : lb))); }; const pickReplace = (i) => { setReplaceIdx(i); if (replaceRef.current) replaceRef.current.click(); }; const onReplaceFile = (e) => { const f = e.target.files && e.target.files[0]; e.target.value = ""; if (f == null || replaceIdx == null) return; const at = replaceIdx; setReplaceIdx(null); FX.readImageFile(f, 1280, (url) => { if (url) { dispatch({ type: "REPLACE_IMAGE", id: task.id, index: at, url }); FX.sfx.play("click"); } }); }; const ytId = (url) => { if (!url) return null; const m = url.match(/(?:youtu\.be\/|youtube\.com\/(?:watch\?v=|embed\/|shorts\/|live\/|v\/))([\w-]{11})/); if (m) return m[1]; if (/^[\w-]{11}$/.test(url.trim())) return url.trim(); return null; }; const addVideo = () => { const id = ytId(vidUrl.trim()); if (!id) { setVidErr("Cole um link válido do YouTube."); return; } dispatch({ type: "ADD_VIDEO", id: task.id, video: { id, url: vidUrl.trim() } }); setVidUrl(""); setVidErr(""); FX.sfx.play("click"); }; const delVideo = (i) => { dispatch({ type: "DEL_VIDEO", id: task.id, index: i }); FX.sfx.play("uncheck"); }; const videos = task.videos || []; return ( <>
{lightbox != null && task.images[lightbox] && ( setLightbox(null)} onNav={setLightbox} onDelete={delImage} /> )} ); } function NewTaskModal({ defaultStatus, onClose, onCreate }) { const { Ic, Store } = window; const [title, setTitle] = React.useState(""); const [desc, setDesc] = React.useState(""); const [category, setCategory] = React.useState("projeto"); const [priority, setPriority] = React.useState("baixa"); const [status, setStatus] = React.useState(defaultStatus || "todo"); const titleRef = React.useRef(null); React.useEffect(() => { titleRef.current && titleRef.current.focus(); }, []); const submit = (e) => { e.preventDefault(); if (!title.trim()) { titleRef.current.focus(); return; } onCreate({ title: title.trim(), desc: desc.trim(), category, priority, status }); }; return (
{ if (e.target === e.currentTarget) onClose(); }}>

Nova Tarefa

Adicione uma missão ao seu quadro.

setTitle(e.target.value)} placeholder="Ex: Bug: spawn de veículo" />