refactor(admin): extract emailsScript to src/scripts/client/emails-page.ts

Moves the inline JS template literal from emails.tsx into a typed
TypeScript source file. The dynamic feedId value (previously interpolated
directly) is now passed via a window.__APP_CONFIG__ bootstrap script
injected immediately before the compiled static script in the HTML.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
Julien HerrandClaude Sonnet 4.6 committed 2026-05-22 13:50:54 +02:00
1 parent 40f4b42cd5
commit 0f6670d0e9
3 files changed
+643 -315

No files matched your search

+632
View File
@@ -0,0 +1,632 @@
// Client-side script for the admin emails page.
// Compiled by scripts/build-client.mjs — do not import DOM types from here in Worker code.
// feedId is read at runtime from window.__APP_CONFIG__ (injected as a config bootstrap script before this one).
const EMAIL_FEED_ID: string =
(window as unknown as { __APP_CONFIG__?: { feedId?: string } }).__APP_CONFIG__
?.feedId ?? "";
let EMAIL_ROWS: HTMLElement[] = [];
let EMAIL_CHECKBOXES: HTMLInputElement[] = [];
let EMAIL_SELECTED_COUNT_EL: HTMLElement | null = null;
let EMAIL_MATCH_COUNT_EL: HTMLElement | null = null;
let EMAIL_TOTAL_COUNT_EL: HTMLElement | null = null;
let EMAIL_BULK_DELETE_BUTTON_EL: HTMLButtonElement | null = null;
let EMAIL_SELECT_ALL_EL: HTMLInputElement | null = null;
let EMAIL_FILTER_TIMER: ReturnType<typeof setTimeout> | null = null;
let EMAIL_BULK_DELETE_IN_PROGRESS = false;
let EMAIL_SORT_KEY = "receivedAt";
let EMAIL_SORT_DIR = "desc";
const EMAIL_COLLATOR = new Intl.Collator(undefined, {
numeric: true,
sensitivity: "base",
});
function initEmailUI(): void {
EMAIL_ROWS = Array.from(document.querySelectorAll<HTMLElement>(".email-row"));
EMAIL_CHECKBOXES = Array.from(
document.querySelectorAll<HTMLInputElement>(".email-select"),
);
EMAIL_SELECTED_COUNT_EL = document.getElementById("selected-email-count");
EMAIL_MATCH_COUNT_EL = document.getElementById("email-match-count");
EMAIL_TOTAL_COUNT_EL = document.getElementById("email-total-count");
EMAIL_BULK_DELETE_BUTTON_EL = document.getElementById(
"bulk-delete-emails-button",
) as HTMLButtonElement | null;
EMAIL_SELECT_ALL_EL = document.getElementById(
"select-all-emails",
) as HTMLInputElement | null;
setupEmailTableResizing();
setupEmailTableSorting();
setupEmailDeleteButtons();
updateEmailMatchCount();
updateEmailSelectionState();
}
function updateEmailMatchCount(): void {
if (!EMAIL_MATCH_COUNT_EL) return;
const total = EMAIL_ROWS.length;
const visible = EMAIL_ROWS.filter((row) => !row.hidden).length;
const query = (
(document.getElementById("email-search") as HTMLInputElement | null)
?.value || ""
).trim();
EMAIL_MATCH_COUNT_EL.textContent = query
? "Showing " + visible + " of " + total
: "Showing " + total;
}
function scheduleEmailFilter(): void {
if (EMAIL_FILTER_TIMER) clearTimeout(EMAIL_FILTER_TIMER);
EMAIL_FILTER_TIMER = setTimeout(filterEmailRows, 120);
}
function getEmailSortValue(row: HTMLElement, key: string): string {
const prop = "sort" + key.charAt(0).toUpperCase() + key.slice(1);
return row.dataset && row.dataset[prop] ? row.dataset[prop]! : "";
}
function updateEmailSortIndicators(table: Element): void {
Array.from(table.querySelectorAll<HTMLElement>("th[data-sort-key]")).forEach(
(th) => {
const key = th.getAttribute("data-sort-key") || "";
const indicator = th.querySelector(".sort-indicator");
const active = key === EMAIL_SORT_KEY;
if (indicator)
indicator.textContent = active
? EMAIL_SORT_DIR === "asc"
? "^"
: "v"
: "";
th.setAttribute(
"aria-sort",
active
? EMAIL_SORT_DIR === "asc"
? "ascending"
: "descending"
: "none",
);
},
);
}
function sortEmailTableBy(key: string): void {
const table = document.querySelector("table.table-emails");
const tbody = table ? table.querySelector("tbody") : null;
if (!table || !tbody) return;
if (EMAIL_SORT_KEY === key) {
EMAIL_SORT_DIR = EMAIL_SORT_DIR === "asc" ? "desc" : "asc";
} else {
EMAIL_SORT_KEY = key;
EMAIL_SORT_DIR = key === "receivedAt" ? "desc" : "asc";
}
const dir = EMAIL_SORT_DIR === "asc" ? 1 : -1;
const rows = Array.from(tbody.querySelectorAll<HTMLElement>(".email-row"));
rows.sort(
(a, b) =>
dir *
EMAIL_COLLATOR.compare(
getEmailSortValue(a, EMAIL_SORT_KEY),
getEmailSortValue(b, EMAIL_SORT_KEY),
),
);
const frag = document.createDocumentFragment();
rows.forEach((row) => frag.appendChild(row));
tbody.appendChild(frag);
updateEmailSortIndicators(table);
}
function setupEmailTableSorting(): void {
const table = document.querySelector("table.table-emails");
if (!table) return;
table
.querySelectorAll<HTMLButtonElement>("button.th-button[data-sort-key]")
.forEach((btn) => {
btn.addEventListener("click", () => {
const key = btn.getAttribute("data-sort-key");
if (key) sortEmailTableBy(key);
});
});
updateEmailSortIndicators(table);
}
function setupEmailTableResizing(): void {
const table = document.querySelector("table.table-emails");
if (!table) return;
const storageKey = "email-to-rss.admin.emailsTable.colWidths";
const minWidths: Record<string, number> = {
subject: 240,
receivedAt: 180,
actions: 160,
};
const defaultWidths: Record<string, number> = {
subject: 520,
receivedAt: 220,
actions: 200,
};
const cols = Array.from(table.querySelectorAll<HTMLElement>("colgroup col"));
const colByKey: Record<string, HTMLElement> = {};
cols.forEach((col) => {
const key = col.getAttribute("data-col");
if (key) colByKey[key] = col;
});
try {
const saved = JSON.parse(
localStorage.getItem(storageKey) || "{}",
) as Record<string, unknown>;
Object.keys(saved || {}).forEach((key) => {
const px = Number(saved[key]);
if (colByKey[key] && Number.isFinite(px))
colByKey[key].style.width = px + "px";
});
} catch {
/* ignore */
}
const persist = () => {
try {
const out: Record<string, number> = {};
Object.keys(colByKey).forEach((key) => {
if (key === "select") return;
const px = parseInt(colByKey[key].style.width || "0", 10);
if (Number.isFinite(px) && px > 0) out[key] = px;
});
localStorage.setItem(storageKey, JSON.stringify(out));
} catch {
/* ignore */
}
};
type ActiveResize = {
key: string;
col: HTMLElement;
startX: number;
startWidth: number;
};
let active: ActiveResize | null = null;
let rafId = 0;
let pendingWidth = 0;
table.querySelectorAll<HTMLElement>(".col-resizer").forEach((handle) => {
handle.addEventListener("pointerdown", (event: PointerEvent) => {
event.preventDefault();
event.stopPropagation();
const key = handle.getAttribute("data-col");
const col = key ? colByKey[key] : null;
if (!key || !col) return;
const th = handle.closest("th");
const startWidth = th
? th.getBoundingClientRect().width
: parseInt(col.style.width || "0", 10) || 200;
active = { key, col, startX: event.clientX, startWidth };
document.body.classList.add("is-resizing");
handle.setPointerCapture(event.pointerId);
});
handle.addEventListener("pointermove", (event: PointerEvent) => {
if (!active) return;
const minPx = minWidths[active.key] || 180;
pendingWidth = Math.max(
minPx,
Math.round(active.startWidth + (event.clientX - active.startX)),
);
if (rafId) return;
rafId = requestAnimationFrame(() => {
active!.col.style.width = pendingWidth + "px";
rafId = 0;
});
});
const finish = () => {
if (!active) return;
active = null;
document.body.classList.remove("is-resizing");
persist();
};
handle.addEventListener("pointerup", finish);
handle.addEventListener("pointercancel", finish);
handle.addEventListener("dblclick", (event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
const key = handle.getAttribute("data-col");
const col = key ? colByKey[key] : null;
const px = key ? defaultWidths[key] : null;
if (!key || !col || !px) return;
col.style.width = px + "px";
persist();
});
});
}
const EMAIL_DELETE_CONFIRM_LABEL = "Confirm delete";
const EMAIL_DELETE_CONFIRM_TIMEOUT_MS = 4000;
function resetEmailDeleteButton(buttonEl: HTMLButtonElement): void {
if (!buttonEl) return;
buttonEl.classList.remove("is-confirming");
buttonEl.removeAttribute("data-confirming");
buttonEl.disabled = false;
buttonEl.textContent = buttonEl.dataset.originalLabel || "Delete";
}
function setEmailButtonLoading(
buttonEl: HTMLButtonElement | null,
loading: boolean,
label?: string,
): void {
if (!buttonEl) return;
if (loading) {
if (!buttonEl.dataset.originalLabel)
buttonEl.dataset.originalLabel = (buttonEl.textContent || "").trim();
buttonEl.classList.add("is-loading");
buttonEl.disabled = true;
buttonEl.textContent = "";
const spinner = document.createElement("span");
spinner.className = "spinner";
spinner.setAttribute("aria-hidden", "true");
buttonEl.appendChild(spinner);
buttonEl.appendChild(
document.createTextNode(" " + (label || "Working...")),
);
return;
}
buttonEl.classList.remove("is-loading");
buttonEl.textContent =
buttonEl.dataset.originalLabel || (buttonEl.textContent || "").trim();
}
function animateEmailRowRemoval(
row: HTMLElement | null,
onDone?: () => void,
): void {
if (!row) {
if (onDone) onDone();
return;
}
row.classList.add("is-removing");
window.setTimeout(() => {
row.remove();
if (onDone) onDone();
}, 240);
}
async function deleteEmailRequest(
emailKey: string,
feedId: string,
): Promise<unknown> {
const res = await fetch(
"/admin/emails/" +
encodeURIComponent(emailKey) +
"/delete?feedId=" +
encodeURIComponent(feedId),
{
method: "POST",
headers: { Accept: "application/json" },
credentials: "same-origin",
},
);
const data = (await res.json().catch(() => ({}))) as Record<string, unknown>;
if (!res.ok)
throw new Error(
data && data.error
? String(data.error)
: "Request failed (" + res.status + ")",
);
return data;
}
function refreshEmailRowCache(): void {
EMAIL_ROWS = Array.from(document.querySelectorAll<HTMLElement>(".email-row"));
EMAIL_CHECKBOXES = Array.from(
document.querySelectorAll<HTMLInputElement>(".email-select"),
);
if (EMAIL_TOTAL_COUNT_EL)
EMAIL_TOTAL_COUNT_EL.textContent = String(EMAIL_ROWS.length);
updateEmailMatchCount();
updateEmailSelectionState();
}
interface ToastHandle {
update?: (msg: string, opts?: Record<string, unknown>) => void;
dismiss?: () => void;
}
declare global {
interface Window {
showToast?: (msg: string, opts?: Record<string, unknown>) => ToastHandle;
parseJsonResponseOrThrow?: (
res: Response,
opts?: Record<string, unknown>,
) => Promise<Record<string, unknown>>;
}
}
function setupEmailDeleteButtons(): void {
Array.from(
document.querySelectorAll<HTMLButtonElement>(
'button[data-delete-kind="email"]',
),
).forEach((button) => {
if (button.dataset.deleteReady === "true") return;
button.dataset.deleteReady = "true";
button.dataset.originalLabel =
(button.textContent || "").trim() || "Delete";
let confirming = false;
let confirmTimer = 0;
let inFlight = false;
const startConfirm = () => {
confirming = true;
button.classList.add("is-confirming");
button.setAttribute("data-confirming", "true");
button.textContent = EMAIL_DELETE_CONFIRM_LABEL;
if (confirmTimer) window.clearTimeout(confirmTimer);
confirmTimer = window.setTimeout(() => {
confirming = false;
resetEmailDeleteButton(button);
}, EMAIL_DELETE_CONFIRM_TIMEOUT_MS);
};
button.addEventListener("click", async (event: MouseEvent) => {
event.preventDefault();
if (inFlight) return;
if (!confirming) {
startConfirm();
return;
}
if (confirmTimer) window.clearTimeout(confirmTimer);
inFlight = true;
setEmailButtonLoading(button, true, "Deleting...");
const toast = window.showToast
? window.showToast("Deleting email...", {
type: "info",
loading: true,
duration: 0,
})
: null;
const emailKey = button.getAttribute("data-email-key") || "";
const feedId = button.getAttribute("data-feed-id") || EMAIL_FEED_ID;
const row = button.closest<HTMLElement>(".email-row");
try {
await deleteEmailRequest(emailKey, feedId);
if (toast && toast.update)
toast.update("Email deleted.", {
type: "success",
loading: false,
duration: 3200,
});
else if (window.showToast)
window.showToast("Email deleted.", { type: "success" });
animateEmailRowRemoval(row, () => refreshEmailRowCache());
} catch (error) {
const msg =
"Delete failed: " +
(error instanceof Error && error.message
? error.message
: "Unknown error");
if (toast && toast.update)
toast.update(msg, { type: "error", loading: false });
else if (window.showToast) window.showToast(msg, { type: "error" });
setEmailButtonLoading(button, false);
confirming = false;
resetEmailDeleteButton(button);
} finally {
inFlight = false;
if (!row) {
setEmailButtonLoading(button, false);
confirming = false;
resetEmailDeleteButton(button);
}
}
});
button.addEventListener("keydown", (event: KeyboardEvent) => {
if (event.key === "Escape" && confirming && !inFlight) {
confirming = false;
if (confirmTimer) window.clearTimeout(confirmTimer);
resetEmailDeleteButton(button);
}
});
});
}
function updateEmailSelectionState(): void {
if (!EMAIL_CHECKBOXES.length) return;
const selected = EMAIL_CHECKBOXES.filter((c) => c.checked);
if (EMAIL_SELECTED_COUNT_EL)
EMAIL_SELECTED_COUNT_EL.textContent = selected.length + " selected";
if (EMAIL_BULK_DELETE_BUTTON_EL)
EMAIL_BULK_DELETE_BUTTON_EL.disabled = selected.length === 0;
if (EMAIL_SELECT_ALL_EL) {
const visible = EMAIL_CHECKBOXES.filter(
(c) => !(c.closest("tr") as HTMLElement | null)?.hidden,
);
EMAIL_SELECT_ALL_EL.checked =
visible.length > 0 && visible.every((c) => c.checked);
}
}
function toggleAllEmails(checked: boolean): void {
EMAIL_CHECKBOXES.forEach((c) => {
if (!(c.closest("tr") as HTMLElement | null)?.hidden) c.checked = checked;
});
updateEmailSelectionState();
}
function selectMatchingEmails(): void {
EMAIL_CHECKBOXES.forEach((c) => {
if (!(c.closest("tr") as HTMLElement | null)?.hidden) c.checked = true;
});
updateEmailSelectionState();
}
function clearEmailSelection(): void {
EMAIL_CHECKBOXES.forEach((c) => {
c.checked = false;
});
updateEmailSelectionState();
}
function filterEmailRows(): void {
const query = (
(document.getElementById("email-search") as HTMLInputElement | null)
?.value || ""
)
.toLowerCase()
.trim();
EMAIL_ROWS.forEach((row) => {
row.hidden =
!!query && !(row.getAttribute("data-search") || "").includes(query);
});
updateEmailMatchCount();
updateEmailSelectionState();
}
function confirmBulkEmailDelete(): boolean {
const selected = EMAIL_CHECKBOXES.filter((c) => c.checked).length;
if (selected === 0) return false;
const query = (
(document.getElementById("email-search") as HTMLInputElement | null)
?.value || ""
).trim();
const extra =
selected >= 200 && !query
? "\n\nThis is a large delete. Tip: use Search to narrow down spam first."
: "";
return confirm("Delete " + selected + " selected email(s)?" + extra);
}
function removeEmailRowsByKey(emailKeys: string[]): void {
const toRemove = new Set((emailKeys || []).map((v) => String(v)));
if (!toRemove.size) return;
EMAIL_ROWS.forEach((row) => {
const cb = row.querySelector<HTMLInputElement>("input.email-select");
if (cb && toRemove.has(cb.value)) row.remove();
});
EMAIL_ROWS = Array.from(document.querySelectorAll<HTMLElement>(".email-row"));
EMAIL_CHECKBOXES = Array.from(
document.querySelectorAll<HTMLInputElement>(".email-select"),
);
if (EMAIL_TOTAL_COUNT_EL)
EMAIL_TOTAL_COUNT_EL.textContent = String(EMAIL_ROWS.length);
}
function onBulkEmailDeleteSubmit(event: Event): boolean {
if (event && event.preventDefault) event.preventDefault();
void bulkDeleteSelectedEmails();
return false;
}
async function bulkDeleteSelectedEmails(): Promise<void> {
if (EMAIL_BULK_DELETE_IN_PROGRESS) return;
const selectedKeys = EMAIL_CHECKBOXES.filter((c) => c.checked).map(
(c) => c.value,
);
if (!selectedKeys.length) {
if (window.showToast)
window.showToast("No emails selected.", { type: "info" });
return;
}
if (!confirmBulkEmailDelete()) return;
EMAIL_BULK_DELETE_IN_PROGRESS = true;
setEmailButtonLoading(EMAIL_BULK_DELETE_BUTTON_EL, true, "Deleting...");
const toast = window.showToast
? window.showToast("Deleting " + selectedKeys.length + " email(s)...", {
type: "info",
loading: true,
duration: 0,
})
: null;
const batchSize = 50;
let deletedTotal = 0;
const failed: string[] = [];
try {
const url =
"/admin/feeds/" +
encodeURIComponent(EMAIL_FEED_ID) +
"/emails/bulk-delete";
for (let i = 0; i < selectedKeys.length; i += batchSize) {
const batch = selectedKeys.slice(i, i + batchSize);
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "same-origin",
body: JSON.stringify({ emailKeys: batch }),
});
let data: Record<string, unknown> = {};
if (window.parseJsonResponseOrThrow) {
data = await window.parseJsonResponseOrThrow(res, {
prefix: "Bulk email delete failed",
});
} else {
data = (await res.json().catch(() => ({}))) as Record<string, unknown>;
if (!res.ok)
throw new Error(
data && data.error
? String(data.error)
: "Bulk email delete failed (HTTP " + res.status + ")",
);
}
const deletedKeys = Array.isArray(data.deletedEmailKeys)
? (data.deletedEmailKeys as string[])
: batch;
removeEmailRowsByKey(deletedKeys);
deletedTotal += deletedKeys.length;
failed.push(
...(Array.isArray(data.failedEmailKeys)
? (data.failedEmailKeys as string[])
: []),
);
if (toast && toast.update)
toast.update(
"Deleting... (" +
Math.min(i + batch.length, selectedKeys.length) +
" of " +
selectedKeys.length +
")",
{ type: "info" },
);
updateEmailMatchCount();
updateEmailSelectionState();
}
if (toast && toast.dismiss) toast.dismiss();
if (failed.length > 0) {
if (window.showToast)
window.showToast(
"Deleted " +
deletedTotal +
" email(s). " +
failed.length +
" failed (still visible).",
{ type: "error" },
);
} else {
if (window.showToast)
window.showToast("Deleted " + deletedTotal + " email(s).", {
type: "success",
});
}
} catch (error) {
if (toast && toast.dismiss) toast.dismiss();
if (window.showToast)
window.showToast(
error instanceof Error && error.message
? error.message
: "Bulk email delete failed.",
{ type: "error" },
);
} finally {
EMAIL_BULK_DELETE_IN_PROGRESS = false;
setEmailButtonLoading(EMAIL_BULK_DELETE_BUTTON_EL, false);
updateEmailSelectionState();
}
}
// Expose functions needed by inline HTML event handlers
(window as unknown as Record<string, unknown>).scheduleEmailFilter =
scheduleEmailFilter;
(window as unknown as Record<string, unknown>).toggleAllEmails =
toggleAllEmails;
(window as unknown as Record<string, unknown>).selectMatchingEmails =
selectMatchingEmails;
(window as unknown as Record<string, unknown>).clearEmailSelection =
clearEmailSelection;
(window as unknown as Record<string, unknown>).onBulkEmailDeleteSubmit =
onBulkEmailDeleteSubmit;
document.addEventListener("DOMContentLoaded", () => {
initEmailUI();
});
+6
View File
@@ -0,0 +1,6 @@
// AUTO-GENERATED by scripts/build-client.mjs — do not edit directly.
// Source: src/scripts/client/emails-page.ts
// Run `npm run build:client` to regenerate.
export const emailsPageScript =
'"use strict";(()=>{var b=window.__APP_CONFIG__?.feedId??"",g=[],f=[],R=null,_=null,A=null,v=null,C=null,k=null,S=!1,T="receivedAt",p="desc",O=new Intl.Collator(void 0,{numeric:!0,sensitivity:"base"});function q(){g=Array.from(document.querySelectorAll(".email-row")),f=Array.from(document.querySelectorAll(".email-select")),R=document.getElementById("selected-email-count"),_=document.getElementById("email-match-count"),A=document.getElementById("email-total-count"),v=document.getElementById("bulk-delete-emails-button"),C=document.getElementById("select-all-emails"),K(),P(),J(),I(),w()}function I(){if(!_)return;let e=g.length,t=g.filter(i=>!i.hidden).length,n=(document.getElementById("email-search")?.value||"").trim();_.textContent=n?"Showing "+t+" of "+e:"Showing "+e}function x(){k&&clearTimeout(k),k=setTimeout(Y,120)}function H(e,t){let n="sort"+t.charAt(0).toUpperCase()+t.slice(1);return e.dataset&&e.dataset[n]?e.dataset[n]:""}function B(e){Array.from(e.querySelectorAll("th[data-sort-key]")).forEach(t=>{let n=t.getAttribute("data-sort-key")||"",i=t.querySelector(".sort-indicator"),u=n===T;i&&(i.textContent=u?p==="asc"?"^":"v":""),t.setAttribute("aria-sort",u?p==="asc"?"ascending":"descending":"none")})}function N(e){let t=document.querySelector("table.table-emails"),n=t?t.querySelector("tbody"):null;if(!t||!n)return;T===e?p=p==="asc"?"desc":"asc":(T=e,p=e==="receivedAt"?"desc":"asc");let i=p==="asc"?1:-1,u=Array.from(n.querySelectorAll(".email-row"));u.sort((l,r)=>i*O.compare(H(l,T),H(r,T)));let s=document.createDocumentFragment();u.forEach(l=>s.appendChild(l)),n.appendChild(s),B(t)}function P(){let e=document.querySelector("table.table-emails");e&&(e.querySelectorAll("button.th-button[data-sort-key]").forEach(t=>{t.addEventListener("click",()=>{let n=t.getAttribute("data-sort-key");n&&N(n)})}),B(e))}function K(){let e=document.querySelector("table.table-emails");if(!e)return;let t="email-to-rss.admin.emailsTable.colWidths",n={subject:240,receivedAt:180,actions:160},i={subject:520,receivedAt:220,actions:200},u=Array.from(e.querySelectorAll("colgroup col")),s={};u.forEach(o=>{let a=o.getAttribute("data-col");a&&(s[a]=o)});try{let o=JSON.parse(localStorage.getItem(t)||"{}");Object.keys(o||{}).forEach(a=>{let c=Number(o[a]);s[a]&&Number.isFinite(c)&&(s[a].style.width=c+"px")})}catch{}let l=()=>{try{let o={};Object.keys(s).forEach(a=>{if(a==="select")return;let c=parseInt(s[a].style.width||"0",10);Number.isFinite(c)&&c>0&&(o[a]=c)}),localStorage.setItem(t,JSON.stringify(o))}catch{}},r=null,E=0,d=0;e.querySelectorAll(".col-resizer").forEach(o=>{o.addEventListener("pointerdown",c=>{c.preventDefault(),c.stopPropagation();let m=o.getAttribute("data-col"),h=m?s[m]:null;if(!m||!h)return;let y=o.closest("th"),D=y?y.getBoundingClientRect().width:parseInt(h.style.width||"0",10)||200;r={key:m,col:h,startX:c.clientX,startWidth:D},document.body.classList.add("is-resizing"),o.setPointerCapture(c.pointerId)}),o.addEventListener("pointermove",c=>{if(!r)return;let m=n[r.key]||180;d=Math.max(m,Math.round(r.startWidth+(c.clientX-r.startX))),!E&&(E=requestAnimationFrame(()=>{r.col.style.width=d+"px",E=0}))});let a=()=>{r&&(r=null,document.body.classList.remove("is-resizing"),l())};o.addEventListener("pointerup",a),o.addEventListener("pointercancel",a),o.addEventListener("dblclick",c=>{c.preventDefault(),c.stopPropagation();let m=o.getAttribute("data-col"),h=m?s[m]:null,y=m?i[m]:null;!m||!h||!y||(h.style.width=y+"px",l())})})}var F="Confirm delete",U=4e3;function M(e){e&&(e.classList.remove("is-confirming"),e.removeAttribute("data-confirming"),e.disabled=!1,e.textContent=e.dataset.originalLabel||"Delete")}function L(e,t,n){if(e){if(t){e.dataset.originalLabel||(e.dataset.originalLabel=(e.textContent||"").trim()),e.classList.add("is-loading"),e.disabled=!0,e.textContent="";let i=document.createElement("span");i.className="spinner",i.setAttribute("aria-hidden","true"),e.appendChild(i),e.appendChild(document.createTextNode(" "+(n||"Working...")));return}e.classList.remove("is-loading"),e.textContent=e.dataset.originalLabel||(e.textContent||"").trim()}}function W(e,t){if(!e){t&&t();return}e.classList.add("is-removing"),window.setTimeout(()=>{e.remove(),t&&t()},240)}async function j(e,t){let n=await fetch("/admin/emails/"+encodeURIComponent(e)+"/delete?feedId="+encodeURIComponent(t),{method:"POST",headers:{Accept:"application/json"},credentials:"same-origin"}),i=await n.json().catch(()=>({}));if(!n.ok)throw new Error(i&&i.error?String(i.error):"Request failed ("+n.status+")");return i}function z(){g=Array.from(document.querySelectorAll(".email-row")),f=Array.from(document.querySelectorAll(".email-select")),A&&(A.textContent=String(g.length)),I(),w()}function J(){Array.from(document.querySelectorAll(\'button[data-delete-kind="email"]\')).forEach(e=>{if(e.dataset.deleteReady==="true")return;e.dataset.deleteReady="true",e.dataset.originalLabel=(e.textContent||"").trim()||"Delete";let t=!1,n=0,i=!1,u=()=>{t=!0,e.classList.add("is-confirLine truncated