// new-task-modal.jsx — Rich "New task" modal (quick create + full form)

const EPIC_COLOR_PRESETS = ["#a25ddc", "#0073ea", "#00c875", "#fdab3d", "#e2445c", "#579bfc", "#7e3b8a"];

function NewTaskModal({ open, onClose, onCreate, onCreateEpic, defaults = {}, activeSprintId, sprints = SPRINTS, epics, projectAccess, activeProjectId }) {
  // Selected project for THIS task. Defaults to the explicit override
  // (defaults.projectId), then the dashboard's active project, then
  // the first project the user has access to. The user can change it
  // — when they do, sprints / epics / assignees re-derive from the
  // newly chosen project so the form never lets them attach a task
  // to an epic that lives somewhere else.
  // Dedupe by id — defends against a backend join that accidentally
  // multiplies projects across workspaces. The bootstrap query was
  // fixed for this in routes/bootstrap.routes.js, but we keep the
  // dedupe so a future regression can't land doubled-up entries in
  // the picker. First occurrence wins (preserves alpha order).
  const _allProjects = (() => {
    const raw = (typeof PROJECTS !== "undefined" && Array.isArray(PROJECTS)) ? PROJECTS : [];
    const seen = new Set();
    const out = [];
    for (const p of raw) {
      if (!p || !p.id || seen.has(p.id)) continue;
      seen.add(p.id);
      out.push(p);
    }
    return out;
  })();
  const [projectId, setProjectId] = React.useState(
    defaults.projectId || activeProjectId || (_allProjects[0] && _allProjects[0].id) || ""
  );
  // When PROJECTS holds entries from multiple workspaces, narrow the
  // option list to the user's active workspace so the picker doesn't
  // surface projects they probably can't see in the sidebar.
  const _wsId = (typeof WORKSPACE !== "undefined" && WORKSPACE && WORKSPACE.id) || null;
  const projectOptions = _allProjects.filter(p => !_wsId || !p.workspaceId || p.workspaceId === _wsId);
  const projectMeta = _allProjects.find(p => p.id === projectId) || null;
  // Lock the picker when the task is being created from a context
  // that explicitly picked the project (inline + on a project page).
  // The defaults.lockProject flag is set by callers that want the
  // user to commit to one project; otherwise the picker is editable.
  const lockProject = !!defaults.lockProject;

  // Sprints / epics — re-derive from the global SPRINTS / EPICS lists
  // based on the picker's current project, instead of the props that
  // the parent pre-filtered for whatever project was active when the
  // modal opened. Falls back to the props when the globals aren't
  // loaded yet (early bootstrap).
  const _allSprints = (typeof SPRINTS !== "undefined" && Array.isArray(SPRINTS)) ? SPRINTS : [];
  const _allEpics   = (typeof EPICS   !== "undefined" && Array.isArray(EPICS))   ? EPICS   : [];
  const projectSprints = projectId
    ? _allSprints.filter(s => s.project_id === projectId)
    : (Array.isArray(sprints) ? sprints : _allSprints);
  const projectEpicsRaw = projectId
    ? _allEpics.filter(e => e.project_id === projectId)
    : (Array.isArray(epics) ? epics : _allEpics);
  const epicList = projectEpicsRaw.length ? projectEpicsRaw : (Array.isArray(epics) ? epics : EPICS);

  const [mode, setMode] = React.useState("quick"); // quick | full
  const [name, setName] = React.useState("");
  const [desc, setDesc] = React.useState("");
  const [type, setType] = React.useState("task");   // task | bug | chore | spike
  const [epicId, setEpicId] = React.useState(defaults.epicId || epicList[0]?.id || "");
  const [epicCreating, setEpicCreating] = React.useState(false);
  const [epicNewTitle, setEpicNewTitle] = React.useState("");
  const [epicNewColor, setEpicNewColor] = React.useState(EPIC_COLOR_PRESETS[0]);
  const [epicSaving, setEpicSaving] = React.useState(false);
  const epicNewInputRef = React.useRef(null);
  const [sprintId, setSprintId] = React.useState(defaults.sprintId || activeSprintId || "");
  // Optional user story this task lives under. NULL = unparented
  // (matches existing behaviour for tasks created before migration 024).
  const [userStoryId, setUserStoryId] = React.useState(defaults.userStoryId || "");
  const [owners, setOwners] = React.useState(defaults.owners || []);
  const [prio, setPrio] = React.useState("medium");
  const [status, setStatus] = React.useState("todo");
  const [points, setPoints] = React.useState(3);
  const [due, setDue] = React.useState("");
  const [createMore, setCreateMore] = React.useState(false);
  const [showDueCal, setShowDueCal] = React.useState(false);
  // Channel — only used when the active project has content mode on.
  const [channel, setChannel] = React.useState("");

  const nameRef = React.useRef(null);
  const descRef = React.useRef(null);
  const descFileRef = React.useRef(null);
  const dueWrapRef = React.useRef(null);

  React.useEffect(() => {
    if (!open) return;
    // Reset fields
    setMode("quick");
    setName(""); setDesc(""); setType("task");
    // Re-seed projectId on every open — the active project may have
    // changed since the last time the modal was used.
    setProjectId(
      defaults.projectId || activeProjectId || (_allProjects[0] && _allProjects[0].id) || ""
    );
    setEpicId(defaults.epicId || epicList[0]?.id || "");
    setEpicCreating(false);
    setEpicNewTitle("");
    setEpicNewColor(EPIC_COLOR_PRESETS[0]);
    setEpicSaving(false);
    setSprintId(defaults.sprintId || activeSprintId || "");
    setUserStoryId(defaults.userStoryId || "");
    setOwners(defaults.owners || []);
    setPrio("medium"); setStatus("todo"); setPoints(3); setDue("");
    setShowDueCal(false);
    setChannel("");
    if (descRef.current) descRef.current.innerHTML = "";
    setTimeout(() => nameRef.current?.focus(), 50);
  }, [open]);

  // When the user picks a different project mid-flow, anything that
  // belongs to the previous project (epic, sprint, story) is no
  // longer valid — clear them so the user re-picks from the new
  // project's options. Owners stay (they may overlap workspace-wide).
  function changeProject(nextId) {
    if (!nextId || nextId === projectId) return;
    setProjectId(nextId);
    setEpicId("");
    setSprintId("");
    setUserStoryId("");
  }

  // When user switches into Full form, clear the description editor's HTML
  // (state already cleared on open) so the placeholder shows correctly.
  React.useEffect(() => {
    if (mode === "full" && descRef.current && !desc) {
      descRef.current.innerHTML = "";
    }
  }, [mode]);

  // Close due-date calendar when clicking outside it
  React.useEffect(() => {
    if (!showDueCal) return;
    function onDocDown(e) {
      if (dueWrapRef.current && !dueWrapRef.current.contains(e.target)) {
        setShowDueCal(false);
      }
    }
    document.addEventListener("mousedown", onDocDown);
    return () => document.removeEventListener("mousedown", onDocDown);
  }, [showDueCal]);

  React.useEffect(() => {
    if (!open) return;
    const onKey = (e) => {
      if (e.key === "Escape") onClose();
      if ((e.metaKey || e.ctrlKey) && e.key === "Enter") submit();
    };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [open, name, desc, type, epicId, sprintId, owners, prio, status, points, due]);

  function submit() {
    if (!name.trim()) return;
    if (!projectId) return;            // can't create a task without a project
    const epic = epicList.find(e => e.id === epicId);
    onCreate({
      // The picked project — addTask() in app.jsx prefers this over
      // the outer activeProjectId, so a task created from Home or
      // the palette lands in the project the user explicitly chose.
      projectId,
      name: name.trim(),
      desc, type,
      epicId: epicId || null,
      epicTitle: epic?.title || null,
      epicColor: epic?.color || null,
      userStoryId: userStoryId || null,
      sprint: sprintId || null,
      owners, prio, status, points: Number(points) || 0,
      due: due || "—",
      // Only forward channel when content mode is on for the active
      // project — otherwise the field stays unused on regular tasks.
      channel: channel || null,
      subtasks: 0, updated: "just now",
    });
    if (createMore) {
      setName(""); setDesc("");
      setTimeout(() => nameRef.current?.focus(), 0);
    } else {
      onClose();
    }
  }

  function toggleOwner(id) {
    setOwners(o => o.includes(id) ? o.filter(x => x !== id) : [...o, id]);
  }

  // ── Rich-text description helpers (mirror drawer.jsx) ──
  function commitDescFromDOM() {
    if (!descRef.current) return;
    setDesc(descRef.current.innerHTML);
  }
  function insertHtmlAtCaret(html) {
    if (!descRef.current) return;
    descRef.current.focus();
    const sel = window.getSelection();
    let range;
    if (sel && sel.rangeCount > 0 && descRef.current.contains(sel.anchorNode)) {
      range = sel.getRangeAt(0);
    } else {
      range = document.createRange();
      range.selectNodeContents(descRef.current);
      range.collapse(false);
    }
    range.deleteContents();
    const tpl = document.createElement("template");
    tpl.innerHTML = html;
    const frag = tpl.content;
    const lastNode = frag.lastChild;
    range.insertNode(frag);
    if (lastNode && sel) {
      const newRange = document.createRange();
      newRange.setStartAfter(lastNode);
      newRange.collapse(true);
      sel.removeAllRanges();
      sel.addRange(newRange);
    }
    commitDescFromDOM();
  }
  // Downscale + recompress before insertion. Mirrors drawer.jsx — see
  // BG_9D39977B23. 1600px max, JPEG quality 0.85, PNG kept only for
  // small (≤500px) images so icon transparency survives.
  function _resizeImageToDataUrl(file, { maxSide = 1600, quality = 0.85 } = {}) {
    return new Promise((resolve, reject) => {
      try {
        const reader = new FileReader();
        reader.onload = () => {
          const img = new Image();
          img.onload = () => {
            try {
              const w = img.naturalWidth || img.width;
              const h = img.naturalHeight || img.height;
              if (!w || !h) return reject(new Error("zero-dim image"));
              if (w <= maxSide && h <= maxSide && (file.size || 0) < 300 * 1024) {
                return resolve(String(reader.result || ""));
              }
              const scale = Math.min(1, maxSide / Math.max(w, h));
              const tw = Math.max(1, Math.round(w * scale));
              const th = Math.max(1, Math.round(h * scale));
              const cv = document.createElement("canvas");
              cv.width = tw; cv.height = th;
              const ctx = cv.getContext("2d");
              if (!ctx) return reject(new Error("no canvas ctx"));
              ctx.drawImage(img, 0, 0, tw, th);
              const out = (file.type === "image/png" && Math.max(tw, th) <= 500)
                ? cv.toDataURL("image/png")
                : cv.toDataURL("image/jpeg", quality);
              resolve(out);
            } catch (e) { reject(e); }
          };
          img.onerror = () => reject(new Error("image decode failed"));
          img.src = String(reader.result || "");
        };
        reader.onerror = () => reject(new Error("file read failed"));
        reader.readAsDataURL(file);
      } catch (e) { reject(e); }
    });
  }
  async function readImageFile(file) {
    if (!file || !file.type || !file.type.startsWith("image/")) return;
    let dataUrl;
    try {
      dataUrl = await _resizeImageToDataUrl(file);
    } catch (_) {
      dataUrl = await new Promise((res) => {
        const r = new FileReader();
        r.onload = () => res(String(r.result || ""));
        r.onerror = () => res("");
        r.readAsDataURL(file);
      });
    }
    if (!dataUrl) return;
    const safeName = (file.name || "image").replace(/"/g, "");
    insertHtmlAtCaret(`<img class="nt-desc-img" src="${dataUrl}" alt="${safeName}" />`);
  }
  function onPasteDesc(e) {
    const items = (e.clipboardData && e.clipboardData.items) || [];
    for (const it of items) {
      if (it && it.type && it.type.startsWith("image/")) {
        const f = it.getAsFile();
        if (f) {
          e.preventDefault();
          readImageFile(f);
          return;
        }
      }
    }
  }
  function onDropDesc(e) {
    const files = e.dataTransfer && e.dataTransfer.files;
    if (!files || !files.length) return;
    const f = Array.from(files).find(x => x.type && x.type.startsWith("image/"));
    if (f) {
      e.preventDefault();
      readImageFile(f);
    }
  }

  function startCreateEpic() {
    setEpicCreating(true);
    setEpicNewTitle("");
    setEpicNewColor(EPIC_COLOR_PRESETS[0]);
    setTimeout(() => epicNewInputRef.current?.focus(), 30);
  }
  function cancelCreateEpic() {
    setEpicCreating(false);
    setEpicNewTitle("");
  }
  async function commitCreateEpic() {
    const title = epicNewTitle.trim();
    if (!title || epicSaving) return;
    if (typeof onCreateEpic !== "function") {
      cancelCreateEpic();
      return;
    }
    setEpicSaving(true);
    try {
      const epic = await onCreateEpic({ title, color: epicNewColor });
      if (epic && epic.id) setEpicId(epic.id);
      cancelCreateEpic();
    } catch (e) {
      // surface error message via title field
      console.error("Create epic failed", e);
    } finally {
      setEpicSaving(false);
    }
  }

  if (!open) return null;

  const activeSprints = sprints.filter(s => s.active);
  // Owner picker for the New Task modal — what counts as "a user in
  // this project" depends on the project's visibility:
  //
  //   • Private project    — only the explicit members array. Everyone
  //                          else genuinely doesn't have access.
  //   • Workspace-wide     — every active workspace user. The members
  //                          array on the access map only lists people
  //                          with elevated roles, but every other
  //                          workspace member can still see the project
  //                          and own tasks in it. Falling back to the
  //                          members-only list was hiding 90% of the
  //                          team from the assignee picker, which is
  //                          what the bug report flagged.
  const _accessMap = projectAccess || (typeof window !== "undefined" ? window.PROJECT_ACCESS : null) || {};
  // Use the modal's PICKED project (not the dashboard's active one) —
  // when the user changes the project picker, the assignee list must
  // re-derive against the newly-chosen project's membership.
  const _accessEntry = projectId ? _accessMap[projectId] : null;
  const _isPrivate = _accessEntry && _accessEntry.visibility === "private";
  const _explicitIds = _accessEntry
    ? new Set((_accessEntry.members || []).map(m => m.id))
    : null;
  // Workspace owners + admins always belong on every project's
  // assignee list — even a private project — because their role
  // grants them access. Fold them in alongside the explicit member
  // list so the picker can never accidentally exclude them.
  const _adminIds = new Set((PEOPLE || []).filter(p =>
    p.wsRole === "owner" || p.wsRole === "admin"
  ).map(p => p.id));
  const activePeople = (PEOPLE || [])
    .filter(p => p.status !== "deactivated")
    .filter(p => {
      if (!_accessEntry) return true;            // bootstrap not ready yet
      if (!_isPrivate) return true;              // workspace-wide → everyone
      // Private project: only explicit members + workspace admins/owners.
      return (_explicitIds && _explicitIds.has(p.id)) || _adminIds.has(p.id);
    });

  return ReactDOM.createPortal(
    <div className="modal-backdrop" onMouseDown={onClose}>
      <div className="modal nt-modal" onMouseDown={e => e.stopPropagation()} style={{ width: 620 }}>
        <div className="modal-header">
          <div style={{ flex: 1, minWidth: 0 }}>
            <div className="nt-breadcrumb">
              {/* Project picker — sits in the breadcrumb so it's
                  the first thing the user confirms before typing.
                  Shows the project's color dot + name; click to
                  switch when invoked from outside any project
                  (Home, palette, Owner dashboard). Locked when the
                  caller pinned a project (e.g. inline-add inside
                  an epic). */}
              <NtProjectPicker
                projects={projectOptions}
                selectedId={projectId}
                locked={lockProject}
                onChange={changeProject}/>
              <span style={{ color: "var(--ink-muted)" }}>›</span>
              <span style={{ color: "var(--ink-muted)" }}>{mode === "quick" ? "Quick create" : "New task"}</span>
            </div>
            <div className="modal-title">Create a new task</div>
          </div>
          <div className="nt-mode-toggle">
            <button className={mode === "quick" ? "is-active" : ""} onClick={() => setMode("quick")}>Quick</button>
            <button className={mode === "full"  ? "is-active" : ""} onClick={() => setMode("full")}>Full form</button>
          </div>
          <button className="modal-close" onClick={onClose}><Icons.Close size={16}/></button>
        </div>

        <div className="modal-body">
          {/* Type chips */}
          <div className="nt-type-row">
            {[
              { id: "task",  label: "Task",  icon: "Check",    color: "#0073ea" },
              { id: "bug",   label: "Bug",   icon: "Flag",     color: "#e2445c" },
              { id: "chore", label: "Chore", icon: "Activity", color: "#676879" },
              { id: "spike", label: "Spike", icon: "Lightning",color: "#a25ddc" },
            ].map(t => {
              const Ic = Icons[t.icon] || Icons.Check;
              return (
                <button key={t.id} className={`nt-type-chip ${type === t.id ? "is-active" : ""}`}
                        style={type === t.id ? { "--nt-c": t.color } : null}
                        onClick={() => setType(t.id)}>
                  <Ic size={13}/> {t.label}
                </button>
              );
            })}
          </div>

          <div className="ms-grid">
            <div className="ms-row">
              <label className="ms-label">Title</label>
              <input className="ms-input nt-name"
                     ref={nameRef}
                     placeholder="e.g. Fix Safari 17 Apple Pay merchant ID"
                     value={name} onChange={e => setName(e.target.value)}
                     onKeyDown={e => { if (e.key === "Enter" && !e.shiftKey && (e.metaKey || e.ctrlKey || mode === "quick")) { e.preventDefault(); submit(); } }}/>
            </div>

            {mode === "full" && (
              <div className="ms-row">
                <label className="ms-label nt-desc-label">
                  <span>Description <span className="ms-optional">(optional)</span></span>
                  <span className="nt-desc-tools">
                    <button type="button" className="nt-desc-tool"
                            onClick={() => descFileRef.current?.click()}
                            title="Insert image">
                      <Icons.Image size={12}/> Add image
                    </button>
                    <input ref={descFileRef} type="file" accept="image/*"
                           style={{ display: "none" }}
                           onChange={(e) => {
                             const f = e.target.files && e.target.files[0];
                             if (f) readImageFile(f);
                             e.target.value = "";
                           }}/>
                  </span>
                </label>
                <div ref={descRef}
                     className={`ms-input nt-desc nt-desc-rich${desc ? "" : " is-empty"}`}
                     contentEditable suppressContentEditableWarning
                     data-placeholder="What needs to happen? Context, acceptance criteria, links… (paste or drop images)"
                     onInput={commitDescFromDOM}
                     onBlur={commitDescFromDOM}
                     onPaste={onPasteDesc}
                     onDragOver={(e) => e.preventDefault()}
                     onDrop={onDropDesc}/>
              </div>
            )}

            <div className="ms-row-split">
              <div className="ms-row">
                <label className="ms-label">Epic</label>
                {epicCreating ? (
                  <div className="nt-epic-create">
                    <div className="nt-epic-create-row">
                      <span className="nt-epic-swatch-pick" style={{ background: epicNewColor }} title={epicNewColor}/>
                      <input ref={epicNewInputRef}
                             className="ms-input nt-epic-title-input"
                             placeholder="New epic title (e.g. Performance pass)"
                             value={epicNewTitle}
                             onChange={e => setEpicNewTitle(e.target.value)}
                             onKeyDown={e => {
                               if (e.key === "Enter") { e.preventDefault(); commitCreateEpic(); }
                               if (e.key === "Escape") { e.preventDefault(); cancelCreateEpic(); }
                             }}
                             disabled={epicSaving}/>
                    </div>
                    <div className="nt-epic-color-row">
                      {EPIC_COLOR_PRESETS.map(c => (
                        <button key={c} type="button"
                                className={"nt-epic-swatch" + (epicNewColor === c ? " is-active" : "")}
                                style={{ background: c }}
                                onClick={() => setEpicNewColor(c)}
                                disabled={epicSaving}
                                title={c}/>
                      ))}
                      <div style={{ flex: 1 }}/>
                      <button type="button" className="nt-epic-mini-btn" onClick={cancelCreateEpic} disabled={epicSaving}>Cancel</button>
                      <button type="button" className="nt-epic-mini-btn is-primary"
                              onClick={commitCreateEpic}
                              disabled={!epicNewTitle.trim() || epicSaving}>
                        {epicSaving ? "Creating…" : "Create"}
                      </button>
                    </div>
                  </div>
                ) : (
                  <select className="ms-input"
                          value={epicId}
                          onChange={e => {
                            const v = e.target.value;
                            if (v === "__new__") { startCreateEpic(); return; }
                            setEpicId(v);
                          }}>
                    <option value="">No epic</option>
                    {epicList.map(ep => <option key={ep.id} value={ep.id}>{ep.title}</option>)}
                    {typeof onCreateEpic === "function" && (
                      <option value="__new__">＋  Create new epic…</option>
                    )}
                  </select>
                )}
              </div>
              <div className="ms-row">
                <label className="ms-label">Sprint</label>
                <select className="ms-input" value={sprintId} onChange={e => setSprintId(e.target.value)}>
                  <option value="">Backlog</option>
                  {activeSprints.map(s => <option key={s.id} value={s.id}>{s.label} (active)</option>)}
                  {sprints.filter(s => !s.active && !s.completed).map(s => <option key={s.id} value={s.id}>{s.label}</option>)}
                </select>
              </div>
            </div>

            {/* User Story — optional. Filtered to the active project so
                you can't accidentally attach a task to a story in a
                different project. Excludes "done" stories — once a
                story is approved you generally don't add new tasks to
                it; create a follow-up story instead. */}
            {(() => {
              const allStories = (typeof USER_STORIES !== "undefined" && Array.isArray(USER_STORIES))
                ? USER_STORIES : [];
              const eligible = allStories.filter(s =>
                s.projectId === activeProjectId && s.status !== "done"
              );
              if (!eligible.length) return null;
              return (
                <div className="ms-row">
                  <label className="ms-label">
                    User story <span className="ms-optional">(optional)</span>
                  </label>
                  <select className="ms-input" value={userStoryId}
                          onChange={e => setUserStoryId(e.target.value)}>
                    <option value="">—  No story</option>
                    {eligible.map(s => (
                      <option key={s.id} value={s.id}>
                        {s.title}
                        {s.status === "review" ? " · in review" : ""}
                        {s.status === "changes_req" ? " · changes requested" : ""}
                      </option>
                    ))}
                  </select>
                </div>
              );
            })()}

            <div className="ms-row-split">
              <div className="ms-row">
                <label className="ms-label">Priority</label>
                <div className="nt-prio-row">
                  {PRIORITIES.filter(p => p.id !== "none").map(p => (
                    <button key={p.id}
                            className={`nt-prio-chip ${p.id} ${prio === p.id ? "is-active" : ""}`}
                            onClick={() => setPrio(p.id)}>
                      {p.label}
                    </button>
                  ))}
                </div>
              </div>
              <div className="ms-row">
                <label className="ms-label">Points</label>
                <div className="nt-points-row">
                  {[1, 2, 3, 5, 8, 13].map(p => (
                    <button key={p} className={`nt-pt-chip ${points === p ? "is-active" : ""}`}
                            onClick={() => setPoints(p)}>{p}</button>
                  ))}
                </div>
              </div>
            </div>

            {/* Channel — surfaced only when the active project has
                content-calendar mode turned on. */}
            {(() => {
              const _proj = (typeof PROJECTS !== "undefined")
                ? PROJECTS.find(p => p.id === activeProjectId)
                : null;
              if (!_proj || !_proj.isContentCalendar) return null;
              const _channels = (typeof window !== "undefined" && Array.isArray(window.CHANNELS))
                ? window.CHANNELS : [];
              return (
                <div className="ms-row">
                  <label className="ms-label">
                    Channel <span className="ms-optional">(optional)</span>
                  </label>
                  <div className="nt-prio-row" style={{ flexWrap: "wrap" }}>
                    <button
                      className={`nt-prio-chip ${channel === "" ? "is-active" : ""}`}
                      onClick={() => setChannel("")}
                      style={channel === "" ? { background: "#e6e9ef", color: "#0f1729", borderColor: "#cfd5e0" } : null}
                    >None</button>
                    {_channels.map(c => (
                      <button
                        key={c.id}
                        className={`nt-prio-chip ${channel === c.id ? "is-active" : ""}`}
                        onClick={() => setChannel(c.id)}
                        title={c.label}
                        style={Object.assign(
                          { display: "inline-flex", alignItems: "center", gap: 6 },
                          channel === c.id
                            ? { background: c.color, color: "#fff", borderColor: c.color }
                            : { color: c.color, borderColor: c.color }
                        )}
                      >
                        {typeof ChannelPill === "function"
                          ? <ChannelPill id={c.id} size="sm"/>
                          : <span style={{ fontWeight: 800, marginRight: 4 }}>{c.short}</span>}
                        {c.label}
                      </button>
                    ))}
                  </div>
                </div>
              );
            })()}

            {mode === "full" && (
              <div className="ms-row-split">
                <div className="ms-row">
                  <label className="ms-label">Status</label>
                  <select className="ms-input" value={status} onChange={e => setStatus(e.target.value)}>
                    {STATUSES.map(s => <option key={s.id} value={s.id}>{s.label}</option>)}
                  </select>
                </div>
                <div className="ms-row">
                  <label className="ms-label">Due date <span className="ms-optional">(optional)</span></label>
                  <div className="nt-due-wrap" ref={dueWrapRef}>
                    <button type="button"
                            className={`ms-input nt-due-btn${due ? "" : " is-empty"}`}
                            onClick={() => setShowDueCal(s => !s)}>
                      <Icons.Calendar size={13}/>
                      <span className="nt-due-label">{due || "Pick a date"}</span>
                      {due && (
                        <span className="nt-due-clear"
                              onClick={(e) => { e.stopPropagation(); setDue(""); setShowDueCal(false); }}
                              title="Clear date">
                          <Icons.Close size={11}/>
                        </span>
                      )}
                    </button>
                    {showDueCal && typeof MiniCalendar !== "undefined" && (
                      <div className="nt-due-pop" onMouseDown={(e) => e.stopPropagation()}>
                        <MiniCalendar value={due}
                                      onChange={(d) => { setDue(d); setShowDueCal(false); }}
                                      onClear={() => { setDue(""); setShowDueCal(false); }}/>
                      </div>
                    )}
                  </div>
                </div>
              </div>
            )}

            <div className="ms-row">
              <label className="ms-label">Assignees</label>
              <NtAssigneePicker
                people={activePeople}
                owners={owners}
                onToggle={toggleOwner}
                onClear={() => setOwners([])}/>
            </div>
          </div>
        </div>

        <div className="modal-footer">
          <label className="nt-more">
            <input type="checkbox" checked={createMore} onChange={e => setCreateMore(e.target.checked)}/>
            Create more
          </label>
          <div style={{ flex: 1, color: "var(--ink-muted)", fontSize: 11 }}>
            <kbd className="nt-kbd">⌘</kbd><kbd className="nt-kbd">↵</kbd> to create
          </div>
          <button className="btn" onClick={onClose}>Cancel</button>
          <button className="btn btn-primary" disabled={!name.trim()} onClick={submit}>
            <Icons.Plus size={13}/> Create task
          </button>
        </div>
      </div>
    </div>,
    document.body
  );
}

// ─────────────────────────────────────────────────────────────────────
// NtAssigneePicker — multi-select dropdown for the New Task modal.
//
// Trigger button: avatar stack of currently-picked owners + a count
// label, OR "Pick assignees" when none. Clicking opens a panel with:
//   • Search (filters by name/email)
//   • "Assign to me" shortcut (hidden once me is on the list)
//   • Checkbox-style rows for every project member, sorted with
//     picked users on top so they're easy to remove again.
//   • Footer with "Clear all" + the picked count.
//
// State stays in the parent NewTaskModal (it owns `owners`) — this
// component just renders + emits onToggle / onClear.
// ─────────────────────────────────────────────────────────────────────
function NtAssigneePicker({ people, owners, onToggle, onClear }) {
  const [open, setOpen] = React.useState(false);
  const [q, setQ]       = React.useState("");
  const wrapRef = React.useRef(null);

  // Close on outside click + Escape, same convention as the rest of
  // the modal's poppers (date picker, sprint dropdown, etc.).
  React.useEffect(() => {
    if (!open) return;
    function onDoc(e) {
      if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
    }
    function onKey(e) { if (e.key === "Escape") { e.stopPropagation(); setOpen(false); } }
    document.addEventListener("mousedown", onDoc);
    document.addEventListener("keydown", onKey, true);
    return () => {
      document.removeEventListener("mousedown", onDoc);
      document.removeEventListener("keydown", onKey, true);
    };
  }, [open]);

  const me = (typeof window !== "undefined" && window.api && api.getUser && api.getUser()) || null;
  const ownerSet = new Set(owners);

  // Picked users on top, then the rest alphabetically. Search
  // re-filters within both groups.
  const sorted = React.useMemo(() => {
    const arr = (people || []).slice().sort((a, b) => {
      const ap = ownerSet.has(a.id) ? 0 : 1;
      const bp = ownerSet.has(b.id) ? 0 : 1;
      if (ap !== bp) return ap - bp;
      return (a.name || "").localeCompare(b.name || "");
    });
    if (!q.trim()) return arr;
    const s = q.trim().toLowerCase();
    return arr.filter(p =>
      (p.name || "").toLowerCase().includes(s) ||
      (p.email || "").toLowerCase().includes(s));
  }, [people, owners, q]);

  const pickedPeople = (people || []).filter(p => ownerSet.has(p.id));
  const triggerLabel = pickedPeople.length === 0
    ? "Pick assignees"
    : pickedPeople.length === 1
      ? pickedPeople[0].name
      : `${pickedPeople.length} assignees`;

  return (
    <div className="nt-asg" ref={wrapRef}>
      <button
        type="button"
        className={`nt-asg-trigger ${pickedPeople.length ? "is-picked" : ""} ${open ? "is-open" : ""}`}
        onClick={() => setOpen(o => !o)}
        title={pickedPeople.length ? pickedPeople.map(p => p.name).join(", ") : "Pick assignees"}>
        {pickedPeople.length > 0 && (
          <span className="nt-asg-stack">
            {pickedPeople.slice(0, 4).map(p => (
              <span key={p.id} className="nt-asg-stack-avatar">
                <Avatar person={p} size={22}/>
              </span>
            ))}
            {pickedPeople.length > 4 && (
              <span className="nt-asg-stack-more">+{pickedPeople.length - 4}</span>
            )}
          </span>
        )}
        <span className="nt-asg-label">{triggerLabel}</span>
        <Icons.ChevronSm size={12} style={{ marginLeft: "auto", color: "var(--ink-muted)" }}/>
      </button>

      {open && (
        <div className="nt-asg-pop" onMouseDown={(e) => e.stopPropagation()}>
          <div className="nt-asg-search">
            <Icons.Search size={13}/>
            <input
              type="text"
              autoFocus
              placeholder="Search by name or email…"
              value={q}
              onChange={(e) => setQ(e.target.value)}/>
            {q && (
              <button type="button" className="nt-asg-search-clear" onClick={() => setQ("")} title="Clear search">
                <Icons.Close size={11}/>
              </button>
            )}
          </div>

          {me && !ownerSet.has(me.id) && !q.trim() && (
            <button
              type="button"
              className="nt-asg-row nt-asg-self"
              onClick={() => onToggle(me.id)}>
              <Icons.Plus size={12}/>
              <span className="nt-asg-row-name">Assign to me</span>
              <span className="nt-asg-row-sub">{me.name}</span>
            </button>
          )}

          <div className="nt-asg-list">
            {sorted.length === 0 && (
              <div className="nt-asg-empty">
                {q.trim() ? "No people match — try a different search" : "No project members yet"}
              </div>
            )}
            {sorted.map(p => {
              const picked = ownerSet.has(p.id);
              return (
                <button
                  key={p.id}
                  type="button"
                  className={`nt-asg-row ${picked ? "is-picked" : ""}`}
                  onClick={() => onToggle(p.id)}>
                  <span className={`nt-asg-check ${picked ? "is-on" : ""}`}>
                    {picked && <Icons.Check size={11}/>}
                  </span>
                  <Avatar person={p} size={22}/>
                  <span className="nt-asg-row-name">{p.name}</span>
                  {p.email && <span className="nt-asg-row-sub">{p.email}</span>}
                </button>
              );
            })}
          </div>

          <div className="nt-asg-foot">
            <span className="nt-asg-foot-count">
              {pickedPeople.length} selected
            </span>
            {pickedPeople.length > 0 && (
              <button type="button" className="nt-asg-foot-clear" onClick={onClear}>Clear all</button>
            )}
            <button type="button" className="nt-asg-foot-done" onClick={() => setOpen(false)}>Done</button>
          </div>
        </div>
      )}
      <style>{NT_ASG_CSS}</style>
    </div>
  );
}

const NT_ASG_CSS = `
.nt-asg { position: relative; flex: 1; min-width: 0; }
.nt-asg-trigger {
  display: inline-flex; align-items: center; gap: 8px;
  width: 100%; min-height: 36px;
  background: #fff;
  border: 1px solid var(--border, #e6e9ef);
  border-radius: 8px;
  padding: 4px 10px;
  font: inherit; font-size: 13px;
  color: var(--ink, #0f1729);
  cursor: pointer;
  transition: border-color .12s, box-shadow .12s;
}
.nt-asg-trigger:hover { border-color: var(--brand, #0073ea); }
.nt-asg-trigger.is-open { border-color: var(--brand, #0073ea); box-shadow: 0 0 0 3px rgba(0,115,234,.12); }
.nt-asg-trigger.is-picked { color: var(--ink, #0f1729); }
.nt-asg-trigger:not(.is-picked) .nt-asg-label { color: var(--ink-muted, #9aa0a6); }

.nt-asg-stack { display: inline-flex; align-items: center; }
.nt-asg-stack-avatar {
  display: inline-flex; margin-right: -6px;
  border: 2px solid #fff; border-radius: 50%;
}
.nt-asg-stack-avatar:last-child { margin-right: 4px; }
.nt-asg-stack-more {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: 50%;
  background: var(--surface-2, #eef0f5); color: var(--ink-muted);
  font-size: 10px; font-weight: 700;
  border: 2px solid #fff;
  margin-right: 4px;
}
.nt-asg-label { flex: 1; min-width: 0; text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.nt-asg-pop {
  position: absolute; top: calc(100% + 4px); left: 0; right: 0;
  background: #fff;
  border: 1px solid var(--border, #e6e9ef);
  border-radius: 10px;
  box-shadow: 0 16px 38px rgba(15,23,41,.18), 0 4px 10px rgba(15,23,41,.06);
  z-index: 60;
  max-height: 380px;
  display: flex; flex-direction: column;
  overflow: hidden;
}
.nt-asg-search {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 10px;
  border-bottom: 1px solid var(--border, #e6e9ef);
  color: var(--ink-muted, #9aa0a6);
}
.nt-asg-search input {
  flex: 1; border: none; outline: none;
  font: inherit; font-size: 13px;
  background: transparent;
  color: var(--ink, #0f1729);
}
.nt-asg-search-clear {
  border: none; background: transparent; cursor: pointer;
  color: var(--ink-muted); padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; border-radius: 4px;
}
.nt-asg-search-clear:hover { background: var(--surface-2, #f1f4f9); color: var(--ink); }

.nt-asg-list { overflow-y: auto; flex: 1; padding: 4px 0; }

.nt-asg-row {
  display: flex; align-items: center; gap: 10px;
  width: 100%;
  padding: 7px 12px;
  border: none; background: transparent;
  font: inherit; font-size: 13px;
  color: var(--ink, #0f1729);
  cursor: pointer; text-align: left;
}
.nt-asg-row:hover { background: var(--surface-2, #f6f7fb); }
.nt-asg-row.is-picked { background: rgba(0,115,234,.06); }
.nt-asg-row-name { font-weight: 600; flex-shrink: 0; }
.nt-asg-row-sub {
  font-size: 11px; color: var(--ink-muted);
  margin-left: auto;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  max-width: 50%;
}

.nt-asg-check {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px;
  border: 1.5px solid var(--border, #d3d6de);
  border-radius: 4px;
  color: #fff;
  flex-shrink: 0;
}
.nt-asg-check.is-on {
  background: var(--brand, #0073ea);
  border-color: var(--brand, #0073ea);
}

.nt-asg-self {
  border-bottom: 1px solid var(--border, #e6e9ef);
  color: var(--brand, #0073ea); font-weight: 600;
  background: rgba(0,115,234,.04);
}
.nt-asg-self:hover { background: rgba(0,115,234,.08); }

.nt-asg-empty {
  padding: 22px 14px;
  text-align: center;
  color: var(--ink-muted, #9aa0a6);
  font-size: 12.5px;
}

.nt-asg-foot {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 12px;
  border-top: 1px solid var(--border, #e6e9ef);
  background: var(--surface-2, #f6f7fb);
  font-size: 12px;
}
.nt-asg-foot-count { color: var(--ink-muted); flex: 1; }
.nt-asg-foot-clear {
  background: transparent; border: none; cursor: pointer;
  color: var(--ink-muted); font: inherit; font-size: 12px;
  padding: 4px 8px; border-radius: 4px;
}
.nt-asg-foot-clear:hover { background: rgba(15,23,41,.06); color: var(--ink); }
.nt-asg-foot-done {
  background: var(--brand, #0073ea); color: #fff;
  border: none; cursor: pointer;
  padding: 5px 14px; border-radius: 6px;
  font: inherit; font-size: 12px; font-weight: 600;
}
.nt-asg-foot-done:hover { background: #005bb8; }
`;

// ─────────────────────────────────────────────────────────────────────
// NtProjectPicker — small dropdown shown in the New Task modal's
// breadcrumb. Shows the picked project's color dot + name; click
// opens a search-filterable list. When `locked` is true the picker
// renders as a static chip — used by inline-add flows that already
// know which project the task belongs to and don't want the user
// accidentally moving it elsewhere mid-create.
// ─────────────────────────────────────────────────────────────────────
function NtProjectPicker({ projects, selectedId, locked, onChange }) {
  const [open, setOpen] = React.useState(false);
  const [q, setQ] = React.useState("");
  const ref = React.useRef(null);

  React.useEffect(() => {
    if (!open) return;
    function onDoc(e) { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }
    function onKey(e) { if (e.key === "Escape") { e.stopPropagation(); setOpen(false); } }
    document.addEventListener("mousedown", onDoc);
    document.addEventListener("keydown", onKey, true);
    return () => {
      document.removeEventListener("mousedown", onDoc);
      document.removeEventListener("keydown", onKey, true);
    };
  }, [open]);

  const list = (projects || []);
  const sel = list.find(p => p.id === selectedId) || null;
  const filtered = q.trim()
    ? list.filter(p => p.name.toLowerCase().includes(q.trim().toLowerCase()))
    : list;

  if (locked) {
    // Static chip — no dropdown affordance.
    return (
      <span className="nt-proj nt-proj-locked" title="Project is fixed for this task">
        <span className="nt-proj-dot" style={{ background: sel?.color || "#a3a8b6" }}/>
        <span className="nt-proj-name">{sel?.name || "—"}</span>
      </span>
    );
  }

  return (
    <span className="nt-proj" ref={ref}>
      <button
        type="button"
        className={`nt-proj-trigger ${open ? "is-open" : ""}`}
        onClick={() => setOpen(o => !o)}
        title={sel ? `Project · ${sel.name}` : "Pick a project"}>
        <span className="nt-proj-dot" style={{ background: sel?.color || "#a3a8b6" }}/>
        <span className="nt-proj-name">{sel?.name || "Pick a project"}</span>
        <Icons.ChevronSm size={11} style={{ color: "var(--ink-muted)" }}/>
      </button>
      {open && (
        <div className="nt-proj-pop" onMouseDown={(e) => e.stopPropagation()}>
          <div className="nt-proj-search">
            <Icons.Search size={12}/>
            <input
              type="text"
              autoFocus
              placeholder="Search projects…"
              value={q}
              onChange={(e) => setQ(e.target.value)}/>
          </div>
          <div className="nt-proj-list">
            {filtered.length === 0 && (
              <div className="nt-proj-empty">No projects match</div>
            )}
            {filtered.map(p => {
              const picked = p.id === selectedId;
              return (
                <button
                  key={p.id}
                  type="button"
                  className={`nt-proj-row ${picked ? "is-picked" : ""}`}
                  onClick={() => { onChange && onChange(p.id); setOpen(false); setQ(""); }}>
                  <span className="nt-proj-row-dot" style={{ background: p.color || "#a3a8b6" }}/>
                  <span className="nt-proj-row-name">{p.name}</span>
                  {picked && <Icons.Check size={12} style={{ color: "var(--brand)", marginLeft: "auto" }}/>}
                </button>
              );
            })}
          </div>
        </div>
      )}
      <style>{NT_PROJ_CSS}</style>
    </span>
  );
}

const NT_PROJ_CSS = `
.nt-proj { position: relative; display: inline-flex; align-items: center; }
.nt-proj-trigger {
  display: inline-flex; align-items: center; gap: 6px;
  background: rgba(0,115,234,.08);
  border: 1px solid rgba(0,115,234,.18);
  color: var(--brand, #0073ea);
  border-radius: 999px;
  padding: 3px 10px 3px 8px;
  font: inherit; font-size: 12px; font-weight: 600;
  cursor: pointer;
}
.nt-proj-trigger:hover { background: rgba(0,115,234,.14); }
.nt-proj-trigger.is-open {
  background: var(--brand, #0073ea); color: #fff;
  border-color: var(--brand, #0073ea);
}
.nt-proj-trigger.is-open .nt-proj-dot { box-shadow: 0 0 0 2px #fff; }
.nt-proj-locked {
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--surface-2, #eef0f5);
  color: var(--ink-muted, #676879);
  border-radius: 999px;
  padding: 3px 10px 3px 8px;
  font: inherit; font-size: 12px; font-weight: 600;
}
.nt-proj-dot {
  width: 8px; height: 8px; border-radius: 50%;
  display: inline-block; flex-shrink: 0;
}
.nt-proj-name {
  max-width: 240px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.nt-proj-pop {
  position: absolute; top: calc(100% + 6px); left: 0;
  min-width: 260px; max-width: 320px;
  background: #fff;
  border: 1px solid var(--border, #e6e9ef);
  border-radius: 10px;
  box-shadow: 0 16px 38px rgba(15,23,41,.18), 0 4px 10px rgba(15,23,41,.06);
  z-index: 80;
  max-height: 340px; overflow: hidden;
  display: flex; flex-direction: column;
}
.nt-proj-search {
  display: flex; align-items: center; gap: 6px;
  padding: 6px 10px;
  border-bottom: 1px solid var(--border, #e6e9ef);
  color: var(--ink-muted, #9aa0a6);
}
.nt-proj-search input {
  flex: 1; border: none; outline: none; background: transparent;
  font: inherit; font-size: 12.5px; color: var(--ink, #0f1729);
}
.nt-proj-list { overflow-y: auto; padding: 4px; flex: 1; }
.nt-proj-row {
  display: flex; align-items: center; gap: 10px; width: 100%;
  border: none; background: transparent; cursor: pointer;
  padding: 7px 10px; border-radius: 6px;
  font: inherit; font-size: 12.5px; color: var(--ink, #0f1729); text-align: left;
}
.nt-proj-row:hover { background: var(--surface-2, #f6f7fb); }
.nt-proj-row.is-picked { background: rgba(0,115,234,.08); }
.nt-proj-row-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.nt-proj-row-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nt-proj-empty {
  padding: 14px; text-align: center;
  color: var(--ink-muted); font-size: 12px;
}
`;

Object.assign(window, { NewTaskModal, NtAssigneePicker, NtProjectPicker });
