/* 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 && (
)}

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(); }}>
);
}
Object.assign(window, { DetailPanel, NewTaskModal, TagSelect, Lightbox });