// dashboard-per-service-configuration-drawer.js — extracted verbatim from dashboard.js (pure move).

// ── Per-service configuration drawer ─────────────────────────────────
//
// Replaces the old "click card → navigate to /signals" pattern. Each
// always-on service now opens a focused drawer with:
//   - Status header (icon + name + state pill + last fired)
//   - Description
//   - User toggle (when togglable) — same as on the card
//   - Stats grid (schedule, last fired, next fire, run count, success%)
//   - Last error preview (when present)
//   - Service-specific inline editor:
//       • signal_twitter / signal_linkedin / signal_reddit / signal_hn
//         → keyword list + add/remove (per-platform)
//       • luma → subscription list (read-only summary + link to /luma)
//       • everything else → just the universal info
//   - "Open full management page →" footer link (when manageRoute exists)
//
// The drawer uses the existing openDrawer() infrastructure. Service
// data is pulled lazy on open (keywords / subs / runs) so the panel
// itself doesn't pay extra fetches on every render of the cockpit.

async function openServiceDrawer(service) {
  // Find the live job state for this service from the cached system-jobs
  // response stashed by buildAlwaysOnServicesPanel, OR fallback fetch.
  let jobRow = null;
  try {
    const cached = window.__systemJobsCache;
    if (cached?.jobs) {
      jobRow = cached.jobs.find((j) => j.name === service.jobName) || null;
    }
    if (!jobRow) {
      const fresh = await api("ops-system-jobs").catch(() => null);
      if (fresh?.jobs) {
        window.__systemJobsCache = fresh;
        jobRow = fresh.jobs.find((j) => j.name === service.jobName) || null;
      }
    }
  } catch {}

  const st = classifyServiceState(jobRow);
  const body = el("div", { style: "display:flex;flex-direction:column;gap:14px;" });

  // ── Description ──────────────────────────────────────────────────
  if (service.description) {
    body.appendChild(
      el(
        "div",
        {
          class: "muted small",
          style: "line-height:1.55;font-size:13px;color:var(--text-1);",
        },
        service.description,
      ),
    );
  }

  // ── State + toggle row ──────────────────────────────────────────
  const stateRow = el(
    "div",
    {
      style: [
        "display:flex",
        "align-items:center",
        "gap:10px",
        "padding:10px 12px",
        "background:" + st.color + "0c",
        "border:1px solid " + st.color + "33",
        "border-radius:8px",
      ].join(";"),
    },
    [
      el(
        "span",
        {
          style: `font-size:13px;color:${st.color};font-weight:600;font-family:ui-monospace,monospace;`,
        },
        `${st.icon} ${st.label}`,
      ),
      el("span", { style: "flex:1;" }),
      // Toggle (when service is togglable). Reuses the existing
      // buildServiceToggleSwitch helper. The `service.toggles` map was
      // attached when buildAlwaysOnServicesPanel ran; if the user opens
      // a card before that runs (cold path), the switch will assume ON.
      service.toggleKey ? buildServiceToggleSwitch(service, !service.userOff) : null,
    ],
  );
  body.appendChild(stateRow);

  // ── Stats grid (schedule / last fired / next fire / runs) ──────
  if (jobRow) {
    const grid = el("div", {
      style: "display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;",
    });
    const stat = (label, value, sub) =>
      el(
        "div",
        {
          style: [
            "padding:10px 12px",
            "background:rgba(255,255,255,0.02)",
            "border:1px solid var(--border)",
            "border-radius:6px",
            "display:flex",
            "flex-direction:column",
            "gap:2px",
          ].join(";"),
        },
        [
          el(
            "div",
            {
              style:
                "font-size:10px;color:var(--text-4);text-transform:uppercase;letter-spacing:0.04em;font-weight:600;",
            },
            label,
          ),
          el(
            "div",
            {
              style:
                "font-size:13px;font-weight:600;color:var(--text-1);font-variant-numeric:tabular-nums;",
            },
            String(value || "—"),
          ),
          sub
            ? el(
                "div",
                { style: "font-size:10px;color:var(--text-4);font-family:ui-monospace,monospace;" },
                sub,
              )
            : null,
        ],
      );
    grid.appendChild(stat("Schedule", jobRow.schedule || "—", "cron"));
    grid.appendChild(
      stat(
        "Last fired",
        jobRow.lastRunAt ? `${_drawerFmtSince(jobRow.lastRunAt)} ago` : "never",
        jobRow.lastDurationMs ? `${(jobRow.lastDurationMs / 1000).toFixed(1)}s` : "",
      ),
    );
    grid.appendChild(
      stat(
        "Next fire",
        jobRow.nextRunAt
          ? `in ${_drawerFmtSince(new Date(Date.now() + (new Date(jobRow.nextRunAt).getTime() - Date.now())).toISOString())}`
          : "—",
        "",
      ),
    );
    if ((jobRow.runCount || 0) > 0) {
      const successPct =
        jobRow.runCount > 0
          ? Math.round(((jobRow.runCount - (jobRow.errorCount || 0)) / jobRow.runCount) * 100)
          : null;
      grid.appendChild(
        stat("Runs", String(jobRow.runCount), successPct !== null ? `${successPct}% ok` : ""),
      );
    }
    body.appendChild(grid);
  }

  // ── Last error preview ──────────────────────────────────────────
  if (jobRow?.lastError) {
    body.appendChild(
      el(
        "div",
        {
          style: [
            "padding:10px 12px",
            "background:rgba(239,68,68,0.06)",
            "border:1px solid rgba(239,68,68,0.3)",
            "border-radius:6px",
            "font-size:11px",
            "color:var(--red)",
            "font-family:ui-monospace,monospace",
            "white-space:pre-wrap",
            "word-break:break-word",
          ].join(";"),
          title: jobRow.lastError,
        },
        [
          el("div", { style: "font-weight:600;margin-bottom:4px;" }, "Last error"),
          el("div", {}, jobRow.lastError),
        ],
      ),
    );
  }

  // ── Service-specific inline editor ──────────────────────────────
  // Build the editor area asynchronously after the drawer opens so the
  // initial paint is instant. The async builder appends into this mount.
  const editorMount = el("div", {});
  body.appendChild(editorMount);
  buildServiceEditor(service, editorMount).catch((err) => {
    editorMount.innerHTML = "";
    editorMount.appendChild(
      el(
        "div",
        { class: "muted small", style: "color:var(--red);" },
        `Couldn't load editor: ${err?.message || "unknown"}`,
      ),
    );
  });

  // ── Footer: full management page link ──────────────────────────
  if (service.manageRoute) {
    body.appendChild(
      el(
        "div",
        {
          style: "padding-top:8px;border-top:1px solid var(--border);font-size:11px;",
        },
        [
          el(
            "a",
            {
              style: "color:#a5b4fc;text-decoration:none;cursor:pointer;",
              onclick: () => {
                closeDrawer();
                navigate(service.manageRoute);
              },
            },
            `${service.manageLabel || "Open full management page"} →`,
          ),
        ],
      ),
    );
  }

  openDrawer(`${service.icon} ${service.label}`, body);
  logTelemetry("automations", "service_drawer_open", { service: service.jobName });
}

function _drawerFmtSince(ts) {
  if (!ts) return "—";
  const ms = Date.now() - new Date(ts).getTime();
  const sec = Math.max(0, Math.round(ms / 1000));
  if (sec < 60) return `${sec}s`;
  if (sec < 3600) return `${Math.round(sec / 60)}m`;
  const h = Math.floor(sec / 3600);
  if (h < 24) return `${h}h`;
  return `${Math.floor(h / 24)}d`;
}

// Service-specific inline editor. Appends into the mount. Each branch
// covers one service family; falls through to a generic "no inline
// editor — use the management page" message for services without
// dedicated knobs (DeFiLlama, RootData, smart-alerts, etc).
async function buildServiceEditor(service, mount) {
  // ── Signal scrapers: per-platform keyword editor ───────────────
  if (/^signal-(twitter|linkedin|reddit|hn)$/.test(service.jobName)) {
    const platformMap = {
      "signal-twitter": "twitter",
      "signal-linkedin": "linkedin",
      "signal-reddit": "reddit",
      "signal-hn": "hackernews",
    };
    const platform = platformMap[service.jobName];
    await renderSignalKeywordsEditor(mount, platform, service.label);
    return;
  }
  // ── Luma: subscription list + add ──────────────────────────────
  if (service.jobName === "luma-discovery" || service.jobName === "luma-approvals") {
    await renderLumaSubsEditor(mount);
    return;
  }
  // ── Generic: no inline editor ──────────────────────────────────
  mount.appendChild(
    el(
      "div",
      {
        style: [
          "padding:10px 12px",
          "background:rgba(255,255,255,0.02)",
          "border:1px dashed var(--border)",
          "border-radius:6px",
          "font-size:11px",
          "color:var(--text-2)",
          "line-height:1.5",
        ].join(";"),
      },
      service.manageRoute
        ? "No inline knobs for this service yet. Use the management page link below for finer control."
        : "This service runs globally on its own schedule — there are no per-user knobs to configure.",
    ),
  );
}

// Inline keyword editor for the four signal scrapers. Renders the
// platform's active+inactive keywords with add + remove + toggle.
async function renderSignalKeywordsEditor(mount, platform, serviceLabel) {
  mount.appendChild(
    el(
      "div",
      {
        style:
          "font-size:11px;color:var(--text-2);text-transform:uppercase;letter-spacing:0.05em;font-weight:600;margin-bottom:6px;",
      },
      "Keywords",
    ),
  );

  const refreshKeywords = async () => {
    const data = await api("signals-keywords-list").catch(() => ({}));
    const all = (data?.keywords || []).filter(
      (k) => k.platform === platform || k.platform === "all",
    );
    keywordList.innerHTML = "";
    if (all.length === 0) {
      keywordList.appendChild(
        el(
          "div",
          {
            class: "muted small",
            style: "padding:8px 0;font-style:italic;",
          },
          `No keywords configured. ${serviceLabel} won't fire until you add at least one — built-in dealflow keywords still run regardless.`,
        ),
      );
      return;
    }
    for (const kw of all) {
      keywordList.appendChild(buildKeywordPill(kw, platform, refreshKeywords));
    }
  };

  const keywordList = el("div", {
    style: "display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px;",
  });
  mount.appendChild(keywordList);

  // Add row
  const addInput = el("input", {
    type: "text",
    placeholder: `Add a ${platform} keyword…`,
    style:
      "flex:1;padding:7px 10px;background:var(--bg-1);border:1px solid var(--border);border-radius:6px;color:var(--text-1);font-size:12px;",
    onkeydown: async (e) => {
      if (e.key !== "Enter") return;
      const value = addInput.value.trim();
      if (!value) return;
      addBtn.click();
    },
  });
  const addBtn = el(
    "button",
    {
      type: "button",
      class: "btn btn-tiny btn-primary",
      onclick: async () => {
        const value = addInput.value.trim();
        if (!value) {
          toast("Type a keyword first", "err");
          return;
        }
        addBtn.disabled = true;
        addBtn.textContent = "Adding…";
        const r = await api("signals-keywords-add", { body: { keyword: value, platform } }).catch(
          (e) => ({ error: e?.message }),
        );
        addBtn.disabled = false;
        addBtn.textContent = "+ Add";
        if (r?.error) {
          toast(`Failed: ${r.error}`, "err");
          return;
        }
        addInput.value = "";
        toast(`Added "${value}"`, "ok", 1500);
        logTelemetry("automations", "keyword_add", { platform });
        refreshKeywords();
      },
    },
    "+ Add",
  );
  mount.appendChild(
    el("div", { style: "display:flex;gap:6px;align-items:center;" }, [addInput, addBtn]),
  );

  await refreshKeywords();
}

function buildKeywordPill(kw, platform, refresh) {
  return el(
    "span",
    {
      style: [
        "display:inline-flex",
        "align-items:center",
        "gap:5px",
        "padding:4px 10px",
        "background:" + (kw.is_active ? "rgba(99,102,241,0.12)" : "rgba(75,85,99,0.15)"),
        "border:1px solid " + (kw.is_active ? "rgba(99,102,241,0.35)" : "rgba(75,85,99,0.3)"),
        "border-radius:14px",
        "font-size:11px",
        "color:" + (kw.is_active ? "#e5e7eb" : "#9ca3af"),
        "font-family:ui-monospace,monospace",
      ].join(";"),
      title: kw.is_active ? "Click ✕ to delete" : "Inactive — click ✕ to delete",
    },
    [
      el("span", {}, kw.keyword),
      kw.platform === "all"
        ? el("span", { style: "color:var(--text-4);font-size:10px;" }, "·all")
        : null,
      el(
        "span",
        {
          style: "color:var(--text-4);cursor:pointer;padding-left:3px;",
          title: "Delete keyword",
          onclick: async (e) => {
            e.stopPropagation();
            if (!confirm(`Delete keyword "${kw.keyword}"?`)) return;
            const r = await api("signals-keywords-delete", { keywordId: kw.id }).catch((e) => ({
              error: e?.message,
            }));
            if (r?.error) {
              toast(`Failed: ${r.error}`, "err");
              return;
            }
            toast(`Deleted "${kw.keyword}"`, "ok", 1500);
            logTelemetry("automations", "keyword_delete", { platform });
            refresh();
          },
        },
        "✕",
      ),
    ],
  );
}

// Inline Luma subscriptions summary — read-only list + delete + a deep
// link to /luma for the full add-subscription flow. Adding subs from
// inside the drawer would duplicate the management page's flow; the
// drawer focuses on quick disable/delete.
async function renderLumaSubsEditor(mount) {
  mount.appendChild(
    el(
      "div",
      {
        style:
          "font-size:11px;color:var(--text-2);text-transform:uppercase;letter-spacing:0.05em;font-weight:600;margin-bottom:6px;",
      },
      "Subscriptions",
    ),
  );

  const data = await api("luma-subs").catch(() => ({}));
  const subs = data?.subs || data?.subscriptions || [];

  if (subs.length === 0) {
    mount.appendChild(
      el(
        "div",
        {
          class: "muted small",
          style: "padding:8px 0;font-style:italic;",
        },
        "No active subscriptions. Click 'Open full management page' below to link your Luma account and create one.",
      ),
    );
    return;
  }

  const list = el("div", { style: "display:flex;flex-direction:column;gap:6px;" });
  for (const s of subs) {
    list.appendChild(
      el(
        "div",
        {
          style: [
            "display:flex",
            "align-items:center",
            "gap:10px",
            "padding:8px 10px",
            "background:rgba(255,255,255,0.02)",
            "border:1px solid var(--border)",
            "border-radius:6px",
            "font-size:12px",
          ].join(";"),
        },
        [
          el("span", {
            style: `width:8px;height:8px;border-radius:50%;background:${s.status === "active" ? "var(--green)" : "#71717a"};flex-shrink:0;`,
          }),
          el(
            "div",
            {
              style:
                "flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-1);",
            },
            s.keywords || s.feed_url || s.name || `Subscription #${s.id}`,
          ),
          el(
            "span",
            { class: "muted small", style: "font-family:ui-monospace,monospace;" },
            s.status === "active" ? "active" : s.status || "paused",
          ),
          el(
            "button",
            {
              class: "btn btn-tiny btn-secondary",
              type: "button",
              title: "Delete subscription",
              onclick: async () => {
                if (!confirm("Delete this Luma subscription?")) return;
                const r = await api("luma-sub-delete", { subId: s.id }).catch((e) => ({
                  error: e?.message,
                }));
                if (r?.error) {
                  toast(`Failed: ${r.error}`, "err");
                  return;
                }
                toast("Deleted", "ok", 1500);
                renderLumaSubsEditor(mount.replaceWith(el("div", {})));
              },
            },
            "✕",
          ),
        ],
      ),
    );
  }
  mount.appendChild(list);
}

// Shared "fresh inbound replies" business rule. Both the Automations cockpit
// (buildCockpitNeedsAttention) and Today (renderMission) surface the same
// filtered feed of genuine inbound replies, differing only in how they render
// it. This is a data-classification rule, not presentation — keep it in ONE
// place so a new backend inbound event_type is honored by both surfaces at once.
//
// Filters the inboxResp.replies feed to genuine inbound since `lastVisitAt`
// (falling back to a 24h window on first visit): drops rows older than the
// cutoff, drops pure notification_received (likes/follows/mentions) unless they
// carry an intent_classification, and keeps high-signal event types or rows from
// the inbox_messages / linkedin_connection_requests source tables.
function freshInboundReplies(replies, lastVisitAt) {
  const HIGH_SIGNAL_TYPES = new Set([
    "reply_received",
    "reply_detected",
    "connect_accepted",
    "connection_request",
    "dm_received",
    "message_received",
  ]);
  const cutoffMs = lastVisitAt ? new Date(lastVisitAt).getTime() : Date.now() - 24 * 3600 * 1000;
  return (Array.isArray(replies) ? replies : []).filter((r) => {
    const ts = new Date(r.created_at).getTime();
    if (!Number.isFinite(ts) || ts < cutoffMs) return false;
    // Drop generic notifications — they're not "replies."
    // Keep classified intent_classification rows from inbox_messages.
    if (r.event_type === "notification_received" && !r.intent_classification) return false;
    // Keep if it's high-signal OR from inbox_messages/connection_requests.
    return (
      HIGH_SIGNAL_TYPES.has(r.event_type) ||
      r.source_table === "inbox_messages" ||
      r.source_table === "linkedin_connection_requests" ||
      !!r.intent_classification
    );
  });
}

function buildCockpitNeedsAttention(opsLive, summary, coverage, workflows, inboxResp, lastVisitAt) {
  const items = [];

  // Replies-waiting (highest-priority — emotional payload). Filter the
  // inbox-replies feed to genuine inbound (drop pure notification_received
  // — likes/follows/mentions are noise here; they belong in the inbox UI
  // proper). Cutoff is `lastVisitAt`, falling back to 24h on first visit.
  // Skip if inbox call errored.
  const replies = Array.isArray(inboxResp?.replies) ? inboxResp.replies : [];
  if (replies.length > 0) {
    const fresh = freshInboundReplies(replies, lastVisitAt);
    if (fresh.length > 0) {
      // Build a one-line preview so the user can decide if it's worth
      // opening inbox right now. Most-recent reply wins the preview.
      const sample = fresh[0];
      const who = sample.contact_name || sample.contact_handle || "someone";
      const when = (() => {
        const ms = Date.now() - new Date(sample.created_at).getTime();
        const sec = Math.max(0, Math.round(ms / 1000));
        if (sec < 60) return `${sec}s ago`;
        if (sec < 3600) return `${Math.round(sec / 60)}m ago`;
        return `${Math.round(sec / 3600)}h ago`;
      })();
      const cutoffLabel = lastVisitAt ? "since you were here" : "in the last 24h";
      items.push({
        severity: "replies",
        icon: "✉",
        color: "#22c55e",
        bg: "rgba(34,197,94,0.08)",
        label:
          fresh.length === 1
            ? `1 new reply ${cutoffLabel} — ${who} replied ${when}`
            : `${fresh.length} new replies ${cutoffLabel} — latest from ${who} ${when}`,
        action: "Open inbox",
        onClick: () => navigate("inbox"),
      });
    }
  }

  // Errors in last 24h. Phase I: summary.error now includes BOTH legacy
  // custom_workflow_runs AND composer composition_runs (merged backend-side
  // in getRunsSummary). The breakout is at summary.composer.error so we can
  // still surface provenance ("3 errored: 1 legacy + 2 composer").
  const errorCount = Number(summary?.error) || 0;
  if (errorCount > 0) {
    const composerErrors = Number(summary?.composer?.error) || 0;
    const legacyErrors = Number(summary?.legacy?.error) || 0;
    // Build the breakout note when both worlds contributed
    let breakoutNote = "";
    if (composerErrors > 0 && legacyErrors > 0) {
      breakoutNote = ` (${legacyErrors} legacy + ${composerErrors} composer)`;
    } else if (composerErrors > 0) {
      breakoutNote = ` (${composerErrors} composer)`;
    }
    // For click destination, prefer the most recent composer error from the
    // live feed if any — that lets the user land directly on the canvas of
    // the broken thing. Falls back to the Active list otherwise.
    const recentComposerError = (opsLive?.compositionRuns || []).find((r) => r.status === "error");
    items.push({
      severity: "error",
      icon: "⚠",
      color: "#ef4444",
      bg: "rgba(239,68,68,0.08)",
      label: `${errorCount} ${errorCount === 1 ? "run" : "runs"} errored in the last 24h${breakoutNote}`,
      action: "Inspect",
      onClick: () => {
        if (recentComposerError) {
          navigate(`workflows/canvas?id=${recentComposerError.compositionId}`);
        } else {
          navigate("automations");
        }
      },
    });
  }

  // Broken-verified workflows from coverage data. brokenPresets is an
  // array of preset ids that have a verified="broken" run.
  const broken = Array.isArray(coverage?.brokenPresets) ? coverage.brokenPresets : [];
  if (broken.length > 0) {
    items.push({
      severity: "broken",
      icon: "🔴",
      color: "#dc2626",
      bg: "rgba(220,38,38,0.08)",
      label: `${broken.length} ${broken.length === 1 ? "automation" : "automations"} marked broken — needs investigation`,
      action: "View",
      onClick: () => {
        // Scroll to the customs section if visible. Fallback to no-op.
        const target = document.querySelector(".automation-card-actions");
        if (target) target.scrollIntoView({ behavior: "smooth", block: "center" });
      },
    });
  }

  // Stale wired customs: in opsLive.customWorkflows, look for entries with
  // lastRunAt older than 24h (and missing/null lastRunAt for actively
  // wired ones is also a flag — engine should have fired by now).
  const STALE_HOURS = 24;
  const staleCutoff = Date.now() - STALE_HOURS * 3600 * 1000;
  const staleCustoms = (opsLive?.customWorkflows || []).filter((c) => {
    if (!c.lastRunAt) return true; // never run despite being wired
    return new Date(c.lastRunAt).getTime() < staleCutoff;
  });
  if (staleCustoms.length > 0) {
    const sample = staleCustoms
      .slice(0, 2)
      .map((c) => c.name)
      .join(", ");
    const more = staleCustoms.length > 2 ? ` +${staleCustoms.length - 2} more` : "";
    items.push({
      severity: "stale",
      icon: "💤",
      color: "#fbbf24",
      bg: "rgba(251,191,36,0.06)",
      label: `${staleCustoms.length} ${staleCustoms.length === 1 ? "automation hasn't" : "automations haven't"} run in 24h+: ${sample}${more}`,
      action: "Review",
      onClick: () => {
        // Jump to the customs section.
        const myWorkflowsHeader = [
          ...document.querySelectorAll(".automation-section-title, h2, h3"),
        ].find((n) => /my workflows/i.test(n.textContent || ""));
        if (myWorkflowsHeader)
          myWorkflowsHeader.scrollIntoView({ behavior: "smooth", block: "start" });
      },
    });
  }

  // Cap at 5 items — a long list defeats the purpose (Hick's Law).
  if (items.length === 0) return null;
  const trimmed = items.slice(0, 5);

  const wrap = el("div", {
    id: "cockpit-needs-attention",
    style: [
      "display:flex",
      "flex-direction:column",
      "gap:6px",
      "padding:10px 14px",
      "margin:6px 0 8px",
      "border:1px solid rgba(239,68,68,0.18)",
      "border-radius:10px",
      "background:rgba(239,68,68,0.03)",
    ].join(";"),
  });
  wrap.appendChild(
    el(
      "div",
      {
        style: "display:flex;align-items:baseline;gap:8px;",
      },
      [
        el(
          "span",
          {
            style:
              "font-size:11px;color:var(--text-2);text-transform:uppercase;letter-spacing:0.05em;",
          },
          "Needs attention",
        ),
        el(
          "span",
          { style: "font-size:11px;color:var(--text-4);font-family:ui-monospace,monospace;" },
          `${trimmed.length}`,
        ),
      ],
    ),
  );

  for (const item of trimmed) {
    wrap.appendChild(
      el(
        "div",
        {
          style: [
            "display:flex",
            "align-items:center",
            "gap:10px",
            "padding:6px 10px",
            "background:" + item.bg,
            "border:1px solid " + item.color + "33",
            "border-radius:6px",
            "font-size:12px",
          ].join(";"),
        },
        [
          el("span", { style: `font-size:13px;color:${item.color};flex-shrink:0;` }, item.icon),
          el("span", { style: "color:var(--text-1);flex:1;line-height:1.4;" }, item.label),
          el(
            "button",
            {
              type: "button",
              style: [
                "padding:3px 10px",
                "background:transparent",
                "border:1px solid " + item.color + "55",
                "border-radius:4px",
                "color:" + item.color,
                "font-size:11px",
                "font-weight:500",
                "cursor:pointer",
              ].join(";"),
              onclick: (e) => {
                e.stopPropagation();
                item.onClick && item.onClick();
              },
            },
            item.action,
          ),
        ],
      ),
    );
  }

  return wrap;
}
