PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.8.8
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.8.8
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
desktop-mode / assets / js / posts-window.js

posts-window.js in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.8.8, at assets/js/posts-window.js

17,842 lines 607.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 var desktopModePostsWindow = function(exports) {
2 "use strict";
3 const TEXT_DOMAIN = "desktop-mode";
4 function i18n() {
5 return window.wp?.i18n;
6 }
7 function __(text, domain = TEXT_DOMAIN) {
8 return i18n()?.__(text, domain) ?? text;
9 }
10 function sprintf(format, ...args) {
11 const impl = i18n()?.sprintf;
12 if (impl) {
13 return impl(format, ...args);
14 }
15 let i = 0;
16 return format.replace(/%[sd]/g, () => String(args[i++] ?? ""));
17 }
18 const NONCE_HEADER = "X-WP-Nonce";
19 function injectRestNonce(input, init) {
20 const nonce = readRestNonce();
21 if (!nonce) {
22 return init;
23 }
24 const url = resolveUrl(input);
25 if (!url || !isSameOriginRestUrl(url)) {
26 return init;
27 }
28 const baseHeaders = init?.headers ?? (typeof Request !== "undefined" && input instanceof Request ? input.headers : void 0);
29 const headers = new Headers(baseHeaders ?? {});
30 if (headers.has(NONCE_HEADER)) {
31 return init;
32 }
33 headers.set(NONCE_HEADER, nonce);
34 return { ...init ?? {}, headers };
35 }
36 function readRestNonce() {
37 if (typeof window === "undefined") {
38 return void 0;
39 }
40 const cfg = window.desktopModeConfig;
41 const value = cfg?.restNonce;
42 return typeof value === "string" && value.length > 0 ? value : void 0;
43 }
44 function resolveUrl(input) {
45 try {
46 const base = typeof window !== "undefined" && window.location ? window.location.href : void 0;
47 if (typeof input === "string") {
48 return new URL(input, base);
49 }
50 if (input instanceof URL) {
51 return input;
52 }
53 if (typeof Request !== "undefined" && input instanceof Request) {
54 return new URL(input.url, base);
55 }
56 return null;
57 } catch {
58 return null;
59 }
60 }
61 function isSameOriginRestUrl(url) {
62 if (typeof window === "undefined" || !window.location || url.origin !== window.location.origin) {
63 return false;
64 }
65 if (url.pathname.includes("/wp-json/")) {
66 return true;
67 }
68 if (url.searchParams.has("rest_route")) {
69 return true;
70 }
71 return false;
72 }
73 function trackedFetch(input, init, opts = {}) {
74 const fn = window.wp?.desktop?.fetch;
75 if (typeof fn === "function") {
76 return fn(input, init, opts);
77 }
78 const finalInit = injectRestNonce(input, init);
79 return fetch(input, finalInit);
80 }
81 const gravatarCache = /* @__PURE__ */ new Map();
82 async function resolveAvatarUrl(raw) {
83 if (!raw) {
84 return null;
85 }
86 let parsed;
87 try {
88 parsed = new URL(raw, window.location.href);
89 } catch {
90 return raw;
91 }
92 if (!/gravatar\.com$/i.test(parsed.hostname)) {
93 return raw;
94 }
95 parsed.searchParams.delete("d");
96 parsed.searchParams.delete("s");
97 const cacheKey2 = parsed.toString();
98 const cached = gravatarCache.get(cacheKey2);
99 if (cached !== void 0) {
100 return cached instanceof Promise ? cached : cached;
101 }
102 const probeUrl = new URL(raw, window.location.href);
103 probeUrl.searchParams.set("d", "blank");
104 const probe = new Promise((resolve) => {
105 const img = new Image();
106 img.crossOrigin = "anonymous";
107 img.onload = () => {
108 try {
109 const canvas = document.createElement("canvas");
110 canvas.width = 1;
111 canvas.height = 1;
112 const ctx = canvas.getContext("2d", { willReadFrequently: true });
113 if (!ctx) {
114 resolve(raw);
115 return;
116 }
117 ctx.drawImage(img, 0, 0, 1, 1);
118 const pixel = ctx.getImageData(0, 0, 1, 1).data;
119 resolve(pixel[3] === 0 ? null : raw);
120 } catch {
121 resolve(raw);
122 }
123 };
124 img.onerror = () => resolve(null);
125 img.src = probeUrl.toString();
126 }).then((next) => {
127 gravatarCache.set(cacheKey2, next);
128 return next;
129 });
130 gravatarCache.set(cacheKey2, probe);
131 return probe;
132 }
133 function applyAvatarSrc(avatar, raw) {
134 if (!raw) {
135 return;
136 }
137 void resolveAvatarUrl(raw).then((url) => {
138 if (!avatar.isConnected) {
139 return;
140 }
141 if (url) {
142 avatar.setAttribute("src", url);
143 } else {
144 avatar.removeAttribute("src");
145 }
146 });
147 }
148 const ROOT_ID = "__root__";
149 const PALETTE = [
150 2257329,
151 // wp blue
152 8141549,
153 // violet
154 366185,
155 // emerald
156 14362487,
157 // pink
158 15357964,
159 // orange
160 561586
161 // cyan
162 ];
163 function buildSeedTree() {
164 const seeds = [
165 { id: "science", name: __("Science"), parent: ROOT_ID },
166 { id: "biology", name: __("Biology"), parent: "science" },
167 { id: "astronomy", name: __("Astronomy"), parent: "science" },
168 { id: "physics", name: __("Physics"), parent: "science" },
169 { id: "society", name: __("Society"), parent: ROOT_ID },
170 { id: "economics", name: __("Economics"), parent: "society" },
171 { id: "politics", name: __("Politics"), parent: "society" },
172 { id: "culture", name: __("Culture"), parent: ROOT_ID },
173 { id: "music", name: __("Music"), parent: "culture" },
174 { id: "cinema", name: __("Cinema"), parent: "culture" }
175 ];
176 const map = /* @__PURE__ */ new Map();
177 seeds.forEach((s, i) => {
178 map.set(s.id, {
179 id: s.id,
180 name: s.name,
181 parent: s.parent,
182 color: PALETTE[i % PALETTE.length],
183 radius: s.parent === ROOT_ID ? 34 : 24,
184 x: 0,
185 y: 0,
186 vx: 0,
187 vy: 0,
188 tx: 0,
189 ty: 0,
190 gfx: null,
191 label: null,
192 dragging: false,
193 ...makeFloatPhase(i, 4, 3.5)
194 });
195 });
196 return map;
197 }
198 function makeFloatPhase(seed, ampX, ampY) {
199 const r = (n) => {
200 const x = Math.sin(seed * 9301 + n * 49297) * 233280;
201 return x - Math.floor(x);
202 };
203 return {
204 phaseX: r(1) * Math.PI * 2,
205 phaseY: r(2) * Math.PI * 2,
206 // 0.0006–0.0012 rad/ms ≈ 5–10 second periods.
207 freqX: 6e-4 + r(3) * 6e-4,
208 freqY: 6e-4 + r(4) * 6e-4,
209 ampX,
210 ampY
211 };
212 }
213 const TAG_SEEDS = [
214 { id: "t-wp", name: "wordpress", count: 42, hue: 210 },
215 { id: "t-design", name: "design", count: 28, hue: 280 },
216 { id: "t-code", name: "code", count: 33, hue: 145 },
217 { id: "t-photo", name: "photo", count: 22, hue: 320 },
218 { id: "t-news", name: "news", count: 19, hue: 10 }
219 ];
220 const TAG_FONT_MIN = 11;
221 const TAG_FONT_MAX = 16;
222 const TAG_PAD_X = 9;
223 const TAG_PAD_Y = 4;
224 const TAG_GAP_HASH = 3;
225 const TAG_GAP_COUNT = 6;
226 function fontSizeFor$1(count, max) {
227 if (max <= 0) {
228 return TAG_FONT_MIN;
229 }
230 const t = Math.min(1, count / max);
231 return TAG_FONT_MIN + (TAG_FONT_MAX - TAG_FONT_MIN) * t;
232 }
233 function darkenColor(color, factor) {
234 const r = Math.round(Math.floor(color / 65536) * factor);
235 const g = Math.round(Math.floor(color % 65536 / 256) * factor);
236 const b = Math.round(color % 256 * factor);
237 return r * 65536 + g * 256 + b;
238 }
239 function hslToInt$2(h, s, l) {
240 const sat = s / 100;
241 const lig = l / 100;
242 const c = (1 - Math.abs(2 * lig - 1)) * sat;
243 const hp = (h % 360 + 360) % 360 / 60;
244 const xCol = c * (1 - Math.abs(hp % 2 - 1));
245 let r = 0;
246 let g = 0;
247 let b = 0;
248 if (hp < 1) {
249 r = c;
250 g = xCol;
251 } else if (hp < 2) {
252 r = xCol;
253 g = c;
254 } else if (hp < 3) {
255 g = c;
256 b = xCol;
257 } else if (hp < 4) {
258 g = xCol;
259 b = c;
260 } else if (hp < 5) {
261 r = xCol;
262 b = c;
263 } else {
264 r = c;
265 b = xCol;
266 }
267 const m = lig - c / 2;
268 const R = Math.round((r + m) * 255);
269 const G = Math.round((g + m) * 255);
270 const B = Math.round((b + m) * 255);
271 return R * 65536 + G * 256 + B;
272 }
273 function isDescendant(nodes, candidateId, targetId) {
274 if (candidateId === targetId) {
275 return true;
276 }
277 let cur = candidateId;
278 const visited = /* @__PURE__ */ new Set();
279 while (cur && !visited.has(cur)) {
280 visited.add(cur);
281 const n = nodes.get(cur);
282 if (!n) {
283 return false;
284 }
285 if (n.parent === targetId) {
286 return true;
287 }
288 cur = n.parent;
289 }
290 return false;
291 }
292 function layoutTree(nodes, width, height) {
293 const cx = width / 2;
294 const cy = height * 0.4;
295 const roots = Array.from(nodes.values()).filter((n) => n.parent === ROOT_ID);
296 const mindmapH = height * 0.62;
297 const rootR = Math.min(width, mindmapH) * 0.22;
298 roots.forEach((root, i) => {
299 const angle = i / Math.max(1, roots.length) * Math.PI * 2 - Math.PI / 2;
300 root.tx = cx + Math.cos(angle) * rootR;
301 root.ty = cy + Math.sin(angle) * rootR;
302 layoutChildren(nodes, root, angle);
303 });
304 }
305 function layoutTags(tags, width, height) {
306 const bandTop = height * 0.72;
307 const bandH = height * 0.26;
308 const bandCy = bandTop + bandH / 2;
309 const gap = 8;
310 const rows = [[]];
311 let rowW = 0;
312 tags.forEach((t) => {
313 const w = t.width || 60;
314 if (rowW + w + gap > width - 24 && rows[rows.length - 1].length > 0) {
315 rows.push([]);
316 rowW = 0;
317 }
318 rows[rows.length - 1].push(t);
319 rowW += w + gap;
320 });
321 const rowSpacing = 38;
322 const totalRowsH = rows.length * rowSpacing - rowSpacing;
323 const startY = bandCy - totalRowsH / 2;
324 rows.forEach((row, rIdx) => {
325 const total = row.reduce((acc, t) => acc + (t.width || 60), 0) + gap * Math.max(0, row.length - 1);
326 let cursor = (width - total) / 2;
327 row.forEach((t) => {
328 const w = t.width || 60;
329 t.tx = cursor + w / 2;
330 t.ty = startY + rIdx * rowSpacing;
331 cursor += w + gap;
332 });
333 });
334 }
335 function layoutChildren(nodes, parent, parentAngle) {
336 const children = Array.from(nodes.values()).filter(
337 (n) => n.parent === parent.id
338 );
339 if (children.length === 0) {
340 return;
341 }
342 const spread = Math.PI * 0.9;
343 const baseAngle = parentAngle;
344 const step = children.length === 1 ? 0 : spread / (children.length - 1);
345 const start = baseAngle - spread / 2;
346 const r = 95;
347 children.forEach((child, i) => {
348 const a = children.length === 1 ? baseAngle : start + step * i;
349 child.tx = parent.tx + Math.cos(a) * r;
350 child.ty = parent.ty + Math.sin(a) * r;
351 layoutChildren(nodes, child, a);
352 });
353 }
354 function layoutTagChip(chip) {
355 chip.hashText.style.fontSize = chip.fontSize;
356 chip.nameText.style.fontSize = chip.fontSize;
357 chip.countText.style.fontSize = Math.max(9, Math.round(chip.fontSize * 0.6));
358 const hashW = chip.hashText.width;
359 const nameW = chip.nameText.width;
360 const nameH = chip.nameText.height;
361 const countW = chip.countText.width;
362 const countH = chip.countText.height;
363 const countBadgeW = Math.max(16, countW + 8);
364 const countBadgeH = Math.max(13, countH + 3);
365 chip.width = TAG_PAD_X + hashW + TAG_GAP_HASH + nameW + TAG_GAP_COUNT + countBadgeW + TAG_PAD_X;
366 chip.height = Math.max(nameH, countBadgeH) + TAG_PAD_Y * 2;
367 }
368 function paintTagChip(chip) {
369 const totalW = chip.width;
370 const totalH = chip.height;
371 const left = -totalW / 2;
372 const top = -totalH / 2;
373 const radius = totalH / 2;
374 const fillBg = chip.hover ? hslToInt$2(chip.hue, 70, 88) : hslToInt$2(chip.hue, 60, 95);
375 const borderColor = hslToInt$2(chip.hue, 50, 70);
376 const textColor = 1909543;
377 const hashColor = hslToInt$2(chip.hue, 65, 42);
378 const countBg = hslToInt$2(chip.hue, 70, 50);
379 chip.bg.clear();
380 chip.bg.roundRect(left, top, totalW, totalH, radius);
381 chip.bg.fill(fillBg);
382 chip.bg.stroke({
383 color: borderColor,
384 width: chip.hover ? 1.6 : 1.2,
385 alpha: 0.85
386 });
387 const hashW = chip.hashText.width;
388 const nameW = chip.nameText.width;
389 const nameH = chip.nameText.height;
390 const countW = chip.countText.width;
391 const countH = chip.countText.height;
392 const countBadgeW = Math.max(16, countW + 8);
393 const countBadgeH = Math.max(13, countH + 3);
394 chip.hashText.x = left + TAG_PAD_X;
395 chip.hashText.y = (totalH - nameH) / 2 + top;
396 chip.hashText.style.fill = hashColor;
397 chip.nameText.x = left + TAG_PAD_X + hashW + TAG_GAP_HASH;
398 chip.nameText.y = (totalH - nameH) / 2 + top;
399 chip.nameText.style.fill = textColor;
400 const badgeX = left + TAG_PAD_X + hashW + TAG_GAP_HASH + nameW + TAG_GAP_COUNT;
401 const badgeY = (totalH - countBadgeH) / 2 + top;
402 chip.bg.roundRect(badgeX, badgeY, countBadgeW, countBadgeH, countBadgeH / 2);
403 chip.bg.fill(countBg);
404 chip.countText.x = badgeX + (countBadgeW - countW) / 2;
405 chip.countText.y = badgeY + (countBadgeH - countH) / 2;
406 }
407 function renderFallback(stage) {
408 stage.replaceChildren();
409 const note = document.createElement("p");
410 note.className = "wpd-intro__fallback";
411 note.textContent = __(
412 "A new visual editor for Categories and Tags awaits inside — drag, drop, and reorganize your taxonomy in seconds."
413 );
414 stage.appendChild(note);
415 }
416 async function showPostsIntroDialog() {
417 return new Promise((resolve) => {
418 const backdrop = document.createElement("div");
419 backdrop.className = "wpd-intro-backdrop";
420 const dialog = document.createElement("div");
421 dialog.className = "wpd-intro";
422 dialog.setAttribute("role", "dialog");
423 dialog.setAttribute("aria-modal", "true");
424 dialog.setAttribute("aria-labelledby", "wpd-intro-title");
425 dialog.tabIndex = -1;
426 backdrop.appendChild(dialog);
427 const titleEl = document.createElement("h2");
428 titleEl.id = "wpd-intro-title";
429 titleEl.className = "wpd-intro__title";
430 titleEl.textContent = __("Welcome to the new Posts");
431 dialog.appendChild(titleEl);
432 const lede = document.createElement("p");
433 lede.className = "wpd-intro__lede";
434 lede.textContent = __(
435 "A redesigned Posts experience built around how you actually work. Try the new Categories canvas — grab a node and drop it on another to reparent it."
436 );
437 dialog.appendChild(lede);
438 const stage = document.createElement("div");
439 stage.className = "wpd-intro__stage";
440 dialog.appendChild(stage);
441 const escape = document.createElement("p");
442 escape.className = "wpd-intro__escape";
443 escape.textContent = __(
444 "Prefer the classic Posts list? You can switch back any time from OS Settings → Features."
445 );
446 dialog.appendChild(escape);
447 const actions = document.createElement("div");
448 actions.className = "wpd-intro__actions";
449 const settingsBtn = document.createElement("button");
450 settingsBtn.type = "button";
451 settingsBtn.className = "wpd-intro__btn wpd-intro__btn--secondary";
452 settingsBtn.textContent = __("Take me to settings");
453 const confirmBtn = document.createElement("button");
454 confirmBtn.type = "button";
455 confirmBtn.className = "wpd-intro__btn wpd-intro__btn--primary";
456 confirmBtn.textContent = __("Got it");
457 actions.appendChild(settingsBtn);
458 actions.appendChild(confirmBtn);
459 dialog.appendChild(actions);
460 document.body.appendChild(backdrop);
461 let teardownPixi = null;
462 const cleanup = (result) => {
463 document.removeEventListener("keydown", onKey);
464 teardownPixi?.();
465 backdrop.remove();
466 resolve(result);
467 };
468 const onKey = (e) => {
469 if (e.key === "Escape") {
470 e.preventDefault();
471 cleanup("cancel");
472 }
473 };
474 document.addEventListener("keydown", onKey);
475 confirmBtn.addEventListener("click", () => cleanup("confirm"));
476 settingsBtn.addEventListener("click", () => cleanup("settings"));
477 backdrop.addEventListener("click", (e) => {
478 if (e.target === backdrop) {
479 cleanup("cancel");
480 }
481 });
482 requestAnimationFrame(() => dialog.focus());
483 void mountPixi(stage).then((teardown) => {
484 teardownPixi = teardown;
485 }).catch(() => {
486 renderFallback(stage);
487 });
488 });
489 }
490 async function mountPixi(stage) {
491 const api = window.wp?.desktop;
492 if (!api || typeof api.loadModules !== "function") {
493 renderFallback(stage);
494 return () => {
495 };
496 }
497 try {
498 await api.loadModules(["pixijs"]);
499 } catch {
500 renderFallback(stage);
501 return () => {
502 };
503 }
504 const pixiMaybe = window.PIXI;
505 if (!pixiMaybe) {
506 renderFallback(stage);
507 return () => {
508 };
509 }
510 const pixi = pixiMaybe;
511 const app = new pixi.Application();
512 await app.init({
513 resizeTo: stage,
514 backgroundAlpha: 0,
515 antialias: true,
516 autoDensity: true,
517 resolution: Math.min(window.devicePixelRatio || 1, 2)
518 });
519 stage.appendChild(app.canvas);
520 app.canvas.classList.add("wpd-intro__canvas");
521 const world = new pixi.Container();
522 world.sortableChildren = true;
523 world.scale.set(1);
524 app.stage.addChild(world);
525 const edgeLayer = new pixi.Container();
526 const nodeLayer = new pixi.Container();
527 const tagLayer = new pixi.Container();
528 const postLayer = new pixi.Container();
529 edgeLayer.zIndex = 1;
530 nodeLayer.zIndex = 2;
531 tagLayer.zIndex = 3;
532 postLayer.zIndex = 5;
533 world.addChild(edgeLayer);
534 world.addChild(postLayer);
535 world.addChild(nodeLayer);
536 world.addChild(tagLayer);
537 const nodes = buildSeedTree();
538 nodes.forEach((n) => {
539 const gfx = new pixi.Graphics();
540 gfx.eventMode = "static";
541 gfx.cursor = "grab";
542 const label = new pixi.Text({
543 text: n.name,
544 style: { fill: 16777215, fontSize: 12, fontWeight: "600", fontFamily: "system-ui, -apple-system, sans-serif" },
545 resolution: 3,
546 anchor: { x: 0.5, y: 0.5 }
547 });
548 gfx.addChild(label);
549 n.gfx = gfx;
550 n.label = label;
551 nodeLayer.addChild(gfx);
552 });
553 const tags = [];
554 const maxTagCount = TAG_SEEDS.reduce((m, t) => Math.max(m, t.count), 0);
555 TAG_SEEDS.forEach((seed, i) => {
556 const container = new pixi.Container();
557 container.eventMode = "static";
558 container.cursor = "grab";
559 const bg = new pixi.Graphics();
560 const fontSize = fontSizeFor$1(seed.count, maxTagCount);
561 const hashText = new pixi.Text({
562 text: "#",
563 style: { fill: 1909543, fontSize, fontWeight: "600", fontFamily: "system-ui, -apple-system, sans-serif" },
564 resolution: 3,
565 anchor: { x: 0, y: 0 }
566 });
567 const nameText = new pixi.Text({
568 text: seed.name,
569 style: { fill: 1909543, fontSize, fontWeight: "600", fontFamily: "system-ui, -apple-system, sans-serif" },
570 resolution: 3,
571 anchor: { x: 0, y: 0 }
572 });
573 const countText = new pixi.Text({
574 text: String(seed.count),
575 style: { fill: 16777215, fontSize: Math.max(9, Math.round(fontSize * 0.6)), fontWeight: "700", fontFamily: "system-ui, -apple-system, sans-serif" },
576 resolution: 3,
577 anchor: { x: 0, y: 0 }
578 });
579 container.addChild(bg, hashText, nameText, countText);
580 tagLayer.addChild(container);
581 const chip = {
582 id: seed.id,
583 name: seed.name,
584 count: seed.count,
585 hue: seed.hue,
586 fontSize,
587 width: 0,
588 height: 0,
589 x: 0,
590 y: 0,
591 tx: 0,
592 ty: 0,
593 bg,
594 hashText,
595 nameText,
596 countText,
597 container,
598 dragging: false,
599 hover: false,
600 ...makeFloatPhase(100 + i, 5, 4)
601 };
602 layoutTagChip(chip);
603 paintTagChip(chip);
604 tags.push(chip);
605 });
606 let stageW = stage.clientWidth || 600;
607 let stageH = stage.clientHeight || 360;
608 layoutTree(nodes, stageW, stageH);
609 layoutTags(tags, stageW, stageH);
610 const cx0 = stageW / 2;
611 const cy0 = stageH * 0.4;
612 nodes.forEach((n) => {
613 n.x = cx0;
614 n.y = cy0;
615 });
616 tags.forEach((t) => {
617 t.x = t.tx;
618 t.y = stageH + 40;
619 });
620 const drawNode = (n, hovered, dropTarget) => {
621 n.gfx.clear();
622 const r = n.radius * (hovered ? 1.08 : 1);
623 if (dropTarget) {
624 n.gfx.circle(0, 0, r + 10).fill({ color: n.color, alpha: 0.18 });
625 }
626 n.gfx.circle(0, 0, r).fill({ color: n.color, alpha: 0.95 }).stroke({ color: 16777215, width: dropTarget ? 3 : 1.5, alpha: 0.9 });
627 const labelW = n.label.width;
628 const labelH = n.label.height;
629 if (labelW + 6 > r * 2) {
630 const padX = 8;
631 const padY = 3;
632 const capW = labelW + padX * 2;
633 const capH = labelH + padY * 2;
634 n.gfx.roundRect(-capW / 2, -capH / 2, capW, capH, capH / 2).fill({ color: darkenColor(n.color, 0.55), alpha: 0.92 });
635 }
636 n.gfx.x = n.x;
637 n.gfx.y = n.y;
638 };
639 const drawEdges = () => {
640 const edgeLayerWithChildren = edgeLayer;
641 const previousChildren = edgeLayerWithChildren.children.slice();
642 previousChildren.forEach((c) => edgeLayer.removeChild(c));
643 const edge = new pixi.Graphics();
644 nodes.forEach((n) => {
645 if (!n.parent || n.parent === ROOT_ID) {
646 return;
647 }
648 const parent = nodes.get(n.parent);
649 if (!parent) {
650 return;
651 }
652 const dx = n.x - parent.x;
653 const cp1x = parent.x + dx * 0.5;
654 const cp1y = parent.y;
655 const cp2x = parent.x + dx * 0.5;
656 const cp2y = n.y;
657 edge.moveTo(parent.x, parent.y);
658 edge.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, n.x, n.y);
659 });
660 edge.stroke({ color: 9741240, width: 1.6, alpha: 0.55 });
661 edgeLayer.addChild(edge);
662 };
663 const POSTS_BY_TAG = {
664 "t-wp": [{ node: "politics", title: __("WordPress at scale") }, { node: "economics", title: __("Plugins economy") }, { node: "astronomy", title: __("Open-source orbits") }],
665 "t-design": [{ node: "cinema", title: __("Title cards reborn") }, { node: "music", title: __("Album art trends") }, { node: "culture", title: __("Type as identity") }],
666 "t-code": [{ node: "physics", title: __("Sim notebooks") }, { node: "astronomy", title: __("Pixel pipelines") }, { node: "science", title: __("Code as method") }],
667 "t-photo": [{ node: "cinema", title: __("Anamorphic notes") }, { node: "biology", title: __("Field portraits") }, { node: "culture", title: __("Sunday playlist") }],
668 "t-news": [{ node: "politics", title: __("Weekly briefing") }, { node: "economics", title: __("Markets recap") }]
669 };
670 let fakePosts = [];
671 const POSTS_BY_CATEGORY = {
672 science: [__("What we learned"), __("Open questions"), __("Methodology notes"), __("Replication study")],
673 biology: [__("Fieldwork log"), __("Cell shapes"), __("Microscope diary")],
674 botany: [__("Pressed leaves"), __("Greenhouse notes"), __("Native species")],
675 zoology: [__("Migration map"), __("Birding weekend"), __("Tracks at dawn")],
676 astronomy: [__("Comet schedule"), __("Backyard telescope"), __("Lunar tides")],
677 physics: [__("Lab notebook"), __("Toy models"), __("Phase transitions")],
678 society: [__("Sunday digest"), __("Local elections"), __("Reader letters")],
679 economics: [__("Macro recap"), __("Numbers I noticed"), __("Market mood")],
680 macro: [__("Inflation trail"), __("Central banks")],
681 micro: [__("Pricing tactics"), __("Coffee shop economics")],
682 politics: [__("Campaign trail"), __("Town hall notes"), __("Policy explainer")],
683 culture: [__("Type as identity"), __("Sunday playlist"), __("City walks")],
684 music: [__("Liner notes"), __("Live this week"), __("Album re-listen")],
685 cinema: [__("Title cards reborn"), __("Director cut"), __("Set on the road")],
686 drama: [__("Three-act notes"), __("Stage to screen")],
687 "sci-fi": [__("Anamorphic notes"), __("Future-proof tropes"), __("Worldbuilding 101")]
688 };
689 const clearFakePosts = () => {
690 fakePosts.forEach((p) => {
691 try {
692 postLayer.removeChild(p.container);
693 p.container.destroy({ children: true });
694 } catch {
695 }
696 });
697 fakePosts = [];
698 };
699 const buildPostChip = (title, anchorKind, anchorId, accentColor, angle, orbit, originX, originY, spawnedAt) => {
700 const container = new pixi.Container();
701 container.alpha = 0;
702 container.x = originX;
703 container.y = originY;
704 const bg = new pixi.Graphics();
705 const text = new pixi.Text({
706 text: title,
707 style: {
708 fill: 1909543,
709 fontSize: 10,
710 fontFamily: "system-ui, -apple-system, sans-serif"
711 },
712 resolution: 3,
713 anchor: { x: 0, y: 0 }
714 });
715 container.addChild(bg, text);
716 postLayer.addChild(container);
717 return {
718 title,
719 anchorKind,
720 anchorId,
721 accentColor,
722 angle,
723 orbit,
724 originX,
725 originY,
726 container,
727 bg,
728 text,
729 spawnedAt
730 };
731 };
732 const spawnFakePostsFromTag = (tag) => {
733 clearFakePosts();
734 const list = POSTS_BY_TAG[tag.id];
735 if (!list) {
736 return;
737 }
738 const now = performance.now();
739 const ox = tag.container.x;
740 const oy = tag.container.y;
741 const accent = hslToInt$2(tag.hue, 70, 50);
742 const titles = list.map((p) => p.title);
743 const spread = Math.PI * 1.2;
744 const baseAngle = -Math.PI / 2;
745 const step = titles.length === 1 ? 0 : spread / (titles.length - 1);
746 const start = baseAngle - spread / 2;
747 const orbitR = 56 + Math.min(16, titles.length * 2);
748 titles.forEach((title, i) => {
749 const angle = titles.length === 1 ? baseAngle : start + step * i;
750 fakePosts.push(
751 buildPostChip(
752 title,
753 "tag",
754 tag.id,
755 accent,
756 angle,
757 orbitR + i % 2 * 6,
758 ox,
759 oy,
760 now
761 )
762 );
763 });
764 };
765 const spawnFakePostsFromCategory = (node) => {
766 clearFakePosts();
767 const titles = POSTS_BY_CATEGORY[node.id];
768 if (!titles || titles.length === 0) {
769 return;
770 }
771 const now = performance.now();
772 const ox = node.gfx.x;
773 const oy = node.gfx.y;
774 const spread = Math.PI * 1.6;
775 const start = -Math.PI / 2 - spread / 2;
776 const step = titles.length === 1 ? 0 : spread / (titles.length - 1);
777 titles.forEach((title, i) => {
778 const angle = titles.length === 1 ? -Math.PI / 2 : start + step * i;
779 fakePosts.push(
780 buildPostChip(
781 title,
782 "node",
783 node.id,
784 node.color,
785 angle,
786 78 + i % 3 * 8,
787 ox,
788 oy,
789 now
790 )
791 );
792 });
793 };
794 let dragging = null;
795 let pointerStart = { x: 0, y: 0 };
796 let nodeStart = { x: 0, y: 0 };
797 let hoverDrop = null;
798 let dragTag = null;
799 let tagDragStart = { x: 0, y: 0 };
800 let tagStart = { x: 0, y: 0 };
801 nodes.forEach((n) => {
802 n.gfx.on("pointerdown", (raw) => {
803 const e = raw;
804 dragging = n;
805 n.dragging = true;
806 pointerStart = { x: e.global.x, y: e.global.y };
807 nodeStart = { x: n.x, y: n.y };
808 n.gfx.cursor = "grabbing";
809 n.gfx.zIndex = 1e3;
810 drawNode(n, true, false);
811 });
812 n.gfx.on("pointerover", () => {
813 if (dragging || dragTag) {
814 return;
815 }
816 drawNode(n, true, false);
817 spawnFakePostsFromCategory(n);
818 });
819 n.gfx.on("pointerout", () => {
820 if (dragging !== n) {
821 drawNode(n, false, hoverDrop === n);
822 }
823 clearFakePosts();
824 });
825 });
826 tags.forEach((t) => {
827 t.container.on("pointerdown", (raw) => {
828 const e = raw;
829 dragTag = t;
830 t.dragging = true;
831 tagDragStart = { x: e.global.x, y: e.global.y };
832 tagStart = { x: t.x, y: t.y };
833 t.container.cursor = "grabbing";
834 t.container.zIndex = 5e3;
835 });
836 t.container.on("pointerover", () => {
837 if (dragTag || dragging) {
838 return;
839 }
840 t.hover = true;
841 paintTagChip(t);
842 spawnFakePostsFromTag(t);
843 });
844 t.container.on("pointerout", () => {
845 t.hover = false;
846 paintTagChip(t);
847 clearFakePosts();
848 });
849 });
850 const onMove = (e) => {
851 const rect = app.canvas.getBoundingClientRect();
852 const px = e.clientX - rect.left;
853 const py = e.clientY - rect.top;
854 if (dragTag) {
855 dragTag.x = tagStart.x + (px - tagDragStart.x);
856 dragTag.y = tagStart.y + (py - tagDragStart.y);
857 dragTag.container.x = dragTag.x;
858 dragTag.container.y = dragTag.y;
859 return;
860 }
861 if (!dragging) {
862 return;
863 }
864 const dx = px - pointerStart.x;
865 const dy = py - pointerStart.y;
866 dragging.x = nodeStart.x + dx;
867 dragging.y = nodeStart.y + dy;
868 let hit = null;
869 nodes.forEach((other) => {
870 if (other === dragging) {
871 return;
872 }
873 if (isDescendant(nodes, other.id, dragging.id)) {
874 return;
875 }
876 const ddx = other.x - dragging.x;
877 const ddy = other.y - dragging.y;
878 if (Math.hypot(ddx, ddy) < other.radius + dragging.radius * 0.6) {
879 hit = other;
880 }
881 });
882 if (hit !== hoverDrop) {
883 if (hoverDrop) {
884 drawNode(hoverDrop, false, false);
885 }
886 hoverDrop = hit;
887 if (hoverDrop) {
888 drawNode(hoverDrop, false, true);
889 }
890 }
891 drawNode(dragging, true, false);
892 };
893 const onUp = () => {
894 if (dragTag) {
895 dragTag.container.cursor = "grab";
896 dragTag.container.zIndex = 0;
897 dragTag.dragging = false;
898 dragTag = null;
899 return;
900 }
901 if (!dragging) {
902 return;
903 }
904 const drop = hoverDrop;
905 if (drop && drop.id !== dragging.parent) {
906 dragging.parent = drop.id;
907 layoutTree(nodes, stageW, stageH);
908 }
909 dragging.gfx.cursor = "grab";
910 dragging.gfx.zIndex = 0;
911 dragging.dragging = false;
912 const dragged = dragging;
913 dragging = null;
914 if (hoverDrop) {
915 drawNode(hoverDrop, false, false);
916 hoverDrop = null;
917 }
918 drawNode(dragged, false, false);
919 };
920 app.canvas.addEventListener("pointermove", onMove);
921 window.addEventListener("pointerup", onUp);
922 window.addEventListener("pointercancel", onUp);
923 const tick = () => {
924 const now = performance.now();
925 const REPULSION_K2 = 6500;
926 const SPRING_K2 = 0.05;
927 const SPRING_LEN2 = 110;
928 const ANCHOR_K = 0.012;
929 const DAMPING = 0.82;
930 const MAX_V = 8;
931 const list = Array.from(nodes.values());
932 const fxArr = new Array(list.length).fill(0);
933 const fyArr = new Array(list.length).fill(0);
934 for (let i = 0; i < list.length; i++) {
935 const a = list[i];
936 if (a === dragging) {
937 continue;
938 }
939 for (let j = i + 1; j < list.length; j++) {
940 const b = list[j];
941 if (b === dragging) {
942 continue;
943 }
944 const dx = b.x - a.x;
945 const dy = b.y - a.y;
946 const d2 = dx * dx + dy * dy + 0.01;
947 const d = Math.sqrt(d2);
948 const minD = a.radius + b.radius;
949 if (d > minD * 4) {
950 continue;
951 }
952 const f = REPULSION_K2 / d2;
953 const fx = dx / d * f;
954 const fy = dy / d * f;
955 fxArr[i] -= fx;
956 fyArr[i] -= fy;
957 fxArr[j] += fx;
958 fyArr[j] += fy;
959 }
960 }
961 list.forEach((c, idx) => {
962 if (!c.parent || c.parent === ROOT_ID) {
963 return;
964 }
965 if (c === dragging) {
966 return;
967 }
968 const parent = nodes.get(c.parent);
969 if (!parent || parent === dragging) {
970 return;
971 }
972 const pIdx = list.indexOf(parent);
973 const dx = parent.x - c.x;
974 const dy = parent.y - c.y;
975 const d = Math.max(0.01, Math.sqrt(dx * dx + dy * dy));
976 const diff = d - SPRING_LEN2;
977 const sx = dx / d * diff * SPRING_K2;
978 const sy = dy / d * diff * SPRING_K2;
979 fxArr[idx] += sx;
980 fyArr[idx] += sy;
981 if (pIdx >= 0) {
982 fxArr[pIdx] -= sx;
983 fyArr[pIdx] -= sy;
984 }
985 });
986 list.forEach((n, idx) => {
987 fxArr[idx] += (n.tx - n.x) * ANCHOR_K;
988 fyArr[idx] += (n.ty - n.y) * ANCHOR_K;
989 });
990 list.forEach((n, idx) => {
991 if (n === dragging) {
992 n.vx = 0;
993 n.vy = 0;
994 return;
995 }
996 n.vx = (n.vx + fxArr[idx]) * DAMPING;
997 n.vy = (n.vy + fyArr[idx]) * DAMPING;
998 if (n.vx > MAX_V) {
999 n.vx = MAX_V;
1000 } else if (n.vx < -MAX_V) {
1001 n.vx = -MAX_V;
1002 }
1003 if (n.vy > MAX_V) {
1004 n.vy = MAX_V;
1005 } else if (n.vy < -MAX_V) {
1006 n.vy = -MAX_V;
1007 }
1008 n.x += n.vx;
1009 n.y += n.vy;
1010 });
1011 drawEdges();
1012 nodes.forEach((n) => {
1013 const fx = n === dragging ? n.x : n.x + Math.sin(now * n.freqX + n.phaseX) * n.ampX;
1014 const fy = n === dragging ? n.y : n.y + Math.sin(now * n.freqY + n.phaseY) * n.ampY;
1015 drawNode(n, false, hoverDrop === n);
1016 n.gfx.x = fx;
1017 n.gfx.y = fy;
1018 });
1019 tags.forEach((t) => {
1020 if (t === dragTag) {
1021 return;
1022 }
1023 t.x += (t.tx - t.x) * 0.16;
1024 t.y += (t.ty - t.y) * 0.16;
1025 const fx = t.x + Math.sin(now * t.freqX + t.phaseX) * t.ampX;
1026 const fy = t.y + Math.sin(now * t.freqY + t.phaseY) * t.ampY * 0.6;
1027 t.container.x = fx;
1028 t.container.y = fy;
1029 });
1030 fakePosts.forEach((p, idx) => {
1031 let anchorX = 0;
1032 let anchorY = 0;
1033 if (p.anchorKind === "tag") {
1034 const t2 = tags.find((tg) => tg.id === p.anchorId);
1035 if (!t2) {
1036 return;
1037 }
1038 anchorX = t2.container.x;
1039 anchorY = t2.container.y;
1040 } else {
1041 const node = nodes.get(p.anchorId);
1042 if (!node) {
1043 return;
1044 }
1045 anchorX = node.gfx.x;
1046 anchorY = node.gfx.y;
1047 }
1048 const elapsed = now - p.spawnedAt;
1049 const t = Math.min(1, elapsed / 320);
1050 p.container.alpha = t;
1051 const wobble = Math.sin(now * 15e-4 + idx) * 4;
1052 const tx = anchorX + Math.cos(p.angle) * (p.orbit + wobble);
1053 const ty = anchorY + Math.sin(p.angle) * (p.orbit + wobble);
1054 p.container.x += (tx - p.container.x) * 0.16;
1055 p.container.y += (ty - p.container.y) * 0.16;
1056 const padX = 7;
1057 const padY = 3;
1058 const textW = p.text.width;
1059 const textH = p.text.height;
1060 const w = textW + padX * 2;
1061 const h = textH + padY * 2;
1062 p.text.x = -w / 2 + padX;
1063 p.text.y = -h / 2 + padY;
1064 p.bg.clear();
1065 p.bg.roundRect(-w / 2, -h / 2, w, h, h / 2);
1066 p.bg.fill({ color: 16777215, alpha: 0.95 });
1067 p.bg.stroke({
1068 color: p.accentColor,
1069 width: 1.2,
1070 alpha: 0.85
1071 });
1072 });
1073 const FIT_MARGIN = 24;
1074 const FIT_EASE = 0.08;
1075 let minX = Infinity;
1076 let minY = Infinity;
1077 let maxX = -Infinity;
1078 let maxY = -Infinity;
1079 nodes.forEach((n) => {
1080 const dx = n.gfx.x;
1081 const dy = n.gfx.y;
1082 const r = n.radius + 8;
1083 if (dx - r < minX) {
1084 minX = dx - r;
1085 }
1086 if (dy - r < minY) {
1087 minY = dy - r;
1088 }
1089 if (dx + r > maxX) {
1090 maxX = dx + r;
1091 }
1092 if (dy + r > maxY) {
1093 maxY = dy + r;
1094 }
1095 });
1096 tags.forEach((tg) => {
1097 const dx = tg.container.x;
1098 const dy = tg.container.y;
1099 const w = tg.width / 2 + 4;
1100 const h = tg.height / 2 + 4;
1101 if (dx - w < minX) {
1102 minX = dx - w;
1103 }
1104 if (dy - h < minY) {
1105 minY = dy - h;
1106 }
1107 if (dx + w > maxX) {
1108 maxX = dx + w;
1109 }
1110 if (dy + h > maxY) {
1111 maxY = dy + h;
1112 }
1113 });
1114 const bw = maxX - minX;
1115 const bh = maxY - minY;
1116 if (bw > 0 && bh > 0 && Number.isFinite(bw) && Number.isFinite(bh)) {
1117 const sx = (stageW - FIT_MARGIN * 2) / bw;
1118 const sy = (stageH - FIT_MARGIN * 2) / bh;
1119 const targetScale = Math.max(0.55, Math.min(1, sx, sy));
1120 const cx = (minX + maxX) / 2;
1121 const cy = (minY + maxY) / 2;
1122 const targetX = stageW / 2 - cx * targetScale;
1123 const targetY = stageH / 2 - cy * targetScale;
1124 world.x += (targetX - world.x) * FIT_EASE;
1125 world.y += (targetY - world.y) * FIT_EASE;
1126 const curScale = world.scale.x;
1127 world.scale.set(curScale + (targetScale - curScale) * FIT_EASE);
1128 }
1129 };
1130 app.ticker.add(tick);
1131 const ro = new ResizeObserver(() => {
1132 stageW = stage.clientWidth || stageW;
1133 stageH = stage.clientHeight || stageH;
1134 layoutTree(nodes, stageW, stageH);
1135 layoutTags(tags, stageW, stageH);
1136 });
1137 ro.observe(stage);
1138 return () => {
1139 ro.disconnect();
1140 app.ticker.remove(tick);
1141 app.canvas.removeEventListener("pointermove", onMove);
1142 window.removeEventListener("pointerup", onUp);
1143 window.removeEventListener("pointercancel", onUp);
1144 clearFakePosts();
1145 try {
1146 app.destroy(true, { children: true });
1147 } catch {
1148 }
1149 };
1150 }
1151 function html(strings, ...values) {
1152 return { __wpdHtml: true, strings, values };
1153 }
1154 function isTemplateResult$1(v) {
1155 return !!v && v.__wpdHtml === true;
1156 }
1157 const MARKER_PREFIX = "$$wpd$$";
1158 const MARKER_RE = /\$\$wpd\$\$(\d+)\$\$/g;
1159 function joinWithMarkers(strings) {
1160 let out = strings[0];
1161 for (let i = 1; i < strings.length; i++) {
1162 out += `${MARKER_PREFIX}${i - 1}$$` + strings[i];
1163 }
1164 return out;
1165 }
1166 const compiledCache = /* @__PURE__ */ new WeakMap();
1167 function compile(strings) {
1168 const cached = compiledCache.get(strings);
1169 if (cached) {
1170 return cached;
1171 }
1172 const template = document.createElement("template");
1173 template.innerHTML = joinWithMarkers(strings);
1174 const recipes = [];
1175 const walk = (node, path) => {
1176 if (node.nodeType === Node.ELEMENT_NODE) {
1177 const el = node;
1178 for (const attr of Array.from(el.attributes)) {
1179 const rawName = attr.name;
1180 const rawValue = attr.value;
1181 const prefix = rawName[0];
1182 if (MARKER_RE.test(rawValue)) {
1183 MARKER_RE.lastIndex = 0;
1184 if (prefix === "@") {
1185 const match = MARKER_RE.exec(rawValue);
1186 MARKER_RE.lastIndex = 0;
1187 recipes.push({
1188 path,
1189 kind: "event",
1190 name: rawName.slice(1),
1191 valueIndex: match ? Number(match[1]) : 0
1192 });
1193 el.removeAttribute(rawName);
1194 } else if (prefix === ".") {
1195 const match = MARKER_RE.exec(rawValue);
1196 MARKER_RE.lastIndex = 0;
1197 recipes.push({
1198 path,
1199 kind: "prop",
1200 name: rawName.slice(1),
1201 valueIndex: match ? Number(match[1]) : 0
1202 });
1203 el.removeAttribute(rawName);
1204 } else if (prefix === "?") {
1205 const match = MARKER_RE.exec(rawValue);
1206 MARKER_RE.lastIndex = 0;
1207 recipes.push({
1208 path,
1209 kind: "bool",
1210 name: rawName.slice(1),
1211 valueIndex: match ? Number(match[1]) : 0
1212 });
1213 el.removeAttribute(rawName);
1214 } else {
1215 const fragments = [];
1216 const indices = [];
1217 let lastEnd = 0;
1218 let m;
1219 MARKER_RE.lastIndex = 0;
1220 while ((m = MARKER_RE.exec(rawValue)) !== null) {
1221 fragments.push(rawValue.slice(lastEnd, m.index));
1222 indices.push(Number(m[1]));
1223 lastEnd = m.index + m[0].length;
1224 }
1225 fragments.push(rawValue.slice(lastEnd));
1226 recipes.push({
1227 path,
1228 kind: "attr",
1229 name: rawName,
1230 template: fragments,
1231 valueIndices: indices
1232 });
1233 el.setAttribute(rawName, "");
1234 }
1235 }
1236 }
1237 }
1238 const children = Array.from(node.childNodes);
1239 let shift = 0;
1240 for (let i = 0; i < children.length; i++) {
1241 const child = children[i];
1242 const liveIndex = i + shift;
1243 if (child.nodeType === Node.TEXT_NODE) {
1244 const text = child.textContent || "";
1245 if (!MARKER_RE.test(text)) {
1246 MARKER_RE.lastIndex = 0;
1247 continue;
1248 }
1249 MARKER_RE.lastIndex = 0;
1250 const parent = child.parentNode;
1251 let lastEnd = 0;
1252 let m;
1253 const newNodes = [];
1254 const newRecipes = [];
1255 MARKER_RE.lastIndex = 0;
1256 while ((m = MARKER_RE.exec(text)) !== null) {
1257 if (m.index > lastEnd) {
1258 newNodes.push(document.createTextNode(text.slice(lastEnd, m.index)));
1259 }
1260 const placeholder = document.createTextNode("");
1261 newNodes.push(placeholder);
1262 newRecipes.push({
1263 path: [...path, liveIndex + newNodes.length - 1],
1264 kind: "node",
1265 valueIndex: Number(m[1])
1266 });
1267 lastEnd = m.index + m[0].length;
1268 }
1269 if (lastEnd < text.length) {
1270 newNodes.push(document.createTextNode(text.slice(lastEnd)));
1271 }
1272 for (const nn of newNodes) {
1273 parent.insertBefore(nn, child);
1274 }
1275 parent.removeChild(child);
1276 shift += newNodes.length - 1;
1277 recipes.push(...newRecipes);
1278 } else {
1279 walk(child, [...path, liveIndex]);
1280 }
1281 }
1282 };
1283 walk(template.content, []);
1284 const buildParts = (fragment) => {
1285 const out = [];
1286 for (const r of recipes) {
1287 let node = fragment;
1288 for (const idx of r.path) {
1289 node = node.childNodes[idx];
1290 }
1291 if (r.kind === "node") {
1292 out.push({
1293 kind: "node",
1294 valueIndex: r.valueIndex,
1295 child: {
1296 anchor: node,
1297 state: null
1298 }
1299 });
1300 } else if (r.kind === "attr") {
1301 out.push({
1302 kind: "attr",
1303 element: node,
1304 name: r.name,
1305 template: r.template,
1306 valueIndices: r.valueIndices
1307 });
1308 } else if (r.kind === "event") {
1309 out.push({
1310 kind: "event",
1311 valueIndex: r.valueIndex,
1312 element: node,
1313 name: r.name
1314 });
1315 } else if (r.kind === "prop") {
1316 out.push({
1317 kind: "prop",
1318 valueIndex: r.valueIndex,
1319 element: node,
1320 name: r.name
1321 });
1322 } else if (r.kind === "bool") {
1323 out.push({
1324 kind: "bool",
1325 valueIndex: r.valueIndex,
1326 element: node,
1327 name: r.name
1328 });
1329 }
1330 }
1331 return out;
1332 };
1333 const entry = { template, buildParts };
1334 compiledCache.set(strings, entry);
1335 return entry;
1336 }
1337 const mountState = /* @__PURE__ */ new WeakMap();
1338 function render(result, container) {
1339 const existing = mountState.get(container);
1340 if (existing && existing.strings === result.strings) {
1341 applyValues(existing.parts, result.values);
1342 return;
1343 }
1344 const compiled = compile(result.strings);
1345 const fragment = compiled.template.content.cloneNode(true);
1346 const parts = compiled.buildParts(fragment);
1347 while (container.firstChild) {
1348 container.removeChild(container.firstChild);
1349 }
1350 container.appendChild(fragment);
1351 applyValues(parts, result.values);
1352 mountState.set(container, { strings: result.strings, parts });
1353 }
1354 function applyValues(parts, values) {
1355 for (const part of parts) {
1356 if (part.kind === "node") {
1357 updateChildPart(part.child, values[part.valueIndex]);
1358 } else if (part.kind === "attr") {
1359 let composed = part.template[0];
1360 for (let i = 0; i < part.valueIndices.length; i++) {
1361 composed += formatText(values[part.valueIndices[i]]);
1362 composed += part.template[i + 1];
1363 }
1364 if (composed !== part.last) {
1365 part.last = composed;
1366 if (composed === "") {
1367 part.element.removeAttribute(part.name);
1368 } else {
1369 part.element.setAttribute(part.name, composed);
1370 }
1371 }
1372 } else if (part.kind === "event") {
1373 const next = values[part.valueIndex];
1374 if (next !== part.current) {
1375 if (part.current) {
1376 part.element.removeEventListener(part.name, part.current);
1377 }
1378 if (next) {
1379 part.element.addEventListener(part.name, next);
1380 }
1381 part.current = next;
1382 }
1383 } else if (part.kind === "prop") {
1384 const next = values[part.valueIndex];
1385 if (next !== part.last) {
1386 part.last = next;
1387 part.element[part.name] = next;
1388 }
1389 } else if (part.kind === "bool") {
1390 const next = !!values[part.valueIndex];
1391 if (next !== part.last) {
1392 part.last = next;
1393 if (next) {
1394 part.element.setAttribute(part.name, "");
1395 } else {
1396 part.element.removeAttribute(part.name);
1397 }
1398 }
1399 }
1400 }
1401 }
1402 function updateChildPart(child, value) {
1403 if (value === null || value === void 0 || value === false) {
1404 if (child.state) {
1405 disposeChildState(child.state);
1406 child.state = null;
1407 }
1408 return;
1409 }
1410 if (Array.isArray(value)) {
1411 updateArrayChild(child, value);
1412 return;
1413 }
1414 if (isTemplateResult$1(value)) {
1415 updateTemplateChild(child, value);
1416 return;
1417 }
1418 if (value instanceof Node) {
1419 updateNodeChild(child, value);
1420 return;
1421 }
1422 updateTextChild(child, formatText(value));
1423 }
1424 function updateNodeChild(child, node) {
1425 const old = child.state;
1426 if (old?.shape === "node" && old.node === node) {
1427 return;
1428 }
1429 if (old) {
1430 disposeChildState(old);
1431 }
1432 insertBeforeAnchor(child, [node]);
1433 child.state = { shape: "node", node };
1434 }
1435 function updateTextChild(child, text) {
1436 const old = child.state;
1437 if (old?.shape === "text") {
1438 if (old.text !== text) {
1439 old.node.textContent = text;
1440 old.text = text;
1441 }
1442 return;
1443 }
1444 if (old) {
1445 disposeChildState(old);
1446 }
1447 const node = document.createTextNode(text);
1448 insertBeforeAnchor(child, [node]);
1449 child.state = { shape: "text", node, text };
1450 }
1451 function updateTemplateChild(child, result) {
1452 const old = child.state;
1453 if (old?.shape === "template" && old.strings === result.strings) {
1454 applyValues(old.parts, result.values);
1455 return;
1456 }
1457 if (old) {
1458 disposeChildState(old);
1459 }
1460 const compiled = compile(result.strings);
1461 const fragment = compiled.template.content.cloneNode(true);
1462 const parts = compiled.buildParts(fragment);
1463 const topNodes = Array.from(fragment.childNodes);
1464 insertBeforeAnchor(child, [fragment]);
1465 applyValues(parts, result.values);
1466 child.state = {
1467 shape: "template",
1468 strings: result.strings,
1469 parts,
1470 nodes: topNodes
1471 };
1472 }
1473 function updateArrayChild(child, arr) {
1474 const old = child.state;
1475 if (old?.shape === "array" && old.entries.length === arr.length) {
1476 for (let i = 0; i < arr.length; i++) {
1477 updateChildPart(old.entries[i], arr[i]);
1478 }
1479 return;
1480 }
1481 if (old) {
1482 disposeChildState(old);
1483 }
1484 const entries = [];
1485 for (const v of arr) {
1486 const entryAnchor = document.createTextNode("");
1487 insertBeforeAnchor(child, [entryAnchor]);
1488 const entry = { anchor: entryAnchor, state: null };
1489 updateChildPart(entry, v);
1490 entries.push(entry);
1491 }
1492 child.state = { shape: "array", entries };
1493 }
1494 function insertBeforeAnchor(child, nodes) {
1495 const parent = child.anchor.parentNode;
1496 if (!parent) {
1497 return;
1498 }
1499 for (const node of nodes) {
1500 parent.insertBefore(node, child.anchor);
1501 }
1502 }
1503 function disposeChildState(state) {
1504 if (state.shape === "text") {
1505 state.node.remove();
1506 return;
1507 }
1508 if (state.shape === "template") {
1509 for (const node of state.nodes) {
1510 if (node.parentNode) {
1511 node.parentNode.removeChild(node);
1512 }
1513 }
1514 return;
1515 }
1516 if (state.shape === "node") {
1517 if (state.node.parentNode) {
1518 state.node.parentNode.removeChild(state.node);
1519 }
1520 return;
1521 }
1522 for (const entry of state.entries) {
1523 if (entry.state) {
1524 disposeChildState(entry.state);
1525 }
1526 entry.anchor.remove();
1527 }
1528 }
1529 function formatText(v) {
1530 if (v === null || v === void 0 || v === false) {
1531 return "";
1532 }
1533 return String(v);
1534 }
1535 const _Component = class _Component extends HTMLElement {
1536 constructor() {
1537 super();
1538 this._renderScheduled = false;
1539 this._propValues = {};
1540 const ctor = this.constructor;
1541 if (ctor.shadow) {
1542 this.attachShadow({ mode: "open" });
1543 this._renderRoot = this.shadowRoot;
1544 } else {
1545 this._renderRoot = this;
1546 }
1547 this._installPropAccessors();
1548 }
1549 static get observedAttributes() {
1550 return this.props.map(kebab);
1551 }
1552 connectedCallback() {
1553 this._adoptStyles();
1554 this.requestUpdate();
1555 }
1556 attributeChangedCallback(name, oldValue, newValue) {
1557 if (oldValue === newValue) {
1558 return;
1559 }
1560 const prop = camel(name);
1561 this._propValues[prop] = newValue;
1562 this.requestUpdate();
1563 }
1564 /**
1565 * Declarative class-name setter. Assign an array (or a
1566 * space-separated string) and the host's `class` attribute is
1567 * rewritten to match. Intended for programmatic styling — when
1568 * a plugin has enqueued its own stylesheet and wants to apply
1569 * one of those classes to a shell component:
1570 *
1571 * ```js
1572 * element.classNames = [ 'my-plugin-brand', 'is-active' ];
1573 * // → <wpd-select class="my-plugin-brand is-active">
1574 * ```
1575 *
1576 * The plain HTML `class="…"` attribute works just the same and
1577 * is always preferred when writing markup by hand — this setter
1578 * exists for the JS-API case where the caller has an array of
1579 * conditional classes in hand.
1580 *
1581 * Getter returns the current `classList` as a plain array for
1582 * symmetric read/write.
1583 *
1584 * @since 0.13.0
1585 */
1586 get classNames() {
1587 return Array.from(this.classList);
1588 }
1589 set classNames(next) {
1590 if (next === null || next === void 0) {
1591 this.removeAttribute("class");
1592 return;
1593 }
1594 const list = Array.isArray(next) ? next : String(next).split(/\s+/);
1595 const cleaned = list.map((s) => String(s).trim()).filter((s) => s !== "");
1596 this.className = cleaned.join(" ");
1597 }
1598 /**
1599 * Request a re-render explicitly. Components rarely need this —
1600 * declare state via props + attribute observers and the render
1601 * loop picks up changes automatically.
1602 */
1603 requestUpdate() {
1604 this._scheduleRender();
1605 }
1606 /**
1607 * Dispatch a `CustomEvent` with a `detail`. Bubbles + composed
1608 * by default (matches typical WC UX — events cross shadow
1609 * boundaries, parents can listen without knowing about internal
1610 * structure).
1611 */
1612 emit(name, detail) {
1613 return this.dispatchEvent(
1614 new CustomEvent(name, {
1615 detail,
1616 bubbles: true,
1617 composed: true
1618 })
1619 );
1620 }
1621 // ------------------------------------------------------------------
1622 // Internals
1623 // ------------------------------------------------------------------
1624 /**
1625 * Wire every `static props` entry to a matched property getter +
1626 * setter on the element. Setting the property reflects into the
1627 * attribute (so downstream observers + CSS selectors see it);
1628 * reading the property falls back to the attribute.
1629 */
1630 _installPropAccessors() {
1631 const ctor = this.constructor;
1632 for (const prop of ctor.props) {
1633 if (Object.getOwnPropertyDescriptor(this, prop)) {
1634 continue;
1635 }
1636 const attr = kebab(prop);
1637 Object.defineProperty(this, prop, {
1638 get: () => {
1639 if (prop in this._propValues) {
1640 return this._propValues[prop];
1641 }
1642 return this.getAttribute(attr);
1643 },
1644 set: (value) => {
1645 let str;
1646 if (value === null || value === void 0 || value === false) {
1647 str = null;
1648 } else if (value === true) {
1649 str = "";
1650 } else {
1651 str = String(value);
1652 }
1653 this._propValues[prop] = str;
1654 if (str === null) {
1655 this.removeAttribute(attr);
1656 } else {
1657 this.setAttribute(attr, str);
1658 }
1659 this.requestUpdate();
1660 },
1661 enumerable: true,
1662 configurable: true
1663 });
1664 }
1665 }
1666 /**
1667 * Schedule a render on the next microtask. Multiple property
1668 * assignments in the same tick collapse into a single render.
1669 */
1670 _scheduleRender() {
1671 if (this._renderScheduled || !this.isConnected) {
1672 return;
1673 }
1674 this._renderScheduled = true;
1675 queueMicrotask(() => {
1676 this._renderScheduled = false;
1677 if (!this.isConnected) {
1678 return;
1679 }
1680 render(this.render(), this._renderRoot);
1681 });
1682 }
1683 /**
1684 * Mount adoptable stylesheets onto the shadow root (via
1685 * `adoptedStyleSheets`) or the light DOM (via one `<style>`
1686 * tag per def). No-op if `static styles` is empty.
1687 */
1688 _adoptStyles() {
1689 const ctor = this.constructor;
1690 if (ctor.styles.length === 0) {
1691 return;
1692 }
1693 if (ctor.shadow && this.shadowRoot) {
1694 const sheets = ctor.styles.map((s) => s.sheet).filter((s) => s !== null);
1695 this.shadowRoot.adoptedStyleSheets = sheets;
1696 if (sheets.length !== ctor.styles.length) {
1697 for (const s of ctor.styles) {
1698 if (!s.sheet) {
1699 const tag = document.createElement("style");
1700 tag.textContent = s.cssText;
1701 this.shadowRoot.appendChild(tag);
1702 }
1703 }
1704 }
1705 } else {
1706 this._adoptLightStyles(ctor);
1707 }
1708 }
1709 _adoptLightStyles(ctor) {
1710 if (_Component._lightStylesAdopted.has(ctor)) {
1711 return;
1712 }
1713 _Component._lightStylesAdopted.add(ctor);
1714 for (const s of ctor.styles) {
1715 const tag = document.createElement("style");
1716 tag.dataset.wpdUi = this.tagName.toLowerCase();
1717 tag.textContent = s.cssText;
1718 document.head.appendChild(tag);
1719 }
1720 }
1721 };
1722 _Component.props = [];
1723 _Component.styles = [];
1724 _Component.shadow = true;
1725 _Component._lightStylesAdopted = /* @__PURE__ */ new WeakSet();
1726 let Component = _Component;
1727 function defineComponent(tag, ctor) {
1728 if (customElements.get(tag)) {
1729 return;
1730 }
1731 customElements.define(tag, ctor);
1732 }
1733 function kebab(s) {
1734 return s.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase());
1735 }
1736 function camel(s) {
1737 return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1738 }
1739 const SUPPORTS_CONSTRUCTABLE_SHEETS = (() => {
1740 try {
1741 const s = new CSSStyleSheet();
1742 return typeof s.replaceSync === "function";
1743 } catch {
1744 return false;
1745 }
1746 })();
1747 function css(strings, ...values) {
1748 let text = strings[0];
1749 for (let i = 1; i < strings.length; i++) {
1750 const v = values[i - 1];
1751 if (typeof v === "string" || typeof v === "number") {
1752 text += String(v);
1753 } else if (v && v.__wpdCss) {
1754 text += v.cssText;
1755 } else {
1756 throw new TypeError(
1757 "[wpd-ui] css`` interpolations must be strings, numbers, or other css`` results. Got: " + typeof v
1758 );
1759 }
1760 text += strings[i];
1761 }
1762 if (SUPPORTS_CONSTRUCTABLE_SHEETS) {
1763 const sheet = new CSSStyleSheet();
1764 sheet.replaceSync(text);
1765 return { __wpdCss: true, sheet, cssText: text };
1766 }
1767 return { __wpdCss: true, sheet: null, cssText: text };
1768 }
1769 function computeAutoId(element) {
1770 const parts = [];
1771 const tabs = [];
1772 let windowId = null;
1773 let node = element.parentElement;
1774 while (node) {
1775 if (node === document.body || node === document.documentElement) {
1776 break;
1777 }
1778 const id = node.id || "";
1779 if (id.startsWith("wp-window-")) {
1780 windowId = id.slice("wp-window-".length);
1781 break;
1782 }
1783 if (node.tagName.toLowerCase() === "wpd-tabpanel") {
1784 const forValue = node.getAttribute("for");
1785 if (forValue) {
1786 tabs.unshift(forValue);
1787 }
1788 }
1789 node = node.parentElement;
1790 }
1791 if (windowId) {
1792 parts.push(slugify(windowId));
1793 }
1794 for (const tab of tabs) {
1795 parts.push("tab-" + slugify(tab));
1796 }
1797 const label = element.getAttribute("label");
1798 if (label) {
1799 parts.push(slugify(label));
1800 }
1801 if (parts.length === 0) {
1802 return "wpd-unnamed";
1803 }
1804 return "wpd-" + parts.filter((p) => p !== "").join("-");
1805 }
1806 function slugify(s) {
1807 return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1808 }
1809 function ensureAutoId(element) {
1810 if (element.id) {
1811 return element.id;
1812 }
1813 const id = computeAutoId(element);
1814 element.id = id;
1815 return id;
1816 }
1817 const styles$8 = css`:host{display:block;--wpd-table-bg:var( --wpd-surface,#fff );--wpd-table-border:var( --wpd-border,rgba( 0,0,0,0.08 ) );--wpd-table-column-border:var( --wpd-border-strong,rgba( 0,0,0,0.14 ) );--wpd-table-header-bg:var( --wpd-surface-elevated,#f6f7f7 );--wpd-table-row-hover:rgba( 0,0,0,0.04 );--wpd-table-stripe:rgba( 0,0,0,0.03 );--wpd-table-cell-padding:8px 12px;--wpd-table-font-size:13px;--wpd-table-max-height:none;font-size:var( --wpd-table-font-size );color:inherit}:host( [ hidden ] ){display:none}.scroll{position:relative;overflow:auto;max-height:var( --wpd-table-max-height );border:1px solid var( --wpd-table-border );border-radius:4px;background:var( --wpd-table-bg )}table{width:100%;border-collapse:separate;border-spacing:0;background:var( --wpd-table-bg )}thead th{text-align:start;font-weight:600;background-color:var( --wpd-table-header-bg );padding:var( --wpd-table-cell-padding );border-bottom:1px solid var( --wpd-table-border );white-space:nowrap}tbody td{padding:var( --wpd-table-cell-padding );border-bottom:1px solid var( --wpd-table-border );background-color:var( --wpd-table-bg );vertical-align:middle}tbody tr:last-child td{border-bottom:0}:host( [ striped ] ) tbody tr:nth-child( odd ) td{background-image:linear-gradient( var( --wpd-table-stripe ),var( --wpd-table-stripe ) )}:host( [ hover ] ) tbody tr:hover td{background-image:linear-gradient( var( --wpd-table-row-hover ),var( --wpd-table-row-hover ) )}:host( [ hover ] [ striped ] ) tbody tr:nth-child( odd ):hover td{background-image:linear-gradient( var( --wpd-table-row-hover ),var( --wpd-table-row-hover ) ),linear-gradient( var( --wpd-table-stripe ),var( --wpd-table-stripe ) )}:host( [ compact ] ){--wpd-table-cell-padding:4px 8px;--wpd-table-font-size:12px}:host( [ bordered ] ) thead th,:host( [ bordered ] ) tbody td{border-inline-end:1px solid var( --wpd-table-column-border )}:host( [ bordered ] ) thead th:last-child,:host( [ bordered ] ) tbody td:last-child{border-inline-end:0}th.is-sticky,td.is-sticky{position:sticky;z-index:10}tbody td.is-sticky{background-color:var( --wpd-table-bg )}thead th.is-sticky{background-color:var( --wpd-table-header-bg );z-index:30}:host( [ sticky-header ] ) thead th{position:sticky;top:0;z-index:20}:host( [ sticky-header ] ) thead tr.filter-row th{top:var( --wpd-table-header-height,33px );z-index:20}:host( [ sticky-header ] ) thead th.is-sticky{z-index:40}:host( [ sticky-header ] ) thead tr.filter-row th.is-sticky{z-index:40}th.is-sticky-edge,td.is-sticky-edge{border-inline-end:var( --wpd-table-sticky-edge,2px solid var( --wpd-table-border ) )}.align-center{text-align:center}.align-end{text-align:end}.filter-row th{padding:4px 8px;background-color:var( --wpd-table-header-bg );border-bottom:1px solid var( --wpd-table-border );font-weight:400}.filter-input,.filter-select{width:100%;min-width:60px;box-sizing:border-box;padding:4px 6px;font:inherit;color:inherit;background-color:var( --wpd-table-bg );border:1px solid var( --wpd-table-border );border-radius:3px}.filter-input:focus,.filter-select:focus{outline:2px solid var( --wp-admin-theme-color,#2271b1 );outline-offset:-1px}.expander{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border:0;background:transparent;color:inherit;cursor:pointer;border-radius:3px;font-size:11px;line-height:1}.expander:hover{background:rgba( 0,0,0,0.06 )}td.col-expander,th.col-expander{width:36px;min-width:36px;padding-left:0;padding-right:0;text-align:center}tr.subtable td{padding:0;background-color:var( --wpd-table-bg );background-image:linear-gradient( var( --wpd-table-stripe ),var( --wpd-table-stripe ) );border-bottom:1px solid var( --wpd-table-border )}tr.subtable .subtable-inner{padding:8px 12px 8px 32px}tr.empty td{padding:24px;text-align:center;color:var( --wpd-text-muted,rgba( 0,0,0,0.55 ) );font-style:italic}thead th.is-sortable{cursor:pointer;user-select:none}thead th.is-sortable:hover{background-image:linear-gradient( var( --wpd-table-row-hover ),var( --wpd-table-row-hover ) )}thead th.is-sortable:focus-visible{outline:2px solid var( --wp-admin-theme-color,#2271b1 );outline-offset:-2px}.sort-indicator{font-size:10px;color:var( --wpd-text-muted,rgba( 0,0,0,0.55 ) );margin-inline-start:2px}thead th.sort-asc .sort-indicator,thead th.sort-desc .sort-indicator{color:var( --wp-admin-theme-color,#2271b1 )}td.col-select,th.col-select{width:40px;min-width:40px;padding-left:0;padding-right:0;text-align:center}.select-all-checkbox,.select-row-checkbox{cursor:pointer;margin:0}tbody tr.is-selected td{background-color:color-mix( in srgb,var( --wp-admin-theme-color,#2271b1 ) 10%,var( --wpd-table-bg ) );background-image:none}tbody tr.is-selected:hover td{background-color:color-mix( in srgb,var( --wp-admin-theme-color,#2271b1 ) 16%,var( --wpd-table-bg ) )}tbody tr.skeleton td{padding:var( --wpd-table-cell-padding )}.skeleton-bar{display:block;height:12px;border-radius:3px;background:linear-gradient( 90deg,var( --wpd-table-skeleton-color,rgba( 0,0,0,0.06 ) ) 0%,var( --wpd-table-skeleton-highlight,rgba( 0,0,0,0.14 ) ) 50%,var( --wpd-table-skeleton-color,rgba( 0,0,0,0.06 ) ) 100% );background-size:200% 100%;animation:wpd-table-skeleton-pulse 1.4s ease-in-out infinite}@keyframes wpd-table-skeleton-pulse{0%{background-position:200% 50%}100%{background-position:-200% 50%}}@media ( prefers-reduced-motion:reduce ){.skeleton-bar{animation:none}}`;
1818 const EXPANDER_KEY = "__wpd_expander__";
1819 const SELECT_KEY = "__wpd_select__";
1820 const _WpdTable = class _WpdTable extends Component {
1821 constructor() {
1822 super(...arguments);
1823 this._data = [];
1824 this._columns = [];
1825 this._filters = {};
1826 this._expanded = /* @__PURE__ */ new Set();
1827 this._subTable = null;
1828 this._sort = null;
1829 this._selection = /* @__PURE__ */ new Set();
1830 this._getRowId = (_row, index) => index;
1831 this._filterCache = /* @__PURE__ */ new Map();
1832 this._paintScheduled = false;
1833 this._stickyHeaderWarned = false;
1834 this._stickyRaceWarned = false;
1835 this._resizeObserver = null;
1836 this._stickyMicroScheduled = false;
1837 this._stickyRafHandle = null;
1838 this._loadingDesyncWarned = false;
1839 this._lastStickyIndex = -1;
1840 }
1841 // ------------------------------------------------------------------
1842 // Public properties — set from JS (use `.data=${...}` in templates).
1843 // ------------------------------------------------------------------
1844 /** The row buffer. Reassigning replaces (and clears expansion state). */
1845 get data() {
1846 return this._data;
1847 }
1848 set data(next) {
1849 this._data = Array.isArray(next) ? next.slice() : [];
1850 this._expanded.clear();
1851 this._schedulePaint();
1852 }
1853 /** Column descriptors. See {@link WpdTableColumn}. */
1854 get columns() {
1855 return this._columns;
1856 }
1857 set columns(next) {
1858 this._columns = Array.isArray(next) ? next.slice() : [];
1859 const keys = new Set(this._columns.map((c) => c.key));
1860 for (const k of Object.keys(this._filters)) {
1861 if (!keys.has(k)) {
1862 delete this._filters[k];
1863 }
1864 }
1865 for (const k of Array.from(this._filterCache.keys())) {
1866 if (!keys.has(k)) {
1867 this._filterCache.delete(k);
1868 }
1869 }
1870 if (this._sort && !keys.has(this._sort.key)) {
1871 this._sort = null;
1872 }
1873 this._schedulePaint();
1874 }
1875 /** Read or replace the current filter map. */
1876 get filters() {
1877 return { ...this._filters };
1878 }
1879 set filters(next) {
1880 this._filters = next ? { ...next } : {};
1881 this._schedulePaint();
1882 }
1883 /** Read or set the active sort. `null` clears it. */
1884 get sort() {
1885 return this._sort ? { ...this._sort } : null;
1886 }
1887 set sort(next) {
1888 this._sort = next ? { ...next } : null;
1889 this._schedulePaint();
1890 }
1891 /** Read or replace the selection (set of row ids). */
1892 get selection() {
1893 return new Set(this._selection);
1894 }
1895 set selection(next) {
1896 this._selection = new Set(next ?? []);
1897 this._schedulePaint();
1898 }
1899 /** The currently-selected rows (resolved from `selection` + `data`). */
1900 get selectedRows() {
1901 const out = [];
1902 this._data.forEach((row, i) => {
1903 if (this._selection.has(this._getRowId(row, i))) {
1904 out.push(row);
1905 }
1906 });
1907 return out;
1908 }
1909 /** Stable row-id extractor. Default is row index. */
1910 get getRowId() {
1911 return this._getRowId;
1912 }
1913 set getRowId(fn) {
1914 this._getRowId = typeof fn === "function" ? fn : (_r, i) => i;
1915 this._schedulePaint();
1916 }
1917 /**
1918 * Sub-table accessor. Return `null` (or omit) for rows with no
1919 * children. Return `{ columns, data }` to render a nested
1920 * `<wpd-table>` inline; or return any `Node` / `html\`\`` template
1921 * for fully custom expanded content.
1922 */
1923 get subTable() {
1924 return this._subTable;
1925 }
1926 set subTable(fn) {
1927 this._subTable = typeof fn === "function" ? fn : null;
1928 this._expanded.clear();
1929 this._schedulePaint();
1930 }
1931 /** Read or replace the expansion set (row indices that are open). */
1932 get expanded() {
1933 return new Set(this._expanded);
1934 }
1935 set expanded(next) {
1936 this._expanded = new Set(next ?? []);
1937 this._schedulePaint();
1938 }
1939 // ------------------------------------------------------------------
1940 // Programmatic methods
1941 // ------------------------------------------------------------------
1942 /** Open a row's sub-table by index. No-op if the index is out of range. */
1943 expand(index) {
1944 if (index < 0 || index >= this._data.length) {
1945 return;
1946 }
1947 if (this._expanded.has(index)) {
1948 return;
1949 }
1950 this._expanded.add(index);
1951 this.emit("wpd-table-expand-change", {
1952 row: this._data[index],
1953 index,
1954 expanded: true
1955 });
1956 this._schedulePaint();
1957 }
1958 /** Close a row's sub-table by index. No-op if it wasn't open. */
1959 collapse(index) {
1960 if (!this._expanded.has(index)) {
1961 return;
1962 }
1963 this._expanded.delete(index);
1964 this.emit("wpd-table-expand-change", {
1965 row: this._data[index],
1966 index,
1967 expanded: false
1968 });
1969 this._schedulePaint();
1970 }
1971 /** Open every row that has children. */
1972 expandAll() {
1973 if (!this._subTable) {
1974 return;
1975 }
1976 let changed = false;
1977 for (let i = 0; i < this._data.length; i++) {
1978 if (!this._subTable(this._data[i], i)) {
1979 continue;
1980 }
1981 if (!this._expanded.has(i)) {
1982 this._expanded.add(i);
1983 changed = true;
1984 }
1985 }
1986 if (changed) {
1987 this._schedulePaint();
1988 }
1989 }
1990 /** Close every open row. */
1991 collapseAll() {
1992 if (this._expanded.size === 0) {
1993 return;
1994 }
1995 this._expanded.clear();
1996 this._schedulePaint();
1997 }
1998 isExpanded(index) {
1999 return this._expanded.has(index);
2000 }
2001 /** Drop every active filter and emit `wpd-table-filter-change`. */
2002 clearFilters() {
2003 if (Object.keys(this._filters).length === 0) {
2004 return;
2005 }
2006 this._filters = {};
2007 this.emit("wpd-table-filter-change", { filters: {} });
2008 this._schedulePaint();
2009 }
2010 /** Drop the active sort and emit `wpd-table-sort-change`. */
2011 clearSort() {
2012 if (this._sort === null) {
2013 return;
2014 }
2015 this._sort = null;
2016 this.emit("wpd-table-sort-change", { sort: null });
2017 this._schedulePaint();
2018 }
2019 /**
2020 * Add a row id to the selection. Emits `wpd-table-selection-change`.
2021 *
2022 * Selection mutators (`select` / `deselect` / `selectAll` /
2023 * `clearSelection`) update the affected row in place via
2024 * {@link _syncSelectionDom} rather than re-rendering the whole
2025 * tbody — a rebuild would tear down the focused checkbox and
2026 * (because scroll-anchoring abandons a momentarily empty container)
2027 * could snap scroll back to the top.
2028 */
2029 select(id) {
2030 if (this._selection.has(id)) {
2031 return;
2032 }
2033 const mode = this._readSelectable();
2034 const previouslySelected = mode === "single" ? Array.from(this._selection) : [];
2035 if (mode === "single") {
2036 this._selection.clear();
2037 }
2038 this._selection.add(id);
2039 this._emitSelectionChange();
2040 this._syncSelectionDom([id, ...previouslySelected]);
2041 }
2042 /** Remove a row id from the selection. */
2043 deselect(id) {
2044 if (!this._selection.delete(id)) {
2045 return;
2046 }
2047 this._emitSelectionChange();
2048 this._syncSelectionDom([id]);
2049 }
2050 /** Select every row currently in `data` (multi-mode only). */
2051 selectAll() {
2052 if (this._readSelectable() !== "multi") {
2053 return;
2054 }
2055 this._data.forEach(
2056 (row, i) => this._selection.add(this._getRowId(row, i))
2057 );
2058 this._emitSelectionChange();
2059 this._syncSelectionDom("all");
2060 }
2061 /** Empty the selection. */
2062 clearSelection() {
2063 if (this._selection.size === 0) {
2064 return;
2065 }
2066 this._selection.clear();
2067 this._emitSelectionChange();
2068 this._syncSelectionDom("all");
2069 }
2070 /**
2071 * Apply a selection change to the existing tbody DOM without
2072 * rebuilding it. Updates each affected row's `is-selected` class
2073 * and `select-row-checkbox` `checked` state, then re-syncs the
2074 * header select-all checkbox (checked / indeterminate / empty).
2075 *
2076 * @param ids `'all'` to walk every row, or an iterable of row ids
2077 * whose rows need updating. Unknown ids are silently
2078 * skipped (row may not be in the current filter/page).
2079 */
2080 _syncSelectionDom(ids) {
2081 const root = this.shadowRoot;
2082 if (!root) {
2083 return;
2084 }
2085 const tbody = root.querySelector("tbody");
2086 if (!tbody) {
2087 return;
2088 }
2089 let needle = null;
2090 if (ids !== "all") {
2091 needle = /* @__PURE__ */ new Set();
2092 for (const id of ids) {
2093 needle.add(String(id));
2094 }
2095 }
2096 const rows = tbody.querySelectorAll(
2097 "tr[data-row-id]"
2098 );
2099 for (const tr of rows) {
2100 const rowIdStr = tr.dataset.rowId;
2101 if (rowIdStr === void 0) {
2102 continue;
2103 }
2104 if (needle && !needle.has(rowIdStr)) {
2105 continue;
2106 }
2107 const idx = Number(tr.dataset.rowIndex);
2108 if (!Number.isFinite(idx)) {
2109 continue;
2110 }
2111 const row = this._data[idx];
2112 if (row === void 0) {
2113 continue;
2114 }
2115 const id = this._getRowId(row, idx);
2116 const isSelected = this._selection.has(id);
2117 tr.classList.toggle("is-selected", isSelected);
2118 const cb = tr.querySelector(
2119 "input.select-row-checkbox"
2120 );
2121 if (cb && cb.checked !== isSelected) {
2122 cb.checked = isSelected;
2123 }
2124 }
2125 const headerCb = root.querySelector(
2126 "thead .select-all-checkbox"
2127 );
2128 if (headerCb) {
2129 const total = this._data.length;
2130 const selectedCount = this._countSelectedInData();
2131 headerCb.checked = total > 0 && selectedCount === total;
2132 headerCb.indeterminate = selectedCount > 0 && selectedCount < total;
2133 }
2134 }
2135 /** Scroll the (filtered) row at `index` into view inside the table's scroll container. */
2136 scrollToRow(index) {
2137 const root = this.shadowRoot;
2138 if (!root) {
2139 return;
2140 }
2141 const rows = root.querySelectorAll(
2142 "tbody tr:not(.subtable):not(.empty):not(.skeleton)"
2143 );
2144 const row = rows[index];
2145 if (row) {
2146 row.scrollIntoView({ block: "nearest", inline: "nearest" });
2147 }
2148 }
2149 connectedCallback() {
2150 super.connectedCallback();
2151 this._schedulePaint();
2152 }
2153 disconnectedCallback() {
2154 this._resizeObserver?.disconnect();
2155 this._resizeObserver = null;
2156 if (this._stickyRafHandle !== null && typeof cancelAnimationFrame !== "undefined") {
2157 cancelAnimationFrame(this._stickyRafHandle);
2158 this._stickyRafHandle = null;
2159 }
2160 }
2161 /**
2162 * Force a sticky-offsets recompute. Public escape hatch for the
2163 * rare case where layout settles after every internal hook has
2164 * fired — e.g. an out-of-band font swap or a JS-driven width
2165 * change on an ancestor that doesn't bubble through ResizeObserver.
2166 *
2167 * Usually you don't need this: the component schedules recomputes
2168 * on a microtask + animation frame after every paint, and a
2169 * ResizeObserver on the inner scroll element catches geometry
2170 * changes thereafter. Reach for `recomputeLayout()` only if you've
2171 * confirmed that all of those pathways missed your case.
2172 */
2173 recomputeLayout() {
2174 this._applyStickyOffsets();
2175 this._measureHeaderHeight();
2176 }
2177 // ------------------------------------------------------------------
2178 // Skeleton + paint pipeline
2179 // ------------------------------------------------------------------
2180 render() {
2181 return html`
2182 <div class="scroll" part="scroll">
2183 <table part="table">
2184 <colgroup></colgroup>
2185 <thead></thead>
2186 <tbody></tbody>
2187 </table>
2188 </div>
2189 `;
2190 }
2191 requestUpdate() {
2192 super.requestUpdate();
2193 this._schedulePaint();
2194 }
2195 _schedulePaint() {
2196 if (this._paintScheduled || !this.isConnected) {
2197 return;
2198 }
2199 this._paintScheduled = true;
2200 queueMicrotask(() => {
2201 this._paintScheduled = false;
2202 if (!this.isConnected) {
2203 return;
2204 }
2205 this._paint();
2206 });
2207 }
2208 _paint() {
2209 const root = this.shadowRoot;
2210 if (!root) {
2211 return;
2212 }
2213 if (!root.querySelector("tbody")) {
2214 render(this.render(), root);
2215 }
2216 const colgroup = root.querySelector("colgroup");
2217 const thead = root.querySelector("thead");
2218 const tbody = root.querySelector("tbody");
2219 if (!colgroup || !thead || !tbody) {
2220 return;
2221 }
2222 const cols = this._effectiveColumns();
2223 const stickyN = this._readStickyColumns();
2224 this._lastStickyIndex = this._computeLastStickyIndex(cols, stickyN);
2225 this._paintColgroup(colgroup, cols);
2226 this._paintHead(thead, cols, stickyN);
2227 this._paintBody(tbody, cols, stickyN);
2228 this._applyStickyOffsets();
2229 this._measureHeaderHeight();
2230 this._scheduleStickyOffsets();
2231 this._maybeWarnStickyHeader();
2232 this._maybeWarnLoadingDesync(tbody);
2233 this._ensureResizeObserver();
2234 }
2235 /**
2236 * Diagnostic for the "I set `loading` but the skeleton never
2237 * appeared" footgun. If we get here with the attribute on but no
2238 * `.skeleton` rows in `tbody`, something between attribute set and
2239 * paint went off the rails — historically this happened when the
2240 * base `Component.attributeChangedCallback` called `_scheduleRender`
2241 * directly, bypassing our `requestUpdate` override. Same pattern as
2242 * the sticky-columns 0px tripwire: should never fire, but if it
2243 * does, names the bug instead of leaving the dev guessing.
2244 */
2245 _maybeWarnLoadingDesync(tbody) {
2246 if (this._loadingDesyncWarned) {
2247 return;
2248 }
2249 if (!this.hasAttribute("loading")) {
2250 return;
2251 }
2252 if (tbody.querySelector("tr.skeleton")) {
2253 return;
2254 }
2255 this._loadingDesyncWarned = true;
2256 console.warn(
2257 "[wpd-table] `loading` attribute is set but no skeleton rows rendered. Either attributeChangedCallback didn't route through requestUpdate (framework regression), or `loading` was set after the most recent paint and no follow-up trigger ran. Toggling `data` will force a paint as a workaround."
2258 );
2259 }
2260 /**
2261 * Belt-and-braces sticky-offset scheduling.
2262 *
2263 * - Microtask: cheap, fires after the current task drains. Fixes
2264 * mounts where the synchronous read in `_paint` happened before
2265 * a sibling style applied.
2266 * - rAF: fires before the next paint. Catches "layout settles
2267 * after a queued style mutation" races — the most common cause
2268 * of "col 1 ended up at inset-inline-start: 0px".
2269 *
2270 * Both reduce to a no-op when nothing changed. The cost is two
2271 * extra DOM reads per paint; the win is the bug class disappears.
2272 */
2273 _scheduleStickyOffsets() {
2274 if (!this._stickyMicroScheduled) {
2275 this._stickyMicroScheduled = true;
2276 queueMicrotask(() => {
2277 this._stickyMicroScheduled = false;
2278 if (this.isConnected) {
2279 this._applyStickyOffsets();
2280 }
2281 });
2282 }
2283 if (this._stickyRafHandle === null && typeof requestAnimationFrame !== "undefined") {
2284 this._stickyRafHandle = requestAnimationFrame(() => {
2285 this._stickyRafHandle = null;
2286 if (this.isConnected) {
2287 this._applyStickyOffsets();
2288 this._measureHeaderHeight();
2289 }
2290 });
2291 }
2292 }
2293 /**
2294 * Wire a `ResizeObserver` on the inner `.scroll` element (NOT the
2295 * host). Why: the host's outer width is often pinned by its parent
2296 * panel — a vertical scrollbar appearing inside the table changes
2297 * the inner scroll-area width by ~15px without changing the host
2298 * size. Observing the host would miss that reflow and leave sticky
2299 * offsets stale.
2300 *
2301 * Idempotent — runs once after the first paint produces a real
2302 * `.scroll` element. Disconnect happens in `disconnectedCallback`.
2303 */
2304 _ensureResizeObserver() {
2305 if (this._resizeObserver) {
2306 return;
2307 }
2308 if (typeof ResizeObserver === "undefined") {
2309 return;
2310 }
2311 const scroll = this.shadowRoot?.querySelector(
2312 ".scroll"
2313 );
2314 if (!scroll) {
2315 return;
2316 }
2317 this._resizeObserver = new ResizeObserver(() => {
2318 if (!this.isConnected) {
2319 return;
2320 }
2321 this._applyStickyOffsets();
2322 this._measureHeaderHeight();
2323 this._stickyHeaderWarned = false;
2324 this._maybeWarnStickyHeader();
2325 });
2326 this._resizeObserver.observe(scroll);
2327 this._resizeObserver.observe(this);
2328 }
2329 _paintColgroup(colgroup, cols) {
2330 const out = [];
2331 for (const c of cols) {
2332 const col = document.createElement("col");
2333 if (c.width) {
2334 col.style.width = c.width;
2335 }
2336 out.push(col);
2337 }
2338 colgroup.replaceChildren(...out);
2339 }
2340 _paintHead(thead, cols, stickyN) {
2341 const newHeaderRow = document.createElement("tr");
2342 newHeaderRow.setAttribute("part", "header-row");
2343 for (let i = 0; i < cols.length; i++) {
2344 newHeaderRow.appendChild(this._buildHeaderCell(cols[i], i, stickyN));
2345 }
2346 const existingHeader = thead.querySelector(
2347 ':scope > tr[part="header-row"]'
2348 );
2349 if (existingHeader) {
2350 thead.replaceChild(newHeaderRow, existingHeader);
2351 } else {
2352 thead.insertBefore(newHeaderRow, thead.firstChild);
2353 }
2354 const hasFilter = cols.some(
2355 (c) => c.filter || Array.isArray(c.filterOptions) || typeof c.filterRender === "function"
2356 );
2357 let existingFilter = thead.querySelector(
2358 ":scope > tr.filter-row"
2359 );
2360 if (hasFilter) {
2361 const cells = [];
2362 for (let i = 0; i < cols.length; i++) {
2363 cells.push(this._buildFilterCell(cols[i], i, stickyN));
2364 }
2365 if (!existingFilter) {
2366 existingFilter = document.createElement("tr");
2367 existingFilter.classList.add("filter-row");
2368 existingFilter.setAttribute("part", "filter-row");
2369 thead.appendChild(existingFilter);
2370 }
2371 const current = Array.from(existingFilter.children);
2372 let same = current.length === cells.length;
2373 if (same) {
2374 for (let i = 0; i < cells.length; i++) {
2375 if (current[i] !== cells[i]) {
2376 same = false;
2377 break;
2378 }
2379 }
2380 }
2381 if (!same) {
2382 const wanted = new Set(cells);
2383 for (const cell of cells) {
2384 existingFilter.appendChild(cell);
2385 }
2386 for (const child of Array.from(existingFilter.children)) {
2387 if (!wanted.has(child)) {
2388 existingFilter.removeChild(child);
2389 }
2390 }
2391 }
2392 } else if (existingFilter) {
2393 existingFilter.remove();
2394 }
2395 }
2396 _buildHeaderCell(col, index, stickyN) {
2397 const th = document.createElement("th");
2398 th.setAttribute("scope", "col");
2399 th.dataset.key = col.key;
2400 this._applyCellClasses(th, col, index, stickyN);
2401 if (col.minWidth) {
2402 th.style.minWidth = col.minWidth;
2403 }
2404 if (col.key === SELECT_KEY) {
2405 const mode = this._readSelectable();
2406 if (mode === "multi") {
2407 const cb = document.createElement("input");
2408 cb.type = "checkbox";
2409 cb.className = "select-all-checkbox";
2410 cb.setAttribute("data-noclick", "");
2411 cb.setAttribute("aria-label", "Select all rows");
2412 const total = this._data.length;
2413 const selectedCount = this._countSelectedInData();
2414 cb.checked = total > 0 && selectedCount === total;
2415 cb.indeterminate = selectedCount > 0 && selectedCount < total;
2416 cb.addEventListener("change", () => {
2417 if (cb.checked) {
2418 this.selectAll();
2419 } else {
2420 this.clearSelection();
2421 }
2422 });
2423 th.appendChild(cb);
2424 }
2425 return th;
2426 }
2427 th.textContent = col.label ?? (col.key === EXPANDER_KEY ? "" : col.key);
2428 if (col.sortable) {
2429 th.classList.add("is-sortable");
2430 const isActive = this._sort?.key === col.key;
2431 const indicator = document.createElement("span");
2432 indicator.className = "sort-indicator";
2433 let arrow = "";
2434 if (isActive) {
2435 arrow = this._sort.direction === "asc" ? " ▲" : " ▼";
2436 }
2437 indicator.textContent = arrow;
2438 th.appendChild(indicator);
2439 if (isActive) {
2440 th.classList.add(
2441 this._sort.direction === "asc" ? "sort-asc" : "sort-desc"
2442 );
2443 }
2444 th.addEventListener("click", () => this._cycleSort(col.key));
2445 }
2446 return th;
2447 }
2448 _buildFilterCell(col, index, stickyN) {
2449 const cached = this._filterCache.get(col.key);
2450 const hasExplicitOptions = Array.isArray(col.filterOptions);
2451 const hasCustomRender = typeof col.filterRender === "function";
2452 let desiredKind;
2453 if (!col.filter && !hasExplicitOptions && !hasCustomRender || col.key === EXPANDER_KEY || col.key === SELECT_KEY) {
2454 desiredKind = "none";
2455 } else if (hasCustomRender) {
2456 desiredKind = "custom";
2457 } else if (col.filter === "select" || hasExplicitOptions) {
2458 desiredKind = "select";
2459 } else {
2460 desiredKind = "text";
2461 }
2462 if (cached && cached.kind === desiredKind) {
2463 cached.th.className = "";
2464 this._applyCellClasses(cached.th, col, index, stickyN);
2465 if (desiredKind === "select") {
2466 const select = cached.control;
2467 const opts = this._resolveFilterOptions(col);
2468 const optsKey = opts.map((o) => o.value).join("|");
2469 if (optsKey !== cached.optionsKey) {
2470 this._populateSelect(select, opts, this._filters[col.key] ?? "");
2471 cached.optionsKey = optsKey;
2472 } else {
2473 select.value = this._filters[col.key] ?? "";
2474 }
2475 } else if (desiredKind === "text") {
2476 const input = cached.control;
2477 const want = this._filters[col.key] ?? "";
2478 if (input.value !== want && input.ownerDocument.activeElement !== input) {
2479 input.value = want;
2480 }
2481 } else if (desiredKind === "custom" && col.filterRender) {
2482 col.filterRender(cached.th, {
2483 value: this._filters[col.key] ?? "",
2484 setValue: (next) => this._onFilterChange(col.key, next),
2485 col
2486 });
2487 }
2488 return cached.th;
2489 }
2490 const th = document.createElement("th");
2491 this._applyCellClasses(th, col, index, stickyN);
2492 if (desiredKind === "none") {
2493 this._filterCache.set(col.key, {
2494 th,
2495 control: null,
2496 optionsKey: "",
2497 kind: "none"
2498 });
2499 return th;
2500 }
2501 if (desiredKind === "custom" && col.filterRender) {
2502 col.filterRender(th, {
2503 value: this._filters[col.key] ?? "",
2504 setValue: (next) => this._onFilterChange(col.key, next),
2505 col
2506 });
2507 this._filterCache.set(col.key, {
2508 th,
2509 control: null,
2510 optionsKey: "",
2511 kind: "custom"
2512 });
2513 return th;
2514 }
2515 let control;
2516 let optionsKey = "";
2517 if (desiredKind === "select") {
2518 const select = document.createElement("select");
2519 select.classList.add("filter-select");
2520 select.setAttribute("data-noclick", "");
2521 select.setAttribute(
2522 "aria-label",
2523 `Filter ${col.label ?? col.key}`
2524 );
2525 const opts = this._resolveFilterOptions(col);
2526 this._populateSelect(select, opts, this._filters[col.key] ?? "");
2527 optionsKey = opts.map((o) => o.value).join("|");
2528 select.addEventListener("change", () => {
2529 this._onFilterChange(col.key, select.value);
2530 });
2531 control = select;
2532 } else {
2533 const input = document.createElement("input");
2534 input.type = "search";
2535 input.classList.add("filter-input");
2536 input.setAttribute("data-noclick", "");
2537 input.setAttribute("placeholder", "Filter…");
2538 input.setAttribute("aria-label", `Filter ${col.label ?? col.key}`);
2539 input.value = this._filters[col.key] ?? "";
2540 input.addEventListener("input", () => {
2541 this._onFilterChange(col.key, input.value);
2542 });
2543 control = input;
2544 }
2545 th.appendChild(control);
2546 this._filterCache.set(col.key, {
2547 th,
2548 control,
2549 optionsKey,
2550 kind: desiredKind
2551 });
2552 return th;
2553 }
2554 _populateSelect(select, options, current) {
2555 select.replaceChildren();
2556 const all = document.createElement("option");
2557 all.value = "";
2558 all.textContent = "All";
2559 select.appendChild(all);
2560 for (const opt of options) {
2561 const el = document.createElement("option");
2562 el.value = opt.value;
2563 el.textContent = opt.label;
2564 if (opt.value === current) {
2565 el.selected = true;
2566 }
2567 select.appendChild(el);
2568 }
2569 select.value = current;
2570 }
2571 /**
2572 * Resolve the option list for a select-filter column. Explicit
2573 * `filterOptions` win — that's the contract for server-driven
2574 * tables that need the dropdown to list values not present on
2575 * the current page. Without `filterOptions`, fall back to the
2576 * unique row values in the column (legacy behaviour for
2577 * client-side tables).
2578 */
2579 _resolveFilterOptions(col) {
2580 if (Array.isArray(col.filterOptions)) {
2581 return col.filterOptions;
2582 }
2583 return this._uniqueValues(col.key).map((v) => ({
2584 value: v,
2585 label: v
2586 }));
2587 }
2588 // ------------------------------------------------------------------
2589 // Body
2590 // ------------------------------------------------------------------
2591 _paintBody(tbody, cols, stickyN) {
2592 tbody.replaceChildren();
2593 if (this.hasAttribute("loading")) {
2594 const count = this._readLoadingRows();
2595 for (let i = 0; i < count; i++) {
2596 tbody.appendChild(this._buildSkeletonRow(cols, i));
2597 }
2598 return;
2599 }
2600 const filtered = this._sortedRows(this._filteredRows());
2601 if (filtered.length === 0) {
2602 tbody.appendChild(this._buildEmptyRow(cols.length));
2603 return;
2604 }
2605 for (const { row, index } of filtered) {
2606 tbody.appendChild(this._buildBodyRow(row, index, cols, stickyN));
2607 if (this._expanded.has(index) && this._subTable) {
2608 const sub = this._subTable(row, index);
2609 if (sub) {
2610 tbody.appendChild(this._buildSubTableRow(sub, cols.length));
2611 }
2612 }
2613 }
2614 }
2615 _buildEmptyRow(colspan) {
2616 const tr = document.createElement("tr");
2617 tr.classList.add("empty");
2618 const td = document.createElement("td");
2619 td.colSpan = colspan;
2620 const slot = document.createElement("slot");
2621 slot.name = "empty";
2622 slot.textContent = this.getAttribute("empty") || "No data";
2623 td.appendChild(slot);
2624 tr.appendChild(td);
2625 return tr;
2626 }
2627 _buildSkeletonRow(cols, seed) {
2628 const tr = document.createElement("tr");
2629 tr.classList.add("skeleton");
2630 tr.setAttribute("aria-hidden", "true");
2631 for (const _c of cols) {
2632 const td = document.createElement("td");
2633 const bar = document.createElement("span");
2634 bar.className = "skeleton-bar";
2635 const widthPct = 50 + (seed * 7 + tr.children.length * 13) % 40;
2636 bar.style.width = `${widthPct}%`;
2637 td.appendChild(bar);
2638 tr.appendChild(td);
2639 }
2640 return tr;
2641 }
2642 _buildBodyRow(row, rowIndex, cols, stickyN) {
2643 const tr = document.createElement("tr");
2644 tr.setAttribute("part", "row");
2645 tr.dataset.rowIndex = String(rowIndex);
2646 const id = this._getRowId(row, rowIndex);
2647 tr.dataset.rowId = String(id);
2648 if (this._selection.has(id)) {
2649 tr.classList.add("is-selected");
2650 }
2651 tr.addEventListener("click", (e) => {
2652 this._onRowClick(row, rowIndex, e);
2653 });
2654 for (let i = 0; i < cols.length; i++) {
2655 tr.appendChild(
2656 this._buildBodyCell(cols[i], i, row, rowIndex, stickyN)
2657 );
2658 }
2659 return tr;
2660 }
2661 _buildBodyCell(col, colIndex, row, rowIndex, stickyN) {
2662 const td = document.createElement("td");
2663 this._applyCellClasses(td, col, colIndex, stickyN);
2664 if (col.minWidth) {
2665 td.style.minWidth = col.minWidth;
2666 }
2667 if (col.key === SELECT_KEY) {
2668 const id = this._getRowId(row, rowIndex);
2669 const cb = document.createElement("input");
2670 cb.type = "checkbox";
2671 cb.className = "select-row-checkbox";
2672 cb.setAttribute("data-noclick", "");
2673 cb.setAttribute("aria-label", "Select row");
2674 cb.checked = this._selection.has(id);
2675 cb.addEventListener("change", () => {
2676 if (cb.checked) {
2677 this.select(id);
2678 } else {
2679 this.deselect(id);
2680 }
2681 });
2682 td.appendChild(cb);
2683 return td;
2684 }
2685 if (col.key === EXPANDER_KEY) {
2686 const hasChildren = this._subTable ? !!this._subTable(row, rowIndex) : false;
2687 if (!hasChildren) {
2688 return td;
2689 }
2690 const isOpen = this._expanded.has(rowIndex);
2691 const btn = document.createElement("button");
2692 btn.type = "button";
2693 btn.className = "expander";
2694 btn.setAttribute("data-noclick", "");
2695 btn.setAttribute("aria-expanded", isOpen ? "true" : "false");
2696 btn.setAttribute(
2697 "aria-label",
2698 isOpen ? "Collapse row" : "Expand row"
2699 );
2700 btn.textContent = isOpen ? "▾" : "▸";
2701 btn.addEventListener("click", (e) => {
2702 this._toggleRow(rowIndex, row, e);
2703 });
2704 td.appendChild(btn);
2705 return td;
2706 }
2707 const value = row[col.key];
2708 if (col.render) {
2709 const out = col.render(value, row, rowIndex);
2710 this._mountCellContent(td, out);
2711 } else if (value !== null && value !== void 0) {
2712 td.textContent = String(value);
2713 }
2714 return td;
2715 }
2716 _buildSubTableRow(sub, colspan) {
2717 const tr = document.createElement("tr");
2718 tr.classList.add("subtable");
2719 tr.setAttribute("part", "subtable-row");
2720 const td = document.createElement("td");
2721 td.colSpan = colspan;
2722 const inner = document.createElement("div");
2723 inner.classList.add("subtable-inner");
2724 if (sub instanceof Node) {
2725 inner.appendChild(sub);
2726 } else if (isTemplateResult(sub)) {
2727 render(sub, inner);
2728 } else {
2729 const nested = document.createElement("wpd-table");
2730 nested.columns = sub.columns;
2731 nested.data = sub.data;
2732 if (sub.subTable) {
2733 nested.subTable = sub.subTable;
2734 }
2735 inner.appendChild(nested);
2736 }
2737 td.appendChild(inner);
2738 tr.appendChild(td);
2739 return tr;
2740 }
2741 _mountCellContent(td, out) {
2742 if (typeof out === "string") {
2743 td.textContent = out;
2744 return;
2745 }
2746 if (out instanceof Node) {
2747 td.appendChild(out);
2748 return;
2749 }
2750 if (isTemplateResult(out)) {
2751 render(out, td);
2752 }
2753 }
2754 // ------------------------------------------------------------------
2755 // Behavior
2756 // ------------------------------------------------------------------
2757 _onFilterChange(key, value) {
2758 if (value === "") {
2759 delete this._filters[key];
2760 } else {
2761 this._filters[key] = value;
2762 }
2763 this.emit("wpd-table-filter-change", { filters: { ...this._filters } });
2764 const root = this.shadowRoot;
2765 const tbody = root?.querySelector("tbody");
2766 if (tbody) {
2767 const cols = this._effectiveColumns();
2768 const stickyN = this._readStickyColumns();
2769 this._lastStickyIndex = this._computeLastStickyIndex(cols, stickyN);
2770 this._paintBody(tbody, cols, stickyN);
2771 this._applyStickyOffsets();
2772 }
2773 }
2774 _onRowClick(row, index, e) {
2775 const path = e.composedPath?.() ?? [];
2776 for (const node of path) {
2777 if (node instanceof Element && node.hasAttribute("data-noclick")) {
2778 return;
2779 }
2780 if (node === this) {
2781 break;
2782 }
2783 }
2784 this.emit("wpd-table-row-click", { row, index, originalEvent: e });
2785 }
2786 _toggleRow(index, row, e) {
2787 e.stopPropagation();
2788 const isOpen = this._expanded.has(index);
2789 if (isOpen) {
2790 this._expanded.delete(index);
2791 } else {
2792 this._expanded.add(index);
2793 }
2794 this.emit("wpd-table-expand-change", {
2795 row,
2796 index,
2797 expanded: !isOpen
2798 });
2799 this._schedulePaint();
2800 }
2801 _cycleSort(key) {
2802 if (!this._sort || this._sort.key !== key) {
2803 this._sort = { key, direction: "asc" };
2804 } else if (this._sort.direction === "asc") {
2805 this._sort = { key, direction: "desc" };
2806 } else {
2807 this._sort = null;
2808 }
2809 this.emit("wpd-table-sort-change", {
2810 sort: this._sort ? { ...this._sort } : null
2811 });
2812 this._schedulePaint();
2813 }
2814 _emitSelectionChange() {
2815 this.emit("wpd-table-selection-change", {
2816 selection: Array.from(this._selection),
2817 rows: this.selectedRows
2818 });
2819 }
2820 // ------------------------------------------------------------------
2821 // Filtering + sorting
2822 // ------------------------------------------------------------------
2823 _filteredRows() {
2824 const out = [];
2825 const active = Object.keys(this._filters).filter(
2826 (k) => this._filters[k] !== ""
2827 );
2828 for (let i = 0; i < this._data.length; i++) {
2829 const row = this._data[i];
2830 let pass = true;
2831 for (const key of active) {
2832 const col = this._columns.find((c) => c.key === key);
2833 if (col && typeof col.filterRender === "function") {
2834 continue;
2835 }
2836 const filter = this._filters[key] ?? "";
2837 const cell = row[key];
2838 const cellStr = cell === null || cell === void 0 ? "" : String(cell);
2839 if (col?.filter === "select") {
2840 if (cellStr !== filter) {
2841 pass = false;
2842 break;
2843 }
2844 } else if (!cellStr.toLowerCase().includes(filter.toLowerCase())) {
2845 pass = false;
2846 break;
2847 }
2848 }
2849 if (pass) {
2850 out.push({ row, index: i });
2851 }
2852 }
2853 return out;
2854 }
2855 _sortedRows(rows) {
2856 if (!this._sort) {
2857 return rows;
2858 }
2859 const col = this._columns.find((c) => c.key === this._sort.key);
2860 if (!col) {
2861 return rows;
2862 }
2863 const dir = this._sort.direction === "desc" ? -1 : 1;
2864 const out = rows.slice();
2865 out.sort((a, b) => {
2866 const av = col.sortValue ? col.sortValue(a.row, a.row[col.key]) : a.row[col.key];
2867 const bv = col.sortValue ? col.sortValue(b.row, b.row[col.key]) : b.row[col.key];
2868 return compareValues(av, bv) * dir;
2869 });
2870 return out;
2871 }
2872 _uniqueValues(key) {
2873 const seen = /* @__PURE__ */ new Set();
2874 for (const row of this._data) {
2875 const v = row[key];
2876 if (v === null || v === void 0) {
2877 continue;
2878 }
2879 seen.add(String(v));
2880 }
2881 return Array.from(seen).sort();
2882 }
2883 _countSelectedInData() {
2884 let n = 0;
2885 this._data.forEach((row, i) => {
2886 if (this._selection.has(this._getRowId(row, i))) {
2887 n++;
2888 }
2889 });
2890 return n;
2891 }
2892 // ------------------------------------------------------------------
2893 // Sticky columns + attribute reads
2894 // ------------------------------------------------------------------
2895 _readStickyColumns() {
2896 const raw = parseInt(this.getAttribute("sticky-columns") || "0", 10);
2897 return Number.isFinite(raw) && raw > 0 ? raw : 0;
2898 }
2899 _readLoadingRows() {
2900 const raw = parseInt(this.getAttribute("loading-rows") || "5", 10);
2901 return Number.isFinite(raw) && raw > 0 ? Math.min(raw, 100) : 5;
2902 }
2903 _readSelectable() {
2904 const v = this.getAttribute("selectable");
2905 if (v === "single") {
2906 return "single";
2907 }
2908 if (v === "multi" || v === "") {
2909 return "multi";
2910 }
2911 return null;
2912 }
2913 /**
2914 * Sticky-band membership. The first N columns get pinned, with two
2915 * per-column overrides: `column.sticky = true` opts in even outside
2916 * the band; `column.sticky = false` opts out within it.
2917 */
2918 _isStickyIndex(index, stickyN, col) {
2919 if (col.sticky === false) {
2920 return false;
2921 }
2922 if (col.sticky === true) {
2923 return true;
2924 }
2925 return index < stickyN;
2926 }
2927 _computeLastStickyIndex(cols, stickyN) {
2928 let last = -1;
2929 for (let i = 0; i < cols.length; i++) {
2930 if (this._isStickyIndex(i, stickyN, cols[i])) {
2931 last = i;
2932 }
2933 }
2934 return last;
2935 }
2936 _applyCellClasses(cell, col, index, stickyN) {
2937 if (col.key === EXPANDER_KEY) {
2938 cell.classList.add("col-expander");
2939 }
2940 if (col.key === SELECT_KEY) {
2941 cell.classList.add("col-select");
2942 }
2943 if (col.align === "center") {
2944 cell.classList.add("align-center");
2945 }
2946 if (col.align === "end") {
2947 cell.classList.add("align-end");
2948 }
2949 const sticky = this._isStickyIndex(index, stickyN, col);
2950 if (sticky) {
2951 cell.classList.add("is-sticky");
2952 if (index === this._lastStickyIndex) {
2953 cell.classList.add("is-sticky-edge");
2954 }
2955 }
2956 }
2957 _effectiveColumns() {
2958 const out = [];
2959 if (this._readSelectable()) {
2960 out.push({
2961 key: SELECT_KEY,
2962 label: "",
2963 // The descriptor width is painted onto a `<col>`
2964 // element and is the authoritative column-width
2965 // source in table-layout: auto — CSS `td { width }`
2966 // is ignored once `<col>` has a value. Pair with
2967 // the matching `td.col-select` rule (zero
2968 // `padding-inline`, `text-align: center`) so the
2969 // checkbox sits with breathing room on both sides.
2970 width: "40px",
2971 align: "center"
2972 });
2973 }
2974 if (this._subTable) {
2975 out.push({
2976 key: EXPANDER_KEY,
2977 label: "",
2978 // Same contract as col-select. 36px column +
2979 // 20px button + zero padding centers the chevron
2980 // with ~8px on each side.
2981 width: "36px",
2982 align: "center"
2983 });
2984 }
2985 out.push(...this._columns);
2986 return out;
2987 }
2988 /**
2989 * Walk the header row, sum the natural widths of the sticky cells,
2990 * then write cumulative `inset-inline-start` offsets onto every
2991 * row's matching cells.
2992 */
2993 _applyStickyOffsets() {
2994 const root = this.shadowRoot;
2995 if (!root) {
2996 return;
2997 }
2998 const headRow = root.querySelector("thead tr");
2999 if (!headRow) {
3000 return;
3001 }
3002 const ths = Array.from(headRow.children);
3003 const offsets = [];
3004 let acc = 0;
3005 for (let i = 0; i < ths.length; i++) {
3006 offsets[i] = acc;
3007 if (ths[i].classList.contains("is-sticky")) {
3008 acc += ths[i].offsetWidth;
3009 }
3010 }
3011 const rows = root.querySelectorAll(
3012 "thead tr, tbody tr:not(.subtable):not(.empty):not(.skeleton)"
3013 );
3014 rows.forEach((r) => {
3015 const cells = Array.from(r.children);
3016 for (let i = 0; i < cells.length; i++) {
3017 if (cells[i].classList.contains("is-sticky")) {
3018 cells[i].style.insetInlineStart = `${offsets[i]}px`;
3019 }
3020 }
3021 });
3022 this._maybeWarnStickyOffsetRace(ths, offsets);
3023 }
3024 _maybeWarnStickyOffsetRace(ths, offsets) {
3025 if (this._stickyRaceWarned) {
3026 return;
3027 }
3028 const stickyN = this._readStickyColumns();
3029 if (stickyN < 2) {
3030 return;
3031 }
3032 const lastIdx = Math.min(stickyN - 1, ths.length - 1);
3033 if (lastIdx <= 0) {
3034 return;
3035 }
3036 if (offsets[lastIdx] !== 0) {
3037 return;
3038 }
3039 if (this.offsetWidth === 0) {
3040 return;
3041 }
3042 this._stickyRaceWarned = true;
3043 const w0 = ths[0]?.offsetWidth ?? 0;
3044 console.warn(
3045 `[wpd-table] sticky-columns: column ${lastIdx} resolved to inset-inline-start: 0px while the host is visible. ths[0].offsetWidth was ${w0}px at measurement time. Likely a layout race — call recomputeLayout() after the panel finishes its mount/transition, or wrap the assignment of \`data\` in a requestAnimationFrame.`
3046 );
3047 }
3048 _measureHeaderHeight() {
3049 const root = this.shadowRoot;
3050 if (!root) {
3051 return;
3052 }
3053 const headRow = root.querySelector("thead tr");
3054 if (!headRow) {
3055 return;
3056 }
3057 const h = headRow.offsetHeight;
3058 if (h > 0) {
3059 this.style.setProperty("--wpd-table-header-height", `${h}px`);
3060 }
3061 }
3062 /**
3063 * Once-per-element warning for the most common sticky-header
3064 * mistake: forgetting to give the table a scroll container. Without
3065 * a max-height (or a scrolling ancestor), `position: sticky`
3066 * silently does nothing because there's no scrollport for it to
3067 * stick within.
3068 */
3069 _maybeWarnStickyHeader() {
3070 if (this._stickyHeaderWarned) {
3071 return;
3072 }
3073 if (!this.hasAttribute("sticky-header")) {
3074 return;
3075 }
3076 if (this.hasAttribute("loading") || this._data.length < 8) {
3077 return;
3078 }
3079 const scroll = this.shadowRoot?.querySelector(
3080 ".scroll"
3081 );
3082 if (!scroll) {
3083 return;
3084 }
3085 if (scroll.offsetWidth === 0) {
3086 return;
3087 }
3088 if (scroll.scrollHeight <= scroll.clientHeight + 1) {
3089 this._stickyHeaderWarned = true;
3090 console.warn(
3091 "[wpd-table] sticky-header is set but the table has no scroll container. Set --wpd-table-max-height on the host (or wrap it in a scrolling parent) so the header has something to stick to."
3092 );
3093 }
3094 }
3095 };
3096 _WpdTable.props = [
3097 "stickyColumns",
3098 "stickyHeader",
3099 "striped",
3100 "hover",
3101 "compact",
3102 "bordered",
3103 "empty",
3104 "loading",
3105 "loadingRows",
3106 "selectable"
3107 ];
3108 _WpdTable.styles = [styles$8];
3109 _WpdTable.help = {
3110 title: "Table",
3111 summary: "Data-driven table. Assign `columns` + `data` and you get a styled table with optional per-column filters, click-to-sort, multi-row selection, sticky columns/header, sub-tables, custom cell renderers, loading skeleton, and a slottable empty state.",
3112 status: "experimental",
3113 since: "0.18.0",
3114 props: [
3115 {
3116 name: "sticky-columns",
3117 type: "integer",
3118 description: "Pin the first N columns to the inline-start edge. Widths are measured after layout, so variable-width columns work. The auto-injected expander (subTable) and select (selectable) columns count toward N."
3119 },
3120 {
3121 name: "sticky-header",
3122 type: "boolean",
3123 description: "Pin the header (and filter row) to the top. Requires a scrolling parent or `--wpd-table-max-height` — the component warns once if it detects sticky-header on a non-scrolling container."
3124 },
3125 { name: "striped", type: "boolean", description: "Zebra rows." },
3126 { name: "hover", type: "boolean", description: "Highlight rows on hover." },
3127 { name: "compact", type: "boolean", description: "Tighter padding + smaller font." },
3128 { name: "bordered", type: "boolean", description: "Vertical cell borders." },
3129 {
3130 name: "empty",
3131 type: "string",
3132 description: "Fallback text shown when there are no rows. For richer empty states, project light-DOM content into the `empty` slot."
3133 },
3134 {
3135 name: "loading",
3136 type: "boolean",
3137 description: "Paint shimmering skeleton rows in place of body content. Filters / sort headers stay live."
3138 },
3139 {
3140 name: "loading-rows",
3141 type: "integer",
3142 description: "Number of skeleton rows when loading. Default 5."
3143 },
3144 {
3145 name: "selectable",
3146 type: '"single" | "multi"',
3147 description: "Auto-prepend a checkbox column. `multi` puts a select-all checkbox in the header; `single` enforces at-most-one selected."
3148 }
3149 ],
3150 events: [
3151 { name: "wpd-table-filter-change", description: "Filter input changed." },
3152 { name: "wpd-table-sort-change", description: "Header click cycled the sort." },
3153 { name: "wpd-table-selection-change", description: "Selection set changed." },
3154 { name: "wpd-table-row-click", description: "Body row clicked (skips data-noclick descendants)." },
3155 { name: "wpd-table-expand-change", description: "Sub-table toggled." }
3156 ],
3157 slots: [
3158 { name: "empty", description: "Custom empty-state content (CTA, illustration, etc.)." }
3159 ],
3160 cssProps: [
3161 { name: "--wpd-table-bg" },
3162 { name: "--wpd-table-border" },
3163 { name: "--wpd-table-column-border" },
3164 { name: "--wpd-table-header-bg" },
3165 { name: "--wpd-table-row-hover" },
3166 { name: "--wpd-table-stripe" },
3167 { name: "--wpd-table-cell-padding" },
3168 { name: "--wpd-table-font-size" },
3169 { name: "--wpd-table-max-height" },
3170 { name: "--wpd-table-skeleton-color" }
3171 ],
3172 example: html`
3173 <wpd-table id="sample-table" sticky-header striped hover></wpd-table>
3174 `
3175 };
3176 let WpdTable = _WpdTable;
3177 function isTemplateResult(v) {
3178 return !!v && v.__wpdHtml === true;
3179 }
3180 function compareValues(a, b) {
3181 if (a === b) {
3182 return 0;
3183 }
3184 if (a === null || a === void 0) {
3185 return -1;
3186 }
3187 if (b === null || b === void 0) {
3188 return 1;
3189 }
3190 if (typeof a === "number" && typeof b === "number") {
3191 return a - b;
3192 }
3193 if (a instanceof Date && b instanceof Date) {
3194 return a.getTime() - b.getTime();
3195 }
3196 const an = Number(a);
3197 const bn = Number(b);
3198 if (Number.isFinite(an) && Number.isFinite(bn)) {
3199 return an - bn;
3200 }
3201 return String(a).localeCompare(String(b));
3202 }
3203 defineComponent("wpd-table", WpdTable);
3204 const tabsStyles = css`:host{display:flex;gap:4px;margin-bottom:10px;border-bottom:1px solid var( --desktop-mode-border,#dcdcde )}`;
3205 const tabPanelStyles = css`:host{display:block}:host( [ hidden ] ){display:none}:host(:focus-visible ){outline:2px solid var( --wp-admin-theme-color,#2271b1 );outline-offset:4px;border-radius:4px}`;
3206 const tabStyles = css`:host{display:inline-block}button{appearance:none;padding:6px 10px;border:none;background:transparent;color:var( --desktop-mode-muted,#50575e );font:inherit;font-size:12px;font-weight:500;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;transition:color 0.15s ease,border-color 0.15s ease}button:hover{color:var( --wp-admin-theme-color,#2271b1 )}button:focus-visible{outline:2px solid var( --wp-admin-theme-color,#2271b1 );outline-offset:2px}:host( [ aria-selected='true' ] ) button{color:var( --wp-admin-theme-color,#2271b1 );border-bottom-color:var( --wp-admin-theme-color,#2271b1 )}`;
3207 const _WpdTab = class _WpdTab extends Component {
3208 render() {
3209 this.setAttribute("role", "tab");
3210 return html`
3211 <button type="button" @click=${() => this._onPick()}>
3212 <slot></slot>
3213 </button>
3214 `;
3215 }
3216 _onPick() {
3217 this.emit("wpd-tab-pick", {
3218 value: this.value
3219 });
3220 }
3221 };
3222 _WpdTab.props = ["value"];
3223 _WpdTab.styles = [tabStyles];
3224 _WpdTab.help = {
3225 title: "Tab",
3226 summary: "Single tab inside a <wpd-tabs> strip. Carries its identifier via `value`; aria-selected + tabindex are mirrored by the parent.",
3227 status: "stable",
3228 since: "0.7.0",
3229 props: [
3230 {
3231 name: "value",
3232 type: "string",
3233 description: "Identifier the tab contributes to the parent strip selection."
3234 }
3235 ],
3236 slots: [
3237 { name: "(default)", description: "Visible tab label." }
3238 ],
3239 events: [
3240 {
3241 name: "wpd-tab-pick",
3242 description: "Internal event bubbled to the parent <wpd-tabs>. Consumers should listen for wpd-tab-change on the strip instead.",
3243 detail: "{ value: string | null }"
3244 }
3245 ]
3246 };
3247 let WpdTab = _WpdTab;
3248 defineComponent("wpd-tab", WpdTab);
3249 const _WpdTabs = class _WpdTabs extends Component {
3250 connectedCallback() {
3251 super.connectedCallback();
3252 this.addEventListener("wpd-tab-pick", (e) => {
3253 const detail = e.detail;
3254 e.stopPropagation();
3255 this.value = detail.value;
3256 this.emit("wpd-tab-change", { value: detail.value });
3257 });
3258 }
3259 /**
3260 * Declarative item-list setter. Replaces the existing `<wpd-tab>`
3261 * children with a fresh set built from a `{ value, label }`
3262 * array. The `value` prop is preserved if it still matches a new
3263 * entry; otherwise it falls back to the first item.
3264 *
3265 * Lets plugins that populate tabs dynamically (route-driven
3266 * admin screens, filtered lists) replace the declarative
3267 * markup with a one-liner:
3268 *
3269 * ```js
3270 * tabs.items = [
3271 * { value: 'calc', label: 'Calc' },
3272 * { value: 'convert', label: 'Convert' },
3273 * ];
3274 * ```
3275 *
3276 * @since 0.11.0
3277 */
3278 set items(list) {
3279 replaceChildren(this, "wpd-tab", list);
3280 const current = this.value;
3281 const stillValid = current !== null && list.some((i) => i.value === current);
3282 if (!stillValid && list.length > 0) {
3283 this.value = list[0].value;
3284 } else {
3285 this.requestUpdate();
3286 }
3287 }
3288 render() {
3289 this.setAttribute("role", "tablist");
3290 const label = this.label || "";
3291 if (label) {
3292 this.setAttribute("aria-label", label);
3293 }
3294 const current = this.value;
3295 queueMicrotask(() => {
3296 const tabs = this.querySelectorAll("wpd-tab");
3297 for (const tab of Array.from(tabs)) {
3298 const v = tab.getAttribute("value");
3299 tab.setAttribute(
3300 "aria-selected",
3301 v === current ? "true" : "false"
3302 );
3303 tab.setAttribute("tabindex", v === current ? "0" : "-1");
3304 }
3305 syncTabpanels(this, current);
3306 });
3307 return html`<slot></slot>`;
3308 }
3309 };
3310 _WpdTabs.props = ["value", "label"];
3311 _WpdTabs.styles = [tabsStyles];
3312 _WpdTabs.help = {
3313 title: "Tabs",
3314 summary: 'Underline-accent tab strip. Pair with sibling <wpd-tabpanel for="…"> elements and the strip auto-toggles their hidden attribute on selection.',
3315 status: "stable",
3316 since: "0.7.0",
3317 props: [
3318 {
3319 name: "value",
3320 type: "string",
3321 description: "Currently active tab value. Mirrored to child <wpd-tab> aria-selected."
3322 },
3323 {
3324 name: "label",
3325 type: "string",
3326 description: "aria-label for the tablist — describe the tab group for assistive tech."
3327 }
3328 ],
3329 slots: [
3330 {
3331 name: "(default)",
3332 description: '<wpd-tab value="…"> children forming the strip.'
3333 }
3334 ],
3335 events: [
3336 {
3337 name: "wpd-tab-change",
3338 description: "Fires when the active tab changes.",
3339 detail: "{ value: string }"
3340 }
3341 ],
3342 example: html`
3343 <wpd-tabs value="one" label="Demo tabs">
3344 <wpd-tab value="one">One</wpd-tab>
3345 <wpd-tab value="two">Two</wpd-tab>
3346 <wpd-tab value="three">Three</wpd-tab>
3347 </wpd-tabs>
3348 <wpd-tabpanel for="one">First panel.</wpd-tabpanel>
3349 <wpd-tabpanel for="two">Second panel.</wpd-tabpanel>
3350 <wpd-tabpanel for="three">Third panel.</wpd-tabpanel>
3351 `
3352 };
3353 let WpdTabs = _WpdTabs;
3354 defineComponent("wpd-tabs", WpdTabs);
3355 const _WpdTabPanel = class _WpdTabPanel extends Component {
3356 // Shadow DOM — the render target for this component is its
3357 // own shadow root, which holds a single `<slot>` that projects
3358 // whatever the caller placed between the `<wpd-tabpanel>` open
3359 // and close tags. Slotted children remain light-DOM descendants
3360 // of the panel element (the slot rendering mechanism doesn't
3361 // move them), so `panel.querySelector(...)` from plugin render
3362 // callbacks keeps working.
3363 //
3364 // Earlier 0.11.0 builds of this component used light DOM with
3365 // a `<slot>` render, which wiped the panel's server-rendered
3366 // template content on first mount — every `render()` writes
3367 // into `_renderRoot`, and with light DOM that's the panel
3368 // itself. Shadow DOM isolates the render surface.
3369 connectedCallback() {
3370 super.connectedCallback();
3371 this.setAttribute("role", "tabpanel");
3372 if (!this.hasAttribute("tabindex")) {
3373 this.setAttribute("tabindex", "0");
3374 }
3375 const owner = findOwningTabs(this);
3376 if (owner) {
3377 syncTabpanels(owner, owner.getAttribute("value"));
3378 }
3379 }
3380 render() {
3381 return html`<slot></slot>`;
3382 }
3383 };
3384 _WpdTabPanel.props = ["for"];
3385 _WpdTabPanel.styles = [tabPanelStyles];
3386 _WpdTabPanel.help = {
3387 title: "Tab panel",
3388 summary: 'Auto-managed panel paired with a sibling <wpd-tabs>. Declares which tab it belongs to via `for="<tab-value>"`; the parent strip toggles `hidden` whenever the active tab changes. role="tabpanel" and tabindex="0" are set automatically.',
3389 status: "stable",
3390 since: "0.11.0",
3391 props: [
3392 {
3393 name: "for",
3394 type: "string",
3395 description: "Matches the `value` of the owning <wpd-tab>. Panel is shown when its parent tabs strip is on that value."
3396 }
3397 ],
3398 slots: [
3399 { name: "(default)", description: "Panel body content." }
3400 ]
3401 };
3402 let WpdTabPanel = _WpdTabPanel;
3403 defineComponent("wpd-tabpanel", WpdTabPanel);
3404 function replaceChildren(host, tag, items) {
3405 const existing = host.querySelectorAll(`:scope > ${tag}`);
3406 for (const el of Array.from(existing)) {
3407 el.remove();
3408 }
3409 for (const item of items) {
3410 const el = document.createElement(tag);
3411 el.setAttribute("value", item.value);
3412 el.textContent = item.label;
3413 host.appendChild(el);
3414 }
3415 }
3416 function findOwningTabs(panel) {
3417 const parent = panel.parentElement;
3418 if (!parent) {
3419 return null;
3420 }
3421 const sibling = parent.querySelector(":scope > wpd-tabs");
3422 if (sibling) {
3423 return sibling;
3424 }
3425 return panel.closest("wpd-tabs");
3426 }
3427 function syncTabpanels(tabs, value) {
3428 const panels = /* @__PURE__ */ new Set();
3429 const parent = tabs.parentElement;
3430 if (parent) {
3431 for (const p of Array.from(
3432 parent.querySelectorAll(":scope > wpd-tabpanel")
3433 )) {
3434 panels.add(p);
3435 }
3436 }
3437 for (const p of Array.from(
3438 tabs.querySelectorAll(":scope > wpd-tabpanel")
3439 )) {
3440 panels.add(p);
3441 }
3442 for (const panel of panels) {
3443 const pfor = panel.getAttribute("for");
3444 const active = pfor !== null && pfor === value;
3445 if (active) {
3446 panel.removeAttribute("hidden");
3447 } else {
3448 panel.setAttribute("hidden", "");
3449 }
3450 panel.setAttribute("aria-hidden", active ? "false" : "true");
3451 }
3452 }
3453 const styles$7 = css`:host{display:inline-flex;max-width:100%}.wpd-tag-input{display:inline-flex;flex-wrap:wrap;align-items:center;gap:var( --wpd-tag-input-gap,4px );padding:var( --wpd-tag-input-padding,2px );min-height:24px;max-width:100%}.wpd-tag-input__chips{display:inline-flex;flex-wrap:wrap;align-items:center;gap:var( --wpd-tag-input-gap,4px );min-width:0}.wpd-tag-input__add{appearance:none;display:inline-flex;align-items:center;gap:3px;padding:1px 8px;min-height:22px;font:inherit;font-size:11px;font-weight:500;line-height:1;color:var( --wpd-tag-input-add-fg,#50575e );background:transparent;border:1px dashed var( --wpd-tag-input-add-border,#c3c4c7 );border-radius:999px;cursor:pointer;transition:background-color 0.12s ease,color 0.12s ease,border-color 0.12s ease}.wpd-tag-input__add:hover:not(:disabled ){background:rgba( 0,0,0,0.04 );color:var( --wpd-tag-input-add-fg-hover,#1d2327 );border-color:var( --wpd-tag-input-add-border-hover,#8c8f94 )}.wpd-tag-input__add:focus-visible{outline:none;border-style:solid;box-shadow:0 0 0 2px var( --wp-admin-theme-color,#2271b1 )}.wpd-tag-input__add:disabled{opacity:0.5;cursor:not-allowed}.wpd-tag-input__add svg{display:block}.wpd-tag-input__editor{position:relative;display:inline-flex;align-items:center;flex:0 1 auto;min-width:120px}.wpd-tag-input__input{appearance:none;font:inherit;font-size:12px;line-height:1.4;padding:2px 8px;border:1px solid var( --wpd-tag-input-input-border,#2271b1 );border-radius:999px;background:var( --wpd-tag-input-input-bg,#fff );color:var( --wpd-tag-input-input-fg,#1d2327 );min-width:80px;max-width:240px}.wpd-tag-input__input:focus{outline:none;box-shadow:0 0 0 2px color-mix( in srgb,var( --wp-admin-theme-color,#2271b1 ) 30%,transparent )}.wpd-tag-input__suggestions{position:absolute;top:calc( 100% + 4px );left:0;min-width:220px;max-width:320px;max-height:240px;overflow-y:auto;padding:4px 0;background:var( --wpd-tag-input-pop-bg,#fff );color:var( --wpd-tag-input-pop-fg,#1d2327 );border:1px solid var( --wpd-tag-input-pop-border,#c3c4c7 );border-radius:8px;box-shadow:0 6px 16px rgba( 0,0,0,0.08 ),0 1px 2px rgba( 0,0,0,0.06 );z-index:50}.wpd-tag-input__suggestion-item{display:flex;align-items:center;gap:6px;padding:6px 12px;font-size:13px;cursor:pointer;user-select:none}.wpd-tag-input__suggestion-item[ aria-selected='true' ]{background:color-mix( in srgb,var( --wp-admin-theme-color,#2271b1 ) 10%,transparent );color:var( --wp-admin-theme-color,#2271b1 )}.wpd-tag-input__suggestion-create{font-style:italic;color:var( --wpd-tag-input-create-fg,#50575e );border-top:1px solid var( --wpd-tag-input-pop-divider,#f0f0f1 )}.wpd-tag-input__suggestion-create[ aria-selected='true' ]{color:var( --wp-admin-theme-color,#2271b1 )}.wpd-tag-input__suggestion-empty,.wpd-tag-input__suggestion-loading{display:flex;align-items:center;gap:8px;padding:8px 12px;font-size:12px;color:var( --wpd-tag-input-pop-muted,#646970 )}.wpd-tag-input__suggestion-spinner{display:inline-block;width:10px;height:10px;border-radius:50%;border:2px solid currentColor;border-top-color:transparent;animation:wpd-tag-input-spin 0.8s linear infinite}@keyframes wpd-tag-input-spin{to{transform:rotate( 360deg )}}:host( [ disabled ] ) .wpd-tag-input{opacity:0.6;pointer-events:none}`;
3454 const styles$6 = css`:host{display:inline-flex;max-width:100%;vertical-align:middle}:host( [ hidden ] ){display:none}.wpd-chip{display:inline-flex;align-items:center;gap:var( --wpd-chip-gap,4px );padding:var( --wpd-chip-padding,2px 8px );border-radius:var( --wpd-chip-radius,999px );font-size:var( --wpd-chip-font-size,12px );line-height:var( --wpd-chip-line-height,1.6 );font-weight:var( --wpd-chip-font-weight,500 );background:var( --wpd-chip-bg,#f0f0f1 );color:var( --wpd-chip-fg,#1d2327 );border:var( --wpd-chip-border,1px solid transparent );max-width:100%;box-sizing:border-box;transition:background-color 0.12s ease,color 0.12s ease,border-color 0.12s ease,transform 0.12s ease,opacity 0.12s ease}:host( [ tone='accent' ] ) .wpd-chip{background:var( --wpd-chip-bg,color-mix( in srgb,var( --wp-admin-theme-color,#2271b1 ) 14%,transparent ) );color:var( --wpd-chip-fg,var( --wp-admin-theme-color,#2271b1 ) )}:host( [ tone='positive' ] ) .wpd-chip{background:var( --wpd-chip-bg,rgba( 30,132,73,0.14 ) );color:var( --wpd-chip-fg,#1d6f42 )}:host( [ tone='warning' ] ) .wpd-chip{background:var( --wpd-chip-bg,rgba( 217,119,6,0.18 ) );color:var( --wpd-chip-fg,#8a4a06 )}:host( [ tone='danger' ] ) .wpd-chip{background:var( --wpd-chip-bg,rgba( 214,54,56,0.14 ) );color:var( --wpd-chip-fg,#a02622 )}:host( [ pending ] ) .wpd-chip{opacity:0.65;animation:wpd-chip-pulse 1.2s ease-in-out infinite}@keyframes wpd-chip-pulse{0%,100%{opacity:0.55}50%{opacity:0.95}}.wpd-chip__label{max-width:var( --wpd-chip-label-max,220px );overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wpd-chip__icon{display:inline-flex;align-items:center;flex-shrink:0}.wpd-chip__icon::slotted( * ){display:inline-flex}.wpd-chip__dismiss{appearance:none;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:16px;height:16px;margin-inline-start:2px;padding:0;border:0;border-radius:50%;background:transparent;color:inherit;cursor:pointer;opacity:0.55;transition:opacity 0.12s ease,background-color 0.12s ease}.wpd-chip__dismiss:hover,.wpd-chip__dismiss:focus-visible{opacity:1;background:rgba( 0,0,0,0.12 );outline:none}.wpd-chip__dismiss:focus-visible{box-shadow:0 0 0 2px var( --wp-admin-theme-color,#2271b1 )}.wpd-chip__dismiss[ disabled ]{opacity:0.35;cursor:not-allowed}.wpd-chip__dismiss svg{display:block;width:10px;height:10px}:host( [ disabled ] ) .wpd-chip{opacity:0.55;cursor:not-allowed}:host( [ size='compact' ] ) .wpd-chip{padding:var( --wpd-chip-padding,1px 6px );font-size:var( --wpd-chip-font-size,11px )}`;
3455 const _WpdChip = class _WpdChip extends Component {
3456 constructor() {
3457 super(...arguments);
3458 this._onHostKeyDown = (e) => {
3459 const dismissible = this.dismissible !== null;
3460 if (!dismissible) {
3461 return;
3462 }
3463 if (e.key === "Backspace" || e.key === "Delete") {
3464 e.preventDefault();
3465 const disabled = this.disabled !== null;
3466 if (disabled) {
3467 return;
3468 }
3469 const label = this.label ?? "";
3470 this.emit("wpd-chip-dismiss", { label });
3471 }
3472 };
3473 }
3474 connectedCallback() {
3475 super.connectedCallback();
3476 this.addEventListener("keydown", this._onHostKeyDown);
3477 }
3478 disconnectedCallback() {
3479 this.removeEventListener("keydown", this._onHostKeyDown);
3480 }
3481 render() {
3482 const label = this.label ?? "";
3483 const dismissible = this.dismissible !== null;
3484 const disabled = this.disabled !== null;
3485 return html`
3486 <span part="chip" class="wpd-chip">
3487 <span class="wpd-chip__icon">
3488 <slot name="icon"></slot>
3489 </span>
3490 <span class="wpd-chip__label">
3491 ${label === "" ? html`<slot></slot>` : label}
3492 </span>
3493 ${dismissible ? html`
3494 <button
3495 part="dismiss"
3496 class="wpd-chip__dismiss"
3497 type="button"
3498 aria-label=${`Remove ${label || "chip"}`}
3499 ?disabled=${disabled}
3500 @click=${(e) => this._onDismiss(e)}
3501 >
3502 ${_iconCross$1()}
3503 </button>
3504 ` : html``}
3505 </span>
3506 `;
3507 }
3508 _onDismiss(e) {
3509 e.stopPropagation();
3510 const disabled = this.disabled !== null;
3511 if (disabled) {
3512 return;
3513 }
3514 const label = this.label ?? "";
3515 this.emit("wpd-chip-dismiss", { label });
3516 }
3517 };
3518 _WpdChip.props = [
3519 "label",
3520 "tone",
3521 "size",
3522 "dismissible",
3523 "disabled",
3524 "pending"
3525 ];
3526 _WpdChip.styles = [styles$6];
3527 _WpdChip.help = {
3528 title: "Chip",
3529 summary: "Labelled pill primitive with optional leading icon and trailing dismiss button. Tones mirror <wpd-badge>; pair with <wpd-tag-input> for full add/remove ergonomics.",
3530 status: "experimental",
3531 since: "0.8.0",
3532 props: [
3533 {
3534 name: "label",
3535 type: "string",
3536 description: "Visible text. Falls back to the default slot when omitted."
3537 },
3538 {
3539 name: "tone",
3540 type: "'neutral' | 'accent' | 'positive' | 'warning' | 'danger'",
3541 default: "neutral",
3542 description: "Color variant. Mirrors <wpd-badge> tones."
3543 },
3544 {
3545 name: "size",
3546 type: "'default' | 'compact'",
3547 default: "default",
3548 description: "Vertical density. Compact halves horizontal padding for dense lists."
3549 },
3550 {
3551 name: "dismissible",
3552 type: "boolean attribute",
3553 description: "Renders a trailing × button. Click / Enter / Space emits wpd-chip-dismiss."
3554 },
3555 {
3556 name: "disabled",
3557 type: "boolean attribute",
3558 description: "Visually mutes the chip and blocks the dismiss button. Useful while a parent is mid-update."
3559 },
3560 {
3561 name: "pending",
3562 type: "boolean attribute",
3563 description: "Renders a subtle pulse animation while a REST mutation is in flight. Auto-applied by <wpd-tag-input>; safe to set by hand."
3564 }
3565 ],
3566 slots: [
3567 { name: "(default)", description: "Fallback label when `label` is unset." },
3568 {
3569 name: "icon",
3570 description: "Leading icon (Dashicon, SVG, image). Inherits text color."
3571 }
3572 ],
3573 parts: [
3574 { name: "chip", description: "The pill container." },
3575 {
3576 name: "dismiss",
3577 description: "The trailing × button (when `dismissible`)."
3578 }
3579 ],
3580 events: [
3581 {
3582 name: "wpd-chip-dismiss",
3583 description: "Fires when the dismiss button is activated. Detail carries the chip's label so a delegated listener can act without DOM walking.",
3584 detail: "{ label: string }"
3585 }
3586 ],
3587 cssProps: [
3588 { name: "--wpd-chip-bg", description: "Background color." },
3589 { name: "--wpd-chip-fg", description: "Text color." },
3590 { name: "--wpd-chip-border", description: "Border shorthand." },
3591 {
3592 name: "--wpd-chip-padding",
3593 description: "Padding shorthand.",
3594 default: "2px 8px"
3595 },
3596 {
3597 name: "--wpd-chip-radius",
3598 description: "Corner radius.",
3599 default: "999px"
3600 },
3601 {
3602 name: "--wpd-chip-label-max",
3603 description: "Max width of the inner label before ellipsis.",
3604 default: "220px"
3605 }
3606 ],
3607 example: html`
3608 <wpd-cluster gap="6">
3609 <wpd-chip label="Neutral"></wpd-chip>
3610 <wpd-chip label="Accent" tone="accent"></wpd-chip>
3611 <wpd-chip label="Positive" tone="positive"></wpd-chip>
3612 <wpd-chip label="Warning" tone="warning"></wpd-chip>
3613 <wpd-chip label="Danger" tone="danger"></wpd-chip>
3614 <wpd-chip label="Dismissible" dismissible></wpd-chip>
3615 </wpd-cluster>
3616 `
3617 };
3618 let WpdChip = _WpdChip;
3619 defineComponent("wpd-chip", WpdChip);
3620 function _iconCross$1() {
3621 return html`
3622 <svg
3623 viewBox="0 0 12 12"
3624 width="10"
3625 height="10"
3626 aria-hidden="true"
3627 focusable="false"
3628 fill="none"
3629 stroke="currentColor"
3630 stroke-width="1.5"
3631 stroke-linecap="round"
3632 >
3633 <path d="M3 3 L9 9 M9 3 L3 9" />
3634 </svg>
3635 `;
3636 }
3637 const _WpdTagInput = class _WpdTagInput extends Component {
3638 constructor() {
3639 super(...arguments);
3640 this._value = [];
3641 this._suggestions = [];
3642 this._suggestionsLoading = false;
3643 this._query = "";
3644 this._highlight = -1;
3645 this._focusedChip = -1;
3646 this._onDocumentPointerDown = (e) => {
3647 if (!this.isOpen) {
3648 return;
3649 }
3650 const path = e.composedPath();
3651 if (path.includes(this)) {
3652 return;
3653 }
3654 this.closeInput();
3655 };
3656 }
3657 // Resolves to the inline input AFTER each render. Re-queried on
3658 // every `requestUpdate` because the shadow tree builds fresh
3659 // nodes per render.
3660 get _input() {
3661 const root = this.shadowRoot;
3662 return root ? root.querySelector(".wpd-tag-input__input") : null;
3663 }
3664 // --- Public properties (JS-only) -------------------------------------
3665 get value() {
3666 return this._value;
3667 }
3668 set value(next) {
3669 this._value = Array.isArray(next) ? next.slice() : [];
3670 if (this._focusedChip >= this._value.length) {
3671 this._focusedChip = -1;
3672 }
3673 this.requestUpdate();
3674 }
3675 get suggestions() {
3676 return this._suggestions;
3677 }
3678 set suggestions(next) {
3679 this._suggestions = Array.isArray(next) ? next.slice() : [];
3680 this._highlight = this._suggestions.length > 0 ? 0 : -1;
3681 this._suggestionsLoading = false;
3682 this.requestUpdate();
3683 }
3684 get suggestionsLoading() {
3685 return this._suggestionsLoading;
3686 }
3687 set suggestionsLoading(next) {
3688 this._suggestionsLoading = !!next;
3689 this.requestUpdate();
3690 }
3691 get query() {
3692 return this._query;
3693 }
3694 get isOpen() {
3695 return this.open !== null;
3696 }
3697 /**
3698 * Open the inline input + suggestions popover. Equivalent to
3699 * clicking the "+" trigger. Call from the parent to start tag
3700 * entry programmatically (e.g. paste interception).
3701 */
3702 openInput() {
3703 if (this.isOpen) {
3704 return;
3705 }
3706 this.open = "";
3707 this._query = "";
3708 this._highlight = -1;
3709 this.emit("wpd-tag-open", {});
3710 queueMicrotask(() => {
3711 this._input?.focus();
3712 this._emitSuggest("");
3713 });
3714 }
3715 /**
3716 * Close the inline input. Use from a parent to dismiss after a
3717 * background save resolves.
3718 */
3719 closeInput() {
3720 if (!this.isOpen) {
3721 return;
3722 }
3723 this.open = null;
3724 this._query = "";
3725 this._suggestions = [];
3726 this._highlight = -1;
3727 this._suggestionsLoading = false;
3728 this.emit("wpd-tag-close", {});
3729 this.requestUpdate();
3730 }
3731 // --- Lifecycle --------------------------------------------------------
3732 connectedCallback() {
3733 super.connectedCallback();
3734 document.addEventListener("pointerdown", this._onDocumentPointerDown, true);
3735 }
3736 disconnectedCallback() {
3737 document.removeEventListener("pointerdown", this._onDocumentPointerDown, true);
3738 }
3739 // --- Render -----------------------------------------------------------
3740 render() {
3741 const isOpen = this.isOpen;
3742 const disabled = this.disabled !== null;
3743 const readonly = this.readonly !== null;
3744 const removable = this.removable !== null || this.removable === null && !readonly;
3745 const creatable = this.creatable !== null;
3746 const addLabel = this["add-label"] || "+ Add";
3747 const placeholder = this.placeholder || "Add a tag…";
3748 return html`
3749 <span
3750 class="wpd-tag-input"
3751 role="group"
3752 aria-label=${this.label ?? ""}
3753 >
3754 ${this._renderChips(removable, disabled)}
3755 ${this._renderTrailing({
3756 isOpen,
3757 readonly,
3758 disabled,
3759 placeholder,
3760 creatable,
3761 addLabel
3762 })}
3763 </span>
3764 `;
3765 }
3766 _renderTrailing(opts) {
3767 if (opts.isOpen) {
3768 return this._renderEditor(opts.placeholder, opts.creatable);
3769 }
3770 if (opts.readonly || opts.disabled) {
3771 return html``;
3772 }
3773 return this._renderTrigger(opts.addLabel);
3774 }
3775 _renderChips(removable, disabled) {
3776 const tags = this._value;
3777 if (tags.length === 0) {
3778 return html``;
3779 }
3780 return html`
3781 <span class="wpd-tag-input__chips" role="list">
3782 ${tags.map((tag, idx) => {
3783 const tone = tag.tone ?? "neutral";
3784 return html`
3785 <wpd-chip
3786 role="listitem"
3787 size="compact"
3788 tone=${tone}
3789 label=${tag.label}
3790 ?dismissible=${removable && !disabled}
3791 ?disabled=${disabled}
3792 ?pending=${!!tag.pending}
3793 tabindex=${idx === this._focusedChip ? "0" : "-1"}
3794 data-idx=${String(idx)}
3795 @wpd-chip-dismiss=${(e) => this._onChipDismiss(e, tag)}
3796 @focus=${() => this._focusedChip = idx}
3797 ></wpd-chip>
3798 `;
3799 })}
3800 </span>
3801 `;
3802 }
3803 _renderTrigger(addLabel) {
3804 const disabled = this.disabled !== null;
3805 return html`
3806 <button
3807 type="button"
3808 class="wpd-tag-input__add"
3809 aria-label=${addLabel}
3810 aria-haspopup="listbox"
3811 aria-expanded="false"
3812 ?disabled=${disabled}
3813 @click=${() => this.openInput()}
3814 >
3815 ${_iconPlus()}
3816 <span>${addLabel}</span>
3817 </button>
3818 `;
3819 }
3820 _renderEditor(placeholder, creatable) {
3821 const showSuggestions = this._suggestions.length > 0 || this._suggestionsLoading || creatable && this._query.trim().length > 0;
3822 return html`
3823 <span class="wpd-tag-input__editor">
3824 <input
3825 class="wpd-tag-input__input"
3826 type="text"
3827 autocomplete="off"
3828 autocapitalize="off"
3829 spellcheck="false"
3830 placeholder=${placeholder}
3831 .value=${this._query}
3832 aria-autocomplete="list"
3833 aria-expanded=${showSuggestions ? "true" : "false"}
3834 aria-activedescendant=${this._highlight >= 0 ? `wpd-tag-suggestion-${this._highlight}` : ""}
3835 @input=${(e) => this._onInput(e)}
3836 @keydown=${(e) => this._onInputKeyDown(e)}
3837 @blur=${(e) => this._onInputBlur(e)}
3838 />
3839 ${showSuggestions ? this._renderSuggestions(creatable) : html``}
3840 </span>
3841 `;
3842 }
3843 _renderSuggestions(creatable) {
3844 const trimmed = this._query.trim();
3845 const items = this._suggestions;
3846 const showCreate = creatable && trimmed.length > 0 && !items.some((s) => s.label.toLowerCase() === trimmed.toLowerCase()) && !this._value.some((v) => v.label.toLowerCase() === trimmed.toLowerCase());
3847 return html`
3848 <div
3849 class="wpd-tag-input__suggestions"
3850 role="listbox"
3851 >
3852 ${this._suggestionsLoading ? html`
3853 <div class="wpd-tag-input__suggestion-loading">
3854 <span class="wpd-tag-input__suggestion-spinner" aria-hidden="true"></span>
3855 <span>Searching…</span>
3856 </div>
3857 ` : html``}
3858 ${items.length === 0 && !this._suggestionsLoading && !showCreate ? html`
3859 <div class="wpd-tag-input__suggestion-empty">
3860 ${trimmed.length > 0 ? "No matches." : "Type to search."}
3861 </div>
3862 ` : html``}
3863 ${items.map((item, idx) => {
3864 const selected = idx === this._highlight;
3865 return html`
3866 <div
3867 id=${`wpd-tag-suggestion-${idx}`}
3868 role="option"
3869 aria-selected=${selected ? "true" : "false"}
3870 class="wpd-tag-input__suggestion-item"
3871 @mousedown=${(e) => {
3872 e.preventDefault();
3873 this._addSuggestion(item, false);
3874 }}
3875 @mouseenter=${() => {
3876 this._highlight = idx;
3877 this.requestUpdate();
3878 }}
3879 >
3880 <span>${item.label}</span>
3881 </div>
3882 `;
3883 })}
3884 ${showCreate ? html`
3885 <div
3886 id=${`wpd-tag-suggestion-${items.length}`}
3887 role="option"
3888 aria-selected=${this._highlight === items.length ? "true" : "false"}
3889 class="wpd-tag-input__suggestion-item wpd-tag-input__suggestion-create"
3890 @mousedown=${(e) => {
3891 e.preventDefault();
3892 this._addSuggestion(
3893 { label: trimmed },
3894 true
3895 );
3896 }}
3897 @mouseenter=${() => {
3898 this._highlight = items.length;
3899 this.requestUpdate();
3900 }}
3901 >
3902 Create "${trimmed}"
3903 </div>
3904 ` : html``}
3905 </div>
3906 `;
3907 }
3908 // --- Event handlers ---------------------------------------------------
3909 _onChipDismiss(e, tag) {
3910 e.stopPropagation();
3911 this.emit("wpd-tag-remove", { tag });
3912 }
3913 _onInput(e) {
3914 const value = e.target.value;
3915 this._query = value;
3916 this._emitSuggest(value);
3917 }
3918 _emitSuggest(query) {
3919 const minQuery = parseInt(
3920 this["min-query"] || "0",
3921 10
3922 ) || 0;
3923 if (query.length < minQuery) {
3924 this._suggestions = [];
3925 this._suggestionsLoading = false;
3926 this.requestUpdate();
3927 return;
3928 }
3929 this._suggestionsLoading = true;
3930 this.requestUpdate();
3931 this.emit("wpd-tag-suggest", { query });
3932 }
3933 _onInputKeyDown(e) {
3934 const creatable = this.creatable !== null;
3935 const items = this._suggestions;
3936 const trimmed = this._query.trim();
3937 const showCreate = creatable && trimmed.length > 0 && !items.some((s) => s.label.toLowerCase() === trimmed.toLowerCase()) && !this._value.some((v) => v.label.toLowerCase() === trimmed.toLowerCase());
3938 const totalSelectable = items.length + (showCreate ? 1 : 0);
3939 switch (e.key) {
3940 case "ArrowDown": {
3941 if (totalSelectable === 0) {
3942 return;
3943 }
3944 e.preventDefault();
3945 this._highlight = this._highlight + 1 >= totalSelectable ? 0 : this._highlight + 1;
3946 this.requestUpdate();
3947 return;
3948 }
3949 case "ArrowUp": {
3950 if (totalSelectable === 0) {
3951 return;
3952 }
3953 e.preventDefault();
3954 this._highlight = this._highlight <= 0 ? totalSelectable - 1 : this._highlight - 1;
3955 this.requestUpdate();
3956 return;
3957 }
3958 case "Enter": {
3959 e.preventDefault();
3960 if (this._highlight >= 0 && this._highlight < items.length) {
3961 this._addSuggestion(items[this._highlight], false);
3962 return;
3963 }
3964 if (this._highlight === items.length && showCreate) {
3965 this._addSuggestion({ label: trimmed }, true);
3966 return;
3967 }
3968 if (showCreate && trimmed.length > 0) {
3969 this._addSuggestion({ label: trimmed }, true);
3970 return;
3971 }
3972 return;
3973 }
3974 case "Escape": {
3975 e.preventDefault();
3976 this.closeInput();
3977 return;
3978 }
3979 case "Backspace": {
3980 if (this._query === "" && this._value.length > 0) {
3981 e.preventDefault();
3982 const lastIdx = this._value.length - 1;
3983 if (this._focusedChip === lastIdx) {
3984 this.emit("wpd-tag-remove", {
3985 tag: this._value[lastIdx]
3986 });
3987 this._focusedChip = -1;
3988 } else {
3989 this._focusedChip = lastIdx;
3990 this.requestUpdate();
3991 }
3992 }
3993 return;
3994 }
3995 default:
3996 if (this._focusedChip !== -1) {
3997 this._focusedChip = -1;
3998 }
3999 }
4000 }
4001 _onInputBlur(_e) {
4002 queueMicrotask(() => {
4003 if (!this.shadowRoot?.activeElement) {
4004 this.closeInput();
4005 }
4006 });
4007 }
4008 _addSuggestion(tag, isNew) {
4009 const exists = this._value.some(
4010 (v) => v.label.toLowerCase() === tag.label.toLowerCase()
4011 );
4012 if (exists) {
4013 this._query = "";
4014 this._highlight = -1;
4015 this._suggestions = [];
4016 this.requestUpdate();
4017 this._input?.focus();
4018 return;
4019 }
4020 this.emit("wpd-tag-add", { tag, isNew });
4021 this._query = "";
4022 this._highlight = -1;
4023 this._suggestions = [];
4024 this._suggestionsLoading = false;
4025 this.requestUpdate();
4026 queueMicrotask(() => {
4027 this._input?.focus();
4028 });
4029 }
4030 };
4031 _WpdTagInput.props = [
4032 "label",
4033 "placeholder",
4034 "add-label",
4035 "creatable",
4036 "removable",
4037 "disabled",
4038 "readonly",
4039 "size",
4040 "min-query",
4041 "open"
4042 ];
4043 _WpdTagInput.styles = [styles$7];
4044 _WpdTagInput.help = {
4045 title: "Tag input",
4046 summary: "Multi-tag picker with autocomplete and free-form creation. Purely presentational — emits wpd-tag-suggest / wpd-tag-add / wpd-tag-remove and lets the consumer drive REST + optimistic UI.",
4047 status: "experimental",
4048 since: "0.8.0",
4049 props: [
4050 {
4051 name: "label",
4052 type: "string",
4053 description: "Accessible label for the inline input."
4054 },
4055 {
4056 name: "placeholder",
4057 type: "string",
4058 description: "Native placeholder for the inline input.",
4059 default: "Add a tag…"
4060 },
4061 {
4062 name: "add-label",
4063 type: "string",
4064 description: 'Label of the "+" trigger button.',
4065 default: "+ Add"
4066 },
4067 {
4068 name: "creatable",
4069 type: "boolean attribute",
4070 description: "Allow Enter on a non-matching query to emit `wpd-tag-add` with `isNew: true`. Off by default — opt in for taxonomies the user is allowed to extend."
4071 },
4072 {
4073 name: "removable",
4074 type: "boolean attribute",
4075 description: "Show × on every chip and emit `wpd-tag-remove` on click. On by default; switch off for read-only views."
4076 },
4077 {
4078 name: "disabled",
4079 type: "boolean attribute",
4080 description: "Disables the entire control. Chips render but the trigger / input / dismiss buttons are inert."
4081 },
4082 {
4083 name: "readonly",
4084 type: "boolean attribute",
4085 description: 'Hides the "+" trigger and chip × buttons. Same as setting `creatable=false` and `removable=false` together.'
4086 },
4087 {
4088 name: "size",
4089 type: "'default' | 'compact'",
4090 default: "default",
4091 description: "Density preset. Compact suits dense table cells."
4092 },
4093 {
4094 name: "min-query",
4095 type: "integer (string)",
4096 default: "0",
4097 description: "Minimum query length before `wpd-tag-suggest` fires. Set to 1 or 2 for taxonomies with thousands of terms."
4098 },
4099 {
4100 name: "open",
4101 type: "boolean attribute",
4102 description: "Two-way reflected: present while the inline input is showing. Setting it externally opens / closes the picker."
4103 }
4104 ],
4105 events: [
4106 {
4107 name: "wpd-tag-suggest",
4108 description: "Fires when the user types in the input. Consumer fetches suggestions and assigns them back via `el.suggestions = […]`.",
4109 detail: "{ query: string }"
4110 },
4111 {
4112 name: "wpd-tag-add",
4113 description: "Fires when the user picks a suggestion or, with `creatable`, presses Enter on a free-form value. Consumer mutates `value`.",
4114 detail: "{ tag: WpdTagItem; isNew: boolean }"
4115 },
4116 {
4117 name: "wpd-tag-remove",
4118 description: "Fires when × on a chip is activated. Consumer mutates `value`.",
4119 detail: "{ tag: WpdTagItem }"
4120 },
4121 {
4122 name: "wpd-tag-open",
4123 description: "Fires when the inline input opens.",
4124 detail: "{}"
4125 },
4126 {
4127 name: "wpd-tag-close",
4128 description: "Fires when the inline input closes.",
4129 detail: "{}"
4130 }
4131 ],
4132 cssProps: [
4133 {
4134 name: "--wpd-tag-input-gap",
4135 description: "Gap between chips / between chips and trigger.",
4136 default: "4px"
4137 },
4138 {
4139 name: "--wpd-tag-input-padding",
4140 description: "Padding around the chip row.",
4141 default: "2px"
4142 },
4143 {
4144 name: "--wpd-tag-input-add-fg",
4145 description: 'Foreground color of the "+ Add" trigger.'
4146 },
4147 { name: "--wpd-tag-input-pop-bg", description: "Suggestions popover background." }
4148 ],
4149 example: html`
4150 <wpd-tag-input
4151 label="Tags"
4152 placeholder="Add a tag…"
4153 creatable
4154 ></wpd-tag-input>
4155 `
4156 };
4157 let WpdTagInput = _WpdTagInput;
4158 defineComponent("wpd-tag-input", WpdTagInput);
4159 function _iconPlus() {
4160 return html`
4161 <svg
4162 viewBox="0 0 12 12"
4163 width="9"
4164 height="9"
4165 aria-hidden="true"
4166 focusable="false"
4167 fill="none"
4168 stroke="currentColor"
4169 stroke-width="2"
4170 stroke-linecap="round"
4171 >
4172 <path d="M6 2 L6 10 M2 6 L10 6" />
4173 </svg>
4174 `;
4175 }
4176 const styles$5 = css`:host{display:flex;width:100%;min-width:0;max-width:100%;align-items:stretch}.wpd-cat{display:flex;flex-direction:column;align-items:stretch;gap:4px;padding:var( --wpd-cat-padding,2px );min-height:24px;max-width:100%;width:100%}.wpd-cat__chips{display:inline-flex;flex-wrap:wrap;align-items:center;gap:var( --wpd-cat-gap,4px );min-width:0}.wpd-cat__chains{display:flex;flex-wrap:wrap;gap:4px;min-width:0}.wpd-cat__viz-host{display:flex;align-items:center;gap:4px;min-width:0;flex:1 1 auto;max-width:100%;min-height:28px;cursor:pointer;position:relative}.wpd-cat__viz-svg{display:block;width:100%;max-width:100%;min-width:0;overflow:visible;flex:1 1 auto}.wpd-cat__viz-svg .wpd-cat-edge{fill:none;stroke:var( --wpd-cat-edge-color,currentColor );stroke-width:1.25;stroke-linecap:round;opacity:0.55;transition:stroke-width 0.18s ease,opacity 0.18s ease}.wpd-cat__viz-svg .wpd-cat-edge[ data-active='true' ]{stroke-width:2;opacity:1}.wpd-cat__viz-svg .wpd-cat-node{cursor:pointer;transition:r 0.2s cubic-bezier( 0.34,1.56,0.64,1 ),fill 0.18s ease,stroke-width 0.18s ease,filter 0.18s ease}.wpd-cat__viz-svg .wpd-cat-node[ data-selected='true' ]{filter:drop-shadow( 0 0 6px var( --wpd-cat-node-glow,rgba( 0,0,0,0.18 ) ) )}.wpd-cat__viz-svg .wpd-cat-node:hover,.wpd-cat__viz-svg .wpd-cat-node:focus-visible{filter:drop-shadow( 0 0 8px var( --wpd-cat-node-glow,rgba( 0,0,0,0.3 ) ) );outline:none}.wpd-cat__viz-svg .wpd-cat-label{font-family:var( --wpd-font,system-ui,sans-serif );font-size:10.5px;fill:var( --wpd-cat-label-fg,#1d2327 );font-weight:500;pointer-events:none;user-select:none}.wpd-cat__viz-svg .wpd-cat-label[ data-selected='false' ]{fill:var( --wpd-cat-label-muted,#8c8f94 );font-weight:400;font-style:italic}.wpd-cat__trigger{appearance:none;display:inline-flex;align-items:center;gap:4px;padding:1px 8px;min-height:22px;font:inherit;font-size:11px;font-weight:500;line-height:1;color:var( --wpd-cat-trigger-fg,#50575e );background:transparent;border:1px dashed var( --wpd-cat-trigger-border,#c3c4c7 );border-radius:999px;cursor:pointer;transition:background-color 0.12s ease,color 0.12s ease,border-color 0.12s ease}.wpd-cat__trigger:hover:not(:disabled ){background:rgba( 0,0,0,0.04 );color:var( --wpd-cat-trigger-fg-hover,#1d2327 );border-color:var( --wpd-cat-trigger-border-hover,#8c8f94 )}.wpd-cat__trigger:focus-visible{outline:none;border-style:solid;box-shadow:0 0 0 2px var( --wp-admin-theme-color,#2271b1 )}.wpd-cat__trigger svg{display:block}.wpd-cat__uncategorized{display:inline-flex;align-items:center;gap:4px;padding:1px 10px;min-height:22px;font-size:11px;font-weight:500;line-height:1.6;color:var( --wpd-cat-uncat-fg,#8c8f94 );background:transparent;border:1px dashed var( --wpd-cat-uncat-border,#c3c4c7 );border-radius:999px;font-style:italic}.wpd-cat__popover{position:fixed;top:0;left:0;min-width:280px;max-width:360px;max-height:360px;display:flex;flex-direction:column;background:var( --wpd-cat-pop-bg,#fff );color:var( --wpd-cat-pop-fg,#1d2327 );border:1px solid var( --wpd-cat-pop-border,#c3c4c7 );border-radius:8px;box-shadow:0 6px 16px rgba( 0,0,0,0.08 ),0 1px 2px rgba( 0,0,0,0.06 );z-index:1000}.wpd-cat__editor{position:relative;display:inline-flex;align-items:center}.wpd-cat__search{appearance:none;font:inherit;font-size:13px;padding:8px 12px;border:0;border-bottom:1px solid var( --wpd-cat-pop-divider,#f0f0f1 );background:transparent;color:inherit;outline:none}.wpd-cat__search:focus{border-bottom-color:var( --wp-admin-theme-color,#2271b1 )}.wpd-cat__tree{flex:1 1 auto;min-height:0;overflow-y:auto;padding:4px 0}.wpd-cat__row-block{display:contents}.wpd-cat__row{display:flex;align-items:center;gap:6px;padding:4px 8px 4px var( --wpd-cat-row-indent,12px );cursor:pointer;user-select:none;font-size:13px;line-height:1.4;position:relative}.wpd-cat__row:hover,.wpd-cat__row[ data-focused='true' ]{background:rgba( 0,0,0,0.04 )}.wpd-cat__row[ data-selected='true' ]{color:var( --wp-admin-theme-color,#2271b1 );font-weight:600}.wpd-cat__row::before{content:'';position:absolute;left:0;top:0;bottom:0;width:var( --wpd-cat-guide-width,0px );border-left:1px dotted var( --wpd-cat-guide-color,rgba( 0,0,0,0.08 ) )}.wpd-cat__expander{display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;border:0;background:transparent;color:inherit;cursor:pointer;flex-shrink:0;opacity:0.65}.wpd-cat__expander:hover{opacity:1}.wpd-cat__expander svg{display:block;transition:transform 0.12s ease}.wpd-cat__row[ data-expanded='true' ] .wpd-cat__expander svg{transform:rotate( 90deg )}.wpd-cat__expander--placeholder{visibility:hidden}.wpd-cat__create-row{display:flex;align-items:center;padding:2px 8px 2px var( --wpd-cat-row-indent,28px );position:relative}.wpd-cat__create-row::before{content:'';position:absolute;left:0;top:0;bottom:0;width:var( --wpd-cat-guide-width,0px );border-left:1px dotted var( --wpd-cat-guide-color,rgba( 0,0,0,0.08 ) )}.wpd-cat__create-wrap{display:inline-flex;align-items:center;flex:1 1 auto;min-width:0;gap:4px;padding:1px 1px 1px 0;border:1px solid transparent;border-radius:6px;background:transparent;transition:border-color 0.12s ease,background-color 0.12s ease,box-shadow 0.12s ease}.wpd-cat__create-wrap:hover{border-color:rgba( 0,0,0,0.12 );background:var( --wpd-cat-pop-bg,#fff )}.wpd-cat__create-wrap:focus-within{border-color:var( --wp-admin-theme-color,#2271b1 );background:var( --wpd-cat-pop-bg,#fff );box-shadow:0 0 0 2px color-mix( in srgb,var( --wp-admin-theme-color,#2271b1 ) 15%,transparent )}.wpd-cat__create-input{flex:1 1 auto;min-width:0;appearance:none;font:inherit;font-size:12px;padding:3px 6px;border:0;background:transparent;color:inherit;outline:none}.wpd-cat__create-input::placeholder{color:var( --wpd-cat-pop-muted,#8c8f94 );font-style:italic;opacity:1}.wpd-cat__create-submit{appearance:none;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;flex-shrink:0;padding:0;border:0;border-radius:4px;background:transparent;color:var( --wpd-cat-pop-muted,#8c8f94 );cursor:pointer;transition:background-color 0.12s ease,color 0.12s ease}.wpd-cat__create-wrap:focus-within .wpd-cat__create-submit:not( [ disabled ] ){background:var( --wp-admin-theme-color,#2271b1 );color:#fff}.wpd-cat__create-submit:hover:not( [ disabled ] ){filter:brightness( 1.05 )}.wpd-cat__create-submit[ disabled ]{cursor:default;opacity:0.5}.wpd-cat__create-submit svg{display:block;width:11px;height:11px}.wpd-cat__create-spinner{display:inline-block;width:12px;height:12px;margin:0 4px;border-radius:50%;border:2px solid var( --wp-admin-theme-color,#2271b1 );border-top-color:transparent;animation:wpd-cat-spin 0.8s linear infinite;flex-shrink:0}.wpd-cat__check{display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;flex-shrink:0;border:1.5px solid var( --wpd-cat-check-border,#8c8f94 );border-radius:3px;color:transparent;transition:background-color 0.12s ease,border-color 0.12s ease,color 0.12s ease}.wpd-cat__row[ data-selected='true' ] .wpd-cat__check{background:var( --wp-admin-theme-color,#2271b1 );border-color:var( --wp-admin-theme-color,#2271b1 );color:#fff}.wpd-cat__check svg{display:block;width:10px;height:10px}.wpd-cat__label{min-width:0;flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wpd-cat__delete{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:18px;height:18px;border:0;border-radius:50%;padding:0;background:transparent;color:var( --wpd-cat-delete-color,#d63638 );cursor:pointer;opacity:0;transition:opacity 0.12s ease,background-color 0.12s ease}.wpd-cat__row:hover .wpd-cat__delete,.wpd-cat__row[ data-focused='true' ] .wpd-cat__delete,.wpd-cat__delete:focus-visible{opacity:1}.wpd-cat__delete:hover,.wpd-cat__delete:focus-visible{background:rgba( 214,54,56,0.12 )}.wpd-cat__delete svg{display:block;width:10px;height:10px}.wpd-cat__match{background:rgba( 252,211,77,0.45 );border-radius:2px;padding:0 1px}.wpd-cat__empty,.wpd-cat__loading{padding:12px;font-size:12px;color:var( --wpd-cat-pop-muted,#646970 );text-align:center}.wpd-cat__loading-spinner{display:inline-block;width:12px;height:12px;border-radius:50%;border:2px solid currentColor;border-top-color:transparent;animation:wpd-cat-spin 0.8s linear infinite;margin-inline-end:8px;vertical-align:middle}@keyframes wpd-cat-spin{to{transform:rotate( 360deg )}}.wpd-cat__footer{padding:8px 12px;font-size:11px;color:var( --wpd-cat-pop-muted,#646970 );border-top:1px solid var( --wpd-cat-pop-divider,#f0f0f1 );border-radius:10px;background:var( --wpd-cat-pop-footer-bg,#fafafb );display:flex;align-items:center;gap:6px;line-height:1.4}.wpd-cat__footer .dashicons{font-size:14px;width:14px;height:14px;flex-shrink:0}:host( [ disabled ] ) .wpd-cat{opacity:0.6;pointer-events:none}`;
4177 const CHEVRON_W = "10px";
4178 const styles$4 = css`:host{display:inline-flex;max-width:100%;align-items:center;min-width:0;font-family:var( --wpd-font,system-ui,sans-serif );font-size:12px;line-height:1;font-weight:500}.wpd-crumb-chain{display:inline-flex;flex-wrap:nowrap;align-items:stretch;max-width:100%;min-width:0;min-height:22px;border-radius:999px;overflow:hidden;filter:drop-shadow( 0 1px 1px rgba( 0,0,0,0.06 ) )}.wpd-crumb{display:inline-flex;align-items:center;justify-content:center;gap:5px;min-height:22px;padding:2px 12px;background:var( --wpd-crumb-bg,#c3c4c7 );color:var( --wpd-crumb-fg,#1d2327 );text-align:center;min-width:0;max-width:100%;flex-shrink:1;font-size:12px;font-weight:500;letter-spacing:0.01em;white-space:nowrap;cursor:grab;transition:filter 0.15s ease,transform 0.15s ease,background-color 0.12s ease}.wpd-crumb:active{cursor:grabbing}.wpd-crumb:hover{filter:brightness( 1.06 )}.wpd-crumb__remove{cursor:pointer}.wpd-crumb--first{padding-inline-end:22px;clip-path:polygon( 0 0,calc( 100% - ${CHEVRON_W} ) 0,100% 50%,calc( 100% - ${CHEVRON_W} ) 100%,0 100% )}.wpd-crumb--middle{padding-inline:22px;margin-inline-start:calc( -1 * ${CHEVRON_W} );clip-path:polygon( ${CHEVRON_W} 0,calc( 100% - ${CHEVRON_W} ) 0,100% 50%,calc( 100% - ${CHEVRON_W} ) 100%,${CHEVRON_W} 100%,0 50% )}.wpd-crumb--last{padding-inline-start:22px;padding-inline-end:14px;margin-inline-start:calc( -1 * ${CHEVRON_W} );clip-path:polygon( ${CHEVRON_W} 0,100% 0,100% 100%,${CHEVRON_W} 100%,0 50% )}.wpd-crumb--solo{padding:2px 12px;border-radius:999px}.wpd-crumb__label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:1.4}.wpd-crumb__remove{appearance:none;display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;flex-shrink:0;padding:0;border:0;border-radius:50%;background:transparent;color:inherit;cursor:pointer;opacity:0.65;transition:opacity 0.12s ease,background-color 0.12s ease,transform 0.12s ease}.wpd-crumb__remove:hover,.wpd-crumb__remove:focus-visible{opacity:1;background:rgba( 0,0,0,0.22 );outline:none;transform:scale( 1.1 )}.wpd-crumb__remove svg{display:block;width:8px;height:8px}.wpd-crumb-chain:hover{filter:drop-shadow( 0 2px 3px rgba( 0,0,0,0.12 ) )}:host( [ disabled ] ) .wpd-crumb-chain{opacity:0.55;pointer-events:none}`;
4179 var __freeze = Object.freeze;
4180 var __defProp = Object.defineProperty;
4181 var __template = (cooked, raw) => __freeze(__defProp(cooked, "raw", { value: __freeze(cooked.slice()) }));
4182 var _a;
4183 const _WpdCrumbChain = class _WpdCrumbChain extends Component {
4184 constructor() {
4185 super(...arguments);
4186 this._segments = [];
4187 }
4188 get segments() {
4189 return this._segments;
4190 }
4191 set segments(next) {
4192 this._segments = Array.isArray(next) ? next.slice() : [];
4193 this.requestUpdate();
4194 }
4195 render() {
4196 const removable = this.removable !== null;
4197 const segments = this._segments;
4198 if (segments.length === 0) {
4199 return html``;
4200 }
4201 return html`
4202 <div class="wpd-crumb-chain" role="group">
4203 ${segments.map((seg, idx) => {
4204 const variant = pickVariant(idx, segments.length);
4205 const bg = seg.color ?? "rgba( 0, 0, 0, 0.08 )";
4206 const fg = pickForegroundColor(bg);
4207 const styleStr = `--wpd-crumb-bg: ${bg}; --wpd-crumb-fg: ${fg};`;
4208 return html`
4209 <span
4210 class=${`wpd-crumb wpd-crumb--${variant}`}
4211 style=${styleStr}
4212 title=${seg.name}
4213 draggable="true"
4214 @click=${(e) => this._onSegmentClick(e, idx, seg)}
4215 @dragstart=${(e) => this._onSegmentDragStart(e, idx, seg)}
4216 >
4217 <span class="wpd-crumb__label">${seg.name}</span>
4218 ${removable ? html`
4219 <button
4220 type="button"
4221 class="wpd-crumb__remove"
4222 aria-label=${`Remove ${seg.name}`}
4223 draggable="false"
4224 @click=${(e) => this._onRemove(e, idx, seg)}
4225 >${_iconCross()}</button>
4226 ` : html``}
4227 </span>
4228 `;
4229 })}
4230 </div>
4231 `;
4232 }
4233 _onSegmentDragStart(e, index, segment) {
4234 const target = e.target;
4235 if (target?.closest(".wpd-crumb__remove")) {
4236 e.preventDefault();
4237 return;
4238 }
4239 const dragSegments = this._segments.slice(index);
4240 if (e.dataTransfer) {
4241 const ghost = buildDragGhost(dragSegments);
4242 document.body.appendChild(ghost);
4243 const rect = e.currentTarget?.getBoundingClientRect();
4244 const offsetX = rect ? Math.min(30, rect.width / 2) : 16;
4245 const offsetY = rect ? Math.min(16, rect.height / 2) : 12;
4246 e.dataTransfer.setDragImage(ghost, offsetX, offsetY);
4247 requestAnimationFrame(() => ghost.remove());
4248 }
4249 this.emit("wpd-chain-segment-dragstart", {
4250 index,
4251 id: segment.id,
4252 segment,
4253 segments: dragSegments,
4254 dragEvent: e
4255 });
4256 }
4257 _onSegmentClick(e, index, segment) {
4258 const target = e.target;
4259 if (target?.closest(".wpd-crumb__remove")) {
4260 return;
4261 }
4262 this.emit("wpd-chain-segment-click", {
4263 index,
4264 id: segment.id,
4265 segment
4266 });
4267 }
4268 _onRemove(e, index, segment) {
4269 e.stopPropagation();
4270 this.emit("wpd-chain-remove", { index, id: segment.id, segment });
4271 }
4272 };
4273 _WpdCrumbChain.props = ["removable", "disabled"];
4274 _WpdCrumbChain.styles = [styles$4];
4275 _WpdCrumbChain.help = {
4276 title: "Crumb chain",
4277 summary: "Chevron-interlocking breadcrumb. Segments slot together like puzzle pieces, with each segment in its own color so the eye reads root → leaf as a single merged path. Reusable for any parent → child → grandchild relationship.",
4278 status: "experimental",
4279 since: "0.8.0",
4280 props: [
4281 {
4282 name: "removable",
4283 type: "boolean attribute",
4284 description: "Show an × on every segment. Activating it emits `wpd-chain-remove` with the clicked segment + index — consumers cascade the removal down the chain (segment + descendants)."
4285 },
4286 {
4287 name: "disabled",
4288 type: "boolean attribute",
4289 description: "Visually mute the chain and ignore pointer + keyboard input."
4290 }
4291 ],
4292 events: [
4293 {
4294 name: "wpd-chain-remove",
4295 description: "Fires when × on ANY segment is activated. Detail carries the clicked segment + its index. Consumers typically delete the segment AND every descendant in the chain (mirrors the drag semantic, where the same gesture would carry the same set of ids).",
4296 detail: "{ index: number; id?: number | string; segment: WpdCrumbSegment }"
4297 },
4298 {
4299 name: "wpd-chain-segment-click",
4300 description: 'Fires when ANY segment is clicked. Useful for navigation drills (click "Tech" to filter to Tech).',
4301 detail: "{ index: number; id?: number | string; segment: WpdCrumbSegment }"
4302 },
4303 {
4304 name: "wpd-chain-segment-dragstart",
4305 description: 'Fires when a drag begins from any segment OTHER than the × remove button. Detail carries the segments from the drag-source to the leaf so consumers can ship ids for "this branch" — a drag from the middle segment moves the segment + every descendant in the chain.',
4306 detail: "{ index: number; id?: number | string; segment: WpdCrumbSegment; segments: WpdCrumbSegment[]; dragEvent: DragEvent }"
4307 }
4308 ],
4309 example: html(_a || (_a = __template([`
4310 <wpd-crumb-chain id="example-chain" removable></wpd-crumb-chain>
4311 <script>
4312 document.getElementById( 'example-chain' ).segments = [
4313 { id: 1, name: 'Tech', color: '#2271b1' },
4314 { id: 2, name: 'Web Dev', color: '#3a8ed4' },
4315 { id: 3, name: 'Frontend', color: '#5cb0ff' },
4316 ];
4317 <\/script>
4318 `])))
4319 };
4320 let WpdCrumbChain = _WpdCrumbChain;
4321 defineComponent("wpd-crumb-chain", WpdCrumbChain);
4322 const DRAG_GHOST_CHEVRON = 10;
4323 function buildDragGhost(segments) {
4324 const wrap = document.createElement("div");
4325 wrap.style.cssText = [
4326 "display: inline-flex",
4327 "align-items: stretch",
4328 "border-radius: 999px",
4329 "overflow: hidden",
4330 "filter: drop-shadow( 0 1px 2px rgba( 0, 0, 0, 0.18 ) )",
4331 'font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
4332 "font-size: 12px",
4333 "line-height: 1",
4334 "font-weight: 500",
4335 // Position offscreen but rendered — display:none / visibility:
4336 // hidden produce a blank drag-image snapshot.
4337 "position: fixed",
4338 "top: -10000px",
4339 "left: -10000px",
4340 "pointer-events: none",
4341 "z-index: 2147483647"
4342 ].join("; ");
4343 const total = segments.length;
4344 segments.forEach((seg, idx) => {
4345 const span = document.createElement("span");
4346 const bg = seg.color ?? "rgba( 0, 0, 0, 0.08 )";
4347 const fg = pickForegroundColor(bg);
4348 const variant = pickVariant(idx, total);
4349 const styleParts = [
4350 "display: inline-flex",
4351 "align-items: center",
4352 "justify-content: center",
4353 "min-height: 22px",
4354 `background: ${bg}`,
4355 `color: ${fg}`,
4356 "white-space: nowrap",
4357 "box-sizing: border-box",
4358 "letter-spacing: 0.01em"
4359 ];
4360 const c = DRAG_GHOST_CHEVRON;
4361 if (variant === "solo") {
4362 styleParts.push("padding: 2px 12px", "border-radius: 999px");
4363 } else if (variant === "first") {
4364 styleParts.push(
4365 "padding: 2px 22px 2px 12px",
4366 `clip-path: polygon( 0 0, calc( 100% - ${c}px ) 0, 100% 50%, calc( 100% - ${c}px ) 100%, 0 100% )`
4367 );
4368 } else if (variant === "middle") {
4369 styleParts.push(
4370 "padding: 2px 22px",
4371 `margin-inline-start: -${c}px`,
4372 `clip-path: polygon( ${c}px 0, calc( 100% - ${c}px ) 0, 100% 50%, calc( 100% - ${c}px ) 100%, ${c}px 100%, 0 50% )`
4373 );
4374 } else {
4375 styleParts.push(
4376 "padding: 2px 14px 2px 22px",
4377 `margin-inline-start: -${c}px`,
4378 `clip-path: polygon( ${c}px 0, 100% 0, 100% 100%, ${c}px 100%, 0 50% )`
4379 );
4380 }
4381 span.style.cssText = styleParts.join("; ");
4382 span.textContent = seg.name;
4383 wrap.appendChild(span);
4384 });
4385 return wrap;
4386 }
4387 function pickVariant(index, total) {
4388 if (total === 1) {
4389 return "solo";
4390 }
4391 if (index === 0) {
4392 return "first";
4393 }
4394 if (index === total - 1) {
4395 return "last";
4396 }
4397 return "middle";
4398 }
4399 let _readbackCanvas = null;
4400 function pickForegroundColor(bg) {
4401 if (!_readbackCanvas) {
4402 _readbackCanvas = document.createElement("canvas");
4403 _readbackCanvas.width = 1;
4404 _readbackCanvas.height = 1;
4405 }
4406 const ctx = _readbackCanvas.getContext("2d", { willReadFrequently: true });
4407 if (!ctx) {
4408 return "#1d2327";
4409 }
4410 try {
4411 ctx.clearRect(0, 0, 1, 1);
4412 ctx.fillStyle = bg;
4413 ctx.fillRect(0, 0, 1, 1);
4414 const data = ctx.getImageData(0, 0, 1, 1).data;
4415 const a = data[3] / 255;
4416 const r = data[0] * a + 255 * (1 - a);
4417 const g = data[1] * a + 255 * (1 - a);
4418 const b = data[2] * a + 255 * (1 - a);
4419 const lin = (c) => {
4420 const v = c / 255;
4421 return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
4422 };
4423 const L = 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
4424 return L > 0.55 ? "#1d2327" : "#fff";
4425 } catch {
4426 return "#1d2327";
4427 }
4428 }
4429 function _iconCross() {
4430 return html`
4431 <svg
4432 viewBox="0 0 12 12"
4433 aria-hidden="true"
4434 focusable="false"
4435 fill="none"
4436 stroke="currentColor"
4437 stroke-width="2"
4438 stroke-linecap="round"
4439 >
4440 <path d="M3 3 L9 9 M9 3 L3 9" />
4441 </svg>
4442 `;
4443 }
4444 const UNCATEGORIZED_SLUG = "uncategorized";
4445 const UNCATEGORIZED_DEFAULT_ID = 1;
4446 function _isUncategorized(item) {
4447 if (item.id === UNCATEGORIZED_DEFAULT_ID) {
4448 return true;
4449 }
4450 return (item.name || "").toLowerCase() === UNCATEGORIZED_SLUG;
4451 }
4452 const _WpdCategoryPicker = class _WpdCategoryPicker extends Component {
4453 constructor() {
4454 super(...arguments);
4455 this._items = [];
4456 this._value = [];
4457 this._query = "";
4458 this._collapsed = /* @__PURE__ */ new Set();
4459 this._focusedRow = -1;
4460 this._creatingValues = /* @__PURE__ */ new Map();
4461 this._creatingPending = /* @__PURE__ */ new Set();
4462 this._onCellClick = (e) => {
4463 const target = e.target;
4464 if (target?.closest(".wpd-cat-node")) {
4465 return;
4466 }
4467 if (this.isOpen) {
4468 return;
4469 }
4470 const disabled = this.disabled !== null;
4471 const readonly = this.readonly !== null;
4472 if (disabled || readonly) {
4473 return;
4474 }
4475 this.openPicker();
4476 };
4477 this._onDocPointerDown = (e) => {
4478 if (!this.isOpen) {
4479 return;
4480 }
4481 const path = e.composedPath();
4482 if (path.includes(this)) {
4483 return;
4484 }
4485 this.closePicker();
4486 };
4487 this._onLayoutChange = () => {
4488 if (!this.isOpen) {
4489 return;
4490 }
4491 this.closePicker();
4492 };
4493 this._onDocKeydown = (e) => {
4494 if (this.isOpen && e.key === "Escape") {
4495 e.preventDefault();
4496 this.closePicker();
4497 }
4498 };
4499 }
4500 get items() {
4501 return this._items;
4502 }
4503 set items(next) {
4504 this._items = Array.isArray(next) ? next.slice() : [];
4505 this.requestUpdate();
4506 }
4507 get value() {
4508 return this._value;
4509 }
4510 set value(next) {
4511 this._value = Array.isArray(next) ? next.slice() : [];
4512 this.requestUpdate();
4513 }
4514 get isOpen() {
4515 return this.open !== null;
4516 }
4517 openPicker() {
4518 if (this.isOpen) {
4519 return;
4520 }
4521 this.open = "";
4522 this._query = "";
4523 this._focusedRow = 0;
4524 this.emit("wpd-categories-open", {});
4525 queueMicrotask(() => {
4526 this._positionPopover();
4527 this._searchInput?.focus();
4528 });
4529 }
4530 closePicker() {
4531 if (!this.isOpen) {
4532 return;
4533 }
4534 this.open = null;
4535 this._query = "";
4536 this._focusedRow = -1;
4537 this.emit("wpd-categories-close", {});
4538 this.requestUpdate();
4539 }
4540 connectedCallback() {
4541 super.connectedCallback();
4542 document.addEventListener("pointerdown", this._onDocPointerDown, true);
4543 document.addEventListener("keydown", this._onDocKeydown, true);
4544 window.addEventListener("resize", this._onLayoutChange, { passive: true });
4545 window.addEventListener("scroll", this._onLayoutChange, {
4546 passive: true,
4547 capture: true
4548 });
4549 }
4550 disconnectedCallback() {
4551 document.removeEventListener("pointerdown", this._onDocPointerDown, true);
4552 document.removeEventListener("keydown", this._onDocKeydown, true);
4553 window.removeEventListener("resize", this._onLayoutChange);
4554 window.removeEventListener("scroll", this._onLayoutChange, { capture: true });
4555 }
4556 get _searchInput() {
4557 return this.shadowRoot?.querySelector(".wpd-cat__search") ?? null;
4558 }
4559 // --- Render -----------------------------------------------------------
4560 render() {
4561 const isOpen = this.isOpen;
4562 const disabled = this.disabled !== null;
4563 const readonly = this.readonly !== null;
4564 const loading = this.loading !== null;
4565 const addLabel = this["add-label"] || "Categorize";
4566 const placeholder = this.placeholder || "Search categories…";
4567 const maxVisible = Math.max(
4568 0,
4569 parseInt(
4570 this["max-visible"] || "2",
4571 10
4572 ) || 2
4573 );
4574 return html`
4575 <span class="wpd-cat" role="group">
4576 ${this._renderChipRow(maxVisible, readonly, disabled, addLabel)}
4577 ${isOpen ? this._renderPopover(placeholder, loading) : html``}
4578 </span>
4579 `;
4580 }
4581 _renderChipRow(_maxVisible, readonly, disabled, _addLabel) {
4582 const selectedItems = this._selectedItemsInOrder();
4583 if (selectedItems.length === 0) {
4584 return html`
4585 <span class="wpd-cat__chips" role="list">
4586 <span
4587 class="wpd-cat__uncategorized"
4588 title=${'Posts with no category appear as "Uncategorized" in WordPress.'}
4589 @click=${this._onCellClick}
4590 >${"Uncategorized"}</span>
4591 </span>
4592 `;
4593 }
4594 const chains = this._buildChains(selectedItems);
4595 return html`
4596 <div
4597 class="wpd-cat__chains"
4598 role="list"
4599 @click=${this._onCellClick}
4600 >
4601 ${chains.map(
4602 (chain) => this._renderChain(chain, readonly, disabled)
4603 )}
4604 </div>
4605 `;
4606 }
4607 /**
4608 * Build a `WpdCrumbSegment[]` per LEAF selection. A "leaf
4609 * selection" is a selected term that has no other selected
4610 * descendant. When the user has selected a parent AND its
4611 * children AND its grandchildren, only the deepest (leaf)
4612 * selection produces a chain — the chain itself walks
4613 * root → leaf and includes every path segment. Segments that
4614 * the user explicitly picked AND segments that just sit on the
4615 * path render the same way visually; the user's intent ("this
4616 * post is filed under Parent → Child → Grandchild") is what
4617 * gets shown, regardless of which subset of the path they
4618 * happened to tick.
4619 *
4620 * Two leaves under the same parent produce two chains; the
4621 * shared parent appears in both, which matches the user's
4622 * mental model ("filed under Tech/Web Dev/Frontend AND
4623 * Tech/Web Dev/Backend") without the ambiguity of merged-tree
4624 * visualizations.
4625 *
4626 * Each chain's hue is hashed from the root name; segments
4627 * inside the chain step their lightness from root (~38%) to
4628 * leaf (~58%) so the eye reads the gradient direction.
4629 */
4630 _buildChains(selectedItems) {
4631 const byId = /* @__PURE__ */ new Map();
4632 for (const item of this._items) {
4633 byId.set(item.id, item);
4634 }
4635 const selectedIds = new Set(selectedItems.map((s) => s.id));
4636 const hasSelectedDescendant = (ancestorId) => {
4637 for (const otherId of selectedIds) {
4638 if (otherId === ancestorId) {
4639 continue;
4640 }
4641 let cursor = byId.get(otherId);
4642 let safety = 16;
4643 while (cursor && safety-- > 0) {
4644 if (cursor.parent === ancestorId) {
4645 return true;
4646 }
4647 if (!cursor.parent) {
4648 break;
4649 }
4650 cursor = byId.get(cursor.parent);
4651 }
4652 }
4653 return false;
4654 };
4655 const chainLeaves = selectedItems.filter(
4656 (item) => !hasSelectedDescendant(item.id)
4657 );
4658 const chains = [];
4659 for (const leaf of chainLeaves) {
4660 const path = [];
4661 let cursor = leaf;
4662 let safety = 16;
4663 while (cursor && safety-- > 0) {
4664 if (cursor === leaf || selectedIds.has(cursor.id)) {
4665 path.unshift(cursor);
4666 }
4667 if (!cursor.parent) {
4668 break;
4669 }
4670 cursor = byId.get(cursor.parent);
4671 }
4672 const segments = path.map((item) => ({
4673 id: item.id,
4674 name: item.name
4675 }));
4676 chains.push({ id: leaf.id, segments });
4677 }
4678 return chains;
4679 }
4680 _renderChain(chain, readonly, disabled) {
4681 const removable = !readonly && !disabled;
4682 const onRemove = (e) => {
4683 e.stopPropagation();
4684 const detail = e.detail;
4685 const startIdx = typeof detail?.index === "number" ? detail.index : chain.segments.length - 1;
4686 const idsToRemove = /* @__PURE__ */ new Set();
4687 for (const seg of chain.segments.slice(startIdx)) {
4688 if (typeof seg.id === "number") {
4689 idsToRemove.add(seg.id);
4690 }
4691 }
4692 const next = this._value.filter(
4693 (id) => !idsToRemove.has(id)
4694 );
4695 if (next.length === this._value.length) {
4696 return;
4697 }
4698 this.emit("wpd-categories-change", { value: next });
4699 };
4700 const el = document.createElement("wpd-crumb-chain");
4701 el.segments = chain.segments;
4702 if (removable) {
4703 el.setAttribute("removable", "");
4704 }
4705 el.addEventListener("wpd-chain-remove", onRemove);
4706 return html`<div role="listitem">${el}</div>`;
4707 }
4708 _renderPopover(placeholder, loading) {
4709 const tree = this._buildTree();
4710 const filtered = this._filterTree(tree, this._query);
4711 const flat = this._flattenForDisplay(filtered);
4712 if (this._focusedRow >= flat.length) {
4713 this._focusedRow = flat.length > 0 ? flat.length - 1 : -1;
4714 }
4715 return html`
4716 <div class="wpd-cat__popover" role="dialog" aria-label="Choose categories">
4717 <input
4718 class="wpd-cat__search"
4719 type="text"
4720 autocomplete="off"
4721 placeholder=${placeholder}
4722 .value=${this._query}
4723 @input=${(e) => this._onSearchInput(e)}
4724 @keydown=${(e) => this._onSearchKeydown(e, flat)}
4725 />
4726 <div class="wpd-cat__tree" role="listbox" aria-multiselectable="true">
4727 ${this._renderCreateRow(0, 12, 0, "")}
4728 ${this._renderTreeBody(loading, flat)}
4729 </div>
4730 <div class="wpd-cat__footer">
4731 <span class="dashicons dashicons-info-outline" aria-hidden="true"></span>
4732 <span>
4733 Posts with no category appear as
4734 <strong>Uncategorized</strong>.
4735 </span>
4736 </div>
4737 </div>
4738 `;
4739 }
4740 _renderTreeBody(loading, flat) {
4741 if (loading) {
4742 return html`
4743 <div class="wpd-cat__loading">
4744 <span class="wpd-cat__loading-spinner" aria-hidden="true"></span>
4745 ${"Loading categories…"}
4746 </div>
4747 `;
4748 }
4749 if (flat.length === 0) {
4750 return html`
4751 <div class="wpd-cat__empty">
4752 ${this._items.length === 0 ? "No categories yet — create one in WordPress to assign." : "No matches."}
4753 </div>
4754 `;
4755 }
4756 return flat.map((entry, idx) => this._renderRow(entry, idx, flat.length));
4757 }
4758 _renderRow(entry, idx, _total) {
4759 const { node, hasChildren } = entry;
4760 const isSelected = this._value.includes(node.item.id);
4761 const isExpanded = !this._collapsed.has(node.item.id);
4762 const indent = 12 + node.depth * 16;
4763 const guide = node.depth > 0 ? node.depth * 16 : 0;
4764 const isFocused = idx === this._focusedRow;
4765 return html`
4766 <div class="wpd-cat__row-block">
4767 <div
4768 class="wpd-cat__row"
4769 role="option"
4770 aria-selected=${isSelected ? "true" : "false"}
4771 data-selected=${isSelected ? "true" : "false"}
4772 data-expanded=${isExpanded ? "true" : "false"}
4773 data-focused=${isFocused ? "true" : "false"}
4774 data-row-id=${String(node.item.id)}
4775 style=${`--wpd-cat-row-indent: ${indent}px; --wpd-cat-guide-width: ${guide}px;`}
4776 @mouseenter=${() => {
4777 this._focusedRow = idx;
4778 this.requestUpdate();
4779 }}
4780 @click=${(e) => {
4781 e.preventDefault();
4782 this._toggleSelection(node.item.id);
4783 }}
4784 >
4785 ${hasChildren ? html`<button
4786 type="button"
4787 class="wpd-cat__expander"
4788 aria-label=${isExpanded ? "Collapse" : "Expand"}
4789 @click=${(e) => {
4790 e.stopPropagation();
4791 this._toggleExpand(node.item.id);
4792 }}
4793 >${_iconCaretRight()}</button>` : html`<span class="wpd-cat__expander wpd-cat__expander--placeholder" aria-hidden="true">${_iconCaretRight()}</span>`}
4794 <span class="wpd-cat__check" aria-hidden="true">${_iconCheck()}</span>
4795 <span class="wpd-cat__label">${this._highlight(node.item.name, this._query)}</span>
4796 ${_isUncategorized(node.item) ? html`` : html`<button
4797 type="button"
4798 class="wpd-cat__delete"
4799 aria-label=${`Delete ${node.item.name}`}
4800 title=${`Delete ${node.item.name}`}
4801 @click=${(e) => this._onDeleteClick(e, node.item)}
4802 >${_iconCrossSmall()}</button>`}
4803 </div>
4804 ${isExpanded && !_isUncategorized(node.item) ? this._renderCreateRow(
4805 node.item.id,
4806 12 + (node.depth + 1) * 16,
4807 (node.depth + 1) * 16,
4808 node.item.name
4809 ) : html``}
4810 </div>
4811 `;
4812 }
4813 /**
4814 * Render an always-visible inline create-input. One sits at the
4815 * top of the popover (parentId 0 = create a root category) and
4816 * one sits beneath every visible row (create a child of that
4817 * row). Indent + guide-line align the child input with where the
4818 * new term will appear in the tree, so the user reads "this
4819 * input creates a sibling of the children below".
4820 *
4821 * The "+" submit button lives inside the input chrome; pressing
4822 * it (or Enter) emits `wpd-categories-create`. Esc clears the
4823 * field. While the consumer is processing the create REST call,
4824 * the field disables and a spinner replaces the submit button.
4825 */
4826 _renderCreateRow(parentId, indent, guide, parentName) {
4827 const value = this._creatingValues.get(parentId) ?? "";
4828 const pending = this._creatingPending.has(parentId);
4829 const placeholder = parentId === 0 ? "Add new category…" : `Add child of "${parentName}"…`;
4830 return html`
4831 <div
4832 class="wpd-cat__create-row"
4833 style=${`--wpd-cat-row-indent: ${indent}px; --wpd-cat-guide-width: ${guide}px;`}
4834 @click=${(e) => e.stopPropagation()}
4835 >
4836 <div class="wpd-cat__create-wrap">
4837 <input
4838 class="wpd-cat__create-input"
4839 type="text"
4840 autocomplete="off"
4841 spellcheck="false"
4842 placeholder=${placeholder}
4843 aria-label=${placeholder}
4844 .value=${value}
4845 ?disabled=${pending}
4846 @input=${(e) => this._onCreateInput(e, parentId)}
4847 @keydown=${(e) => this._onCreateKeydown(e, parentId)}
4848 />
4849 ${pending ? html`<span class="wpd-cat__create-spinner" aria-hidden="true"></span>` : html`<button
4850 type="button"
4851 class="wpd-cat__create-submit"
4852 aria-label=${parentId === 0 ? "Create category" : `Create child of ${parentName}`}
4853 ?disabled=${value.trim().length === 0}
4854 @click=${(e) => {
4855 e.stopPropagation();
4856 this._submitCreate(parentId);
4857 }}
4858 >${_iconPlusSmall()}</button>`}
4859 </div>
4860 </div>
4861 `;
4862 }
4863 _onCreateInput(e, parentId) {
4864 const value = e.target.value;
4865 if (value === "") {
4866 this._creatingValues.delete(parentId);
4867 } else {
4868 this._creatingValues.set(parentId, value);
4869 }
4870 this.requestUpdate();
4871 }
4872 _onCreateKeydown(e, parentId) {
4873 if (e.key === "Escape") {
4874 e.preventDefault();
4875 this._creatingValues.delete(parentId);
4876 this.requestUpdate();
4877 return;
4878 }
4879 if (e.key === "Enter") {
4880 e.preventDefault();
4881 this._submitCreate(parentId);
4882 }
4883 }
4884 _submitCreate(parentId) {
4885 const name = (this._creatingValues.get(parentId) ?? "").trim();
4886 if (!name || this._creatingPending.has(parentId)) {
4887 return;
4888 }
4889 this._creatingPending.add(parentId);
4890 this.requestUpdate();
4891 this.emit("wpd-categories-create", { name, parent: parentId });
4892 }
4893 /**
4894 * Public API — call after a successful create-handler run to
4895 * clear the inline input for that parent. Consumers usually
4896 * mutate `items` + `value` first (so the new term appears + is
4897 * selected), then call `endCreating( parent )` to clear the
4898 * field.
4899 *
4900 * @param parent The parent id used in the create event detail
4901 * (`0` for a root-level create).
4902 *
4903 * @public
4904 */
4905 endCreating(parent = 0) {
4906 this._creatingPending.delete(parent);
4907 this._creatingValues.delete(parent);
4908 this.requestUpdate();
4909 }
4910 /**
4911 * Public API — call from a consumer's catch path when the
4912 * create REST request fails. Keeps the typed text intact so the
4913 * user can retry with the same name; only the pending flag
4914 * clears.
4915 *
4916 * @param parent The parent id used in the create event detail.
4917 * @param _error Reserved for future use (e.g. surfacing the
4918 * error in the input chrome).
4919 *
4920 * @public
4921 */
4922 failCreating(parent = 0, _error) {
4923 this._creatingPending.delete(parent);
4924 this.requestUpdate();
4925 }
4926 // --- Tree helpers ----------------------------------------------------
4927 _buildTree() {
4928 const byId = /* @__PURE__ */ new Map();
4929 for (const item of this._items) {
4930 byId.set(item.id, { item, children: [], depth: 0 });
4931 }
4932 const roots = [];
4933 for (const node of byId.values()) {
4934 const parentId = node.item.parent;
4935 if (parentId && byId.has(parentId)) {
4936 const parentNode = byId.get(parentId);
4937 parentNode.children.push(node);
4938 } else {
4939 roots.push(node);
4940 }
4941 }
4942 const setDepth = (node, depth) => {
4943 node.depth = depth;
4944 for (const child of node.children) {
4945 setDepth(child, depth + 1);
4946 }
4947 };
4948 for (const root of roots) {
4949 setDepth(root, 0);
4950 }
4951 const sortRecursive = (nodes) => {
4952 nodes.sort((a, b) => {
4953 const aUncat = _isUncategorized(a.item);
4954 const bUncat = _isUncategorized(b.item);
4955 if (aUncat !== bUncat) {
4956 return aUncat ? -1 : 1;
4957 }
4958 return a.item.name.localeCompare(b.item.name);
4959 });
4960 for (const n of nodes) {
4961 sortRecursive(n.children);
4962 }
4963 };
4964 sortRecursive(roots);
4965 return roots;
4966 }
4967 _filterTree(tree, query) {
4968 const trimmed = query.trim().toLowerCase();
4969 if (!trimmed) {
4970 return tree;
4971 }
4972 const matches = (node) => {
4973 const ownMatch = node.item.name.toLowerCase().includes(trimmed);
4974 if (ownMatch) {
4975 return {
4976 item: node.item,
4977 children: node.children.slice(),
4978 depth: node.depth
4979 };
4980 }
4981 const childrenMatched = node.children.map(matches).filter((n) => n !== null);
4982 if (childrenMatched.length > 0) {
4983 return {
4984 item: node.item,
4985 children: childrenMatched,
4986 depth: node.depth
4987 };
4988 }
4989 return null;
4990 };
4991 return tree.map(matches).filter((n) => n !== null);
4992 }
4993 _flattenForDisplay(tree) {
4994 const out = [];
4995 const isSearching = this._query.trim() !== "";
4996 const walk = (nodes) => {
4997 for (const node of nodes) {
4998 out.push({
4999 node,
5000 visible: true,
5001 hasChildren: node.children.length > 0
5002 });
5003 const collapsed = this._collapsed.has(node.item.id) && !isSearching;
5004 if (!collapsed && node.children.length > 0) {
5005 walk(node.children);
5006 }
5007 }
5008 };
5009 walk(tree);
5010 return out;
5011 }
5012 _selectedItemsInOrder() {
5013 const byId = /* @__PURE__ */ new Map();
5014 for (const item of this._items) {
5015 byId.set(item.id, item);
5016 }
5017 const real = [];
5018 const uncatItems = [];
5019 for (const id of this._value) {
5020 const item = byId.get(id);
5021 if (!item) {
5022 continue;
5023 }
5024 if (item.name.toLowerCase() === UNCATEGORIZED_SLUG || item.id === 1) {
5025 uncatItems.push(item);
5026 } else {
5027 real.push(item);
5028 }
5029 }
5030 if (real.length > 0) {
5031 return real;
5032 }
5033 return uncatItems.length > 0 ? [] : real;
5034 }
5035 _highlight(label, query) {
5036 const trimmed = query.trim();
5037 if (!trimmed) {
5038 return label;
5039 }
5040 const lower = label.toLowerCase();
5041 const needle = trimmed.toLowerCase();
5042 const idx = lower.indexOf(needle);
5043 if (idx === -1) {
5044 return label;
5045 }
5046 return html`${label.slice(0, idx)}<span class="wpd-cat__match"
5047 >${label.slice(idx, idx + trimmed.length)}</span
5048 >${label.slice(idx + trimmed.length)}`;
5049 }
5050 // --- Mutations -------------------------------------------------------
5051 _toggleSelection(id) {
5052 const next = this._value.includes(id) ? this._value.filter((v) => v !== id) : [...this._value, id];
5053 this.emit("wpd-categories-change", { value: next });
5054 }
5055 _onDeleteClick(e, item) {
5056 e.stopPropagation();
5057 e.preventDefault();
5058 this.emit("wpd-categories-delete", { id: item.id, name: item.name });
5059 }
5060 _toggleExpand(id) {
5061 if (this._collapsed.has(id)) {
5062 this._collapsed.delete(id);
5063 } else {
5064 this._collapsed.add(id);
5065 }
5066 this.requestUpdate();
5067 }
5068 _onSearchInput(e) {
5069 this._query = e.target.value;
5070 this._focusedRow = 0;
5071 this.requestUpdate();
5072 }
5073 _onSearchKeydown(e, flat) {
5074 switch (e.key) {
5075 case "ArrowDown": {
5076 if (flat.length === 0) {
5077 return;
5078 }
5079 e.preventDefault();
5080 this._focusedRow = this._focusedRow + 1 >= flat.length ? 0 : this._focusedRow + 1;
5081 this.requestUpdate();
5082 this._scrollFocusedIntoView();
5083 return;
5084 }
5085 case "ArrowUp": {
5086 if (flat.length === 0) {
5087 return;
5088 }
5089 e.preventDefault();
5090 this._focusedRow = this._focusedRow <= 0 ? flat.length - 1 : this._focusedRow - 1;
5091 this.requestUpdate();
5092 this._scrollFocusedIntoView();
5093 return;
5094 }
5095 case "ArrowRight": {
5096 if (this._focusedRow < 0 || this._focusedRow >= flat.length) {
5097 return;
5098 }
5099 const entry = flat[this._focusedRow];
5100 if (entry.hasChildren && this._collapsed.has(entry.node.item.id)) {
5101 e.preventDefault();
5102 this._toggleExpand(entry.node.item.id);
5103 }
5104 return;
5105 }
5106 case "ArrowLeft": {
5107 if (this._focusedRow < 0 || this._focusedRow >= flat.length) {
5108 return;
5109 }
5110 const entry = flat[this._focusedRow];
5111 if (entry.hasChildren && !this._collapsed.has(entry.node.item.id)) {
5112 e.preventDefault();
5113 this._toggleExpand(entry.node.item.id);
5114 }
5115 return;
5116 }
5117 case "Enter":
5118 case " ": {
5119 if (this._focusedRow < 0 || this._focusedRow >= flat.length) {
5120 return;
5121 }
5122 e.preventDefault();
5123 const entry = flat[this._focusedRow];
5124 this._toggleSelection(entry.node.item.id);
5125 return;
5126 }
5127 case "Escape": {
5128 e.preventDefault();
5129 this.closePicker();
5130 }
5131 }
5132 }
5133 _scrollFocusedIntoView() {
5134 queueMicrotask(() => {
5135 const tree = this.shadowRoot?.querySelector(".wpd-cat__tree");
5136 if (!tree) {
5137 return;
5138 }
5139 const row = tree.querySelector(
5140 `.wpd-cat__row[data-focused="true"]`
5141 );
5142 if (!row) {
5143 return;
5144 }
5145 const rRect = row.getBoundingClientRect();
5146 const tRect = tree.getBoundingClientRect();
5147 if (rRect.top < tRect.top) {
5148 row.scrollIntoView({ block: "nearest" });
5149 } else if (rRect.bottom > tRect.bottom) {
5150 row.scrollIntoView({ block: "nearest" });
5151 }
5152 });
5153 }
5154 /**
5155 * Anchor the `position: fixed` popover to the trigger button.
5156 * Flips up when the popover would overflow the viewport bottom,
5157 * right-aligns when it would overflow the right edge. Runs on
5158 * every open after the popover has rendered (so we can read its
5159 * actual measured size, not a guess).
5160 *
5161 * Why fixed-positioning: the table cell scrolls inside
5162 * `<wpd-table>`'s shadow DOM, which has its own
5163 * `overflow: auto`. An `absolute` popover anchored to the cell
5164 * would be clipped by both the cell scroll AND the table
5165 * scroll. Fixed positioning escapes every ancestor's overflow
5166 * and lands the popover wherever we tell it relative to the
5167 * viewport.
5168 */
5169 _positionPopover() {
5170 const popover = this.shadowRoot?.querySelector(
5171 ".wpd-cat__popover"
5172 );
5173 if (!popover) {
5174 return;
5175 }
5176 const anchorRect = this.getBoundingClientRect();
5177 const popRect = popover.getBoundingClientRect();
5178 const viewportW = window.innerWidth;
5179 const viewportH = window.innerHeight;
5180 const margin = 8;
5181 let top = anchorRect.bottom + 4;
5182 const overflowBottom = top + popRect.height + margin > viewportH;
5183 const fitsAbove = anchorRect.top - 4 - popRect.height >= margin;
5184 if (overflowBottom && fitsAbove) {
5185 top = anchorRect.top - 4 - popRect.height;
5186 } else if (overflowBottom) {
5187 top = Math.max(margin, viewportH - popRect.height - margin);
5188 }
5189 let left = anchorRect.left;
5190 if (left + popRect.width + margin > viewportW) {
5191 left = anchorRect.right - popRect.width;
5192 }
5193 left = Math.max(
5194 margin,
5195 Math.min(left, viewportW - popRect.width - margin)
5196 );
5197 popover.style.top = `${top}px`;
5198 popover.style.left = `${left}px`;
5199 }
5200 };
5201 _WpdCategoryPicker.props = [
5202 "placeholder",
5203 "add-label",
5204 "disabled",
5205 "readonly",
5206 "open",
5207 "loading",
5208 "max-visible"
5209 ];
5210 _WpdCategoryPicker.styles = [styles$5];
5211 _WpdCategoryPicker.help = {
5212 title: "Category picker",
5213 summary: 'Hierarchical multi-select for taxonomy terms. Compact chip row + tree popover with search, collapsible branches, indent guides, keyboard navigation. Aligns with WordPress core: any subset selectable, "Uncategorized" rendered as a muted dashed sentinel when the value is empty.',
5214 status: "experimental",
5215 since: "0.8.0",
5216 props: [
5217 {
5218 name: "placeholder",
5219 type: "string",
5220 default: "Search categories…",
5221 description: "Native placeholder for the picker search input."
5222 },
5223 {
5224 name: "add-label",
5225 type: "string",
5226 default: "Categorize",
5227 description: "Trigger button label."
5228 },
5229 {
5230 name: "disabled",
5231 type: "boolean attribute",
5232 description: "Disables every interactive surface."
5233 },
5234 {
5235 name: "readonly",
5236 type: "boolean attribute",
5237 description: "Hides the trigger and the dismiss buttons on chips. Same as setting both `disabled` and bypassing the popover."
5238 },
5239 {
5240 name: "open",
5241 type: "boolean attribute",
5242 description: "Two-way reflected: present while the picker popover is open. Setting it externally opens / closes the popover."
5243 },
5244 {
5245 name: "loading",
5246 type: "boolean attribute",
5247 description: 'Show a "Loading categories…" spinner inside the popover. Use while the consumer is fetching the term list.'
5248 },
5249 {
5250 name: "max-visible",
5251 type: "integer (string)",
5252 default: "2",
5253 description: 'Number of selected chips to render before collapsing the rest into a "+N" overflow chip. The overflow chip doubles as the picker trigger.'
5254 }
5255 ],
5256 events: [
5257 {
5258 name: "wpd-categories-change",
5259 description: "Fires when the user toggles a row in the picker. Detail carries the new full id list — consumer mutates `value` (optimistically) and runs REST.",
5260 detail: "{ value: number[] }"
5261 },
5262 {
5263 name: "wpd-categories-open",
5264 description: "Fires when the popover opens.",
5265 detail: "{}"
5266 },
5267 {
5268 name: "wpd-categories-close",
5269 description: "Fires when the popover closes.",
5270 detail: "{}"
5271 },
5272 {
5273 name: "wpd-categories-create",
5274 description: "Fires when the user submits the inline create-child input. Consumer is expected to POST to the taxonomy REST endpoint, append the new term to `items`, and (optionally) auto-select it by adding the new id to `value`. Picker shows a per-row spinner while `creating-pending` is set.",
5275 detail: "{ name: string; parent: number }"
5276 },
5277 {
5278 name: "wpd-categories-delete",
5279 description: "Fires when the per-row × button is activated. The button only renders on hover/keyboard-focus and is suppressed for the WP Uncategorized fallback. Consumer is responsible for confirmation + REST + invalidating any cached tree (typically broadcasts `desktop-mode.term.changed`).",
5280 detail: "{ id: number; name: string }"
5281 }
5282 ],
5283 example: html`
5284 <wpd-category-picker placeholder="Search categories…"></wpd-category-picker>
5285 `
5286 };
5287 let WpdCategoryPicker = _WpdCategoryPicker;
5288 defineComponent("wpd-category-picker", WpdCategoryPicker);
5289 function _iconCaretRight() {
5290 return html`
5291 <svg
5292 viewBox="0 0 12 12"
5293 width="8"
5294 height="8"
5295 aria-hidden="true"
5296 focusable="false"
5297 fill="none"
5298 stroke="currentColor"
5299 stroke-width="2"
5300 stroke-linecap="round"
5301 stroke-linejoin="round"
5302 >
5303 <path d="M5 3 L8 6 L5 9" />
5304 </svg>
5305 `;
5306 }
5307 function _iconPlusSmall() {
5308 return html`
5309 <svg
5310 viewBox="0 0 12 12"
5311 width="11"
5312 height="11"
5313 aria-hidden="true"
5314 focusable="false"
5315 fill="none"
5316 stroke="currentColor"
5317 stroke-width="2"
5318 stroke-linecap="round"
5319 >
5320 <path d="M6 3 L6 9 M3 6 L9 6" />
5321 </svg>
5322 `;
5323 }
5324 function _iconCheck() {
5325 return html`
5326 <svg
5327 viewBox="0 0 12 12"
5328 aria-hidden="true"
5329 focusable="false"
5330 fill="none"
5331 stroke="currentColor"
5332 stroke-width="2"
5333 stroke-linecap="round"
5334 stroke-linejoin="round"
5335 >
5336 <path d="M2.5 6 L5 8.5 L9.5 4" />
5337 </svg>
5338 `;
5339 }
5340 function _iconCrossSmall() {
5341 return html`
5342 <svg
5343 viewBox="0 0 12 12"
5344 aria-hidden="true"
5345 focusable="false"
5346 fill="none"
5347 stroke="currentColor"
5348 stroke-width="2"
5349 stroke-linecap="round"
5350 >
5351 <path d="M3 3 L9 9 M9 3 L3 9" />
5352 </svg>
5353 `;
5354 }
5355 function hashTitleToHue(input) {
5356 if (!input) {
5357 return 214;
5358 }
5359 let hash = 5381;
5360 for (let i = 0; i < input.length; i++) {
5361 hash = Math.imul(hash, 33) + input.charCodeAt(i);
5362 }
5363 return (hash % 360 + 360) % 360;
5364 }
5365 const avatarStyles = css`:host{display:inline-flex;position:relative;width:var( --wpd-avatar-size,32px );height:var( --wpd-avatar-size,32px );flex:0 0 auto;vertical-align:middle;line-height:0;perspective:calc( var( --wpd-avatar-size,32px ) * 8 );--wpd-avatar-tilt-x:0deg;--wpd-avatar-tilt-y:0deg;--wpd-avatar-hover:0;--wpd-avatar-glare-x:50%;--wpd-avatar-glare-y:50%}:host( [ hidden ] ){display:none}.wpd-avatar__tile{position:relative;width:100%;height:100%;border-radius:50%;overflow:hidden;background:var( --desktop-mode-window-bg,#f0f0f1 );color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:calc( var( --wpd-avatar-size,32px ) * 0.48 );line-height:1;letter-spacing:0;font-feature-settings:'tnum' 1;user-select:none;transform-style:preserve-3d;transform:rotateX( var( --wpd-avatar-tilt-x ) ) rotateY( var( --wpd-avatar-tilt-y ) ) scale( calc( 1 + var( --wpd-avatar-hover ) * 0.07 ) );transition:transform 220ms cubic-bezier( 0.2,0.8,0.2,1 ),box-shadow 220ms cubic-bezier( 0.2,0.8,0.2,1 );box-shadow:inset 0 0 0 1px rgba( 255,255,255,calc( 0.18 + 0.22 * var( --wpd-avatar-hover ) ) ),inset 0 0 0 calc( 1px + var( --wpd-avatar-hover ) * 1px ) rgba( 0,0,0,calc( 0.08 + 0.04 * var( --wpd-avatar-hover ) ) ),0 calc( 1px + var( --wpd-avatar-hover ) * 8px ) calc( 6px + var( --wpd-avatar-hover ) * 18px ) rgba( 0,0,0,calc( 0.08 + 0.18 * var( --wpd-avatar-hover ) ) )}.wpd-avatar__tile::after{content:'';position:absolute;inset:0;border-radius:50%;background:radial-gradient( circle at var( --wpd-avatar-glare-x ) var( --wpd-avatar-glare-y ),rgba( 255,255,255,0.55 ) 0%,rgba( 255,255,255,0 ) 55% );opacity:var( --wpd-avatar-hover );mix-blend-mode:overlay;pointer-events:none;transition:opacity 220ms cubic-bezier( 0.2,0.8,0.2,1 )}.wpd-avatar__tile::before{content:'';position:absolute;inset:calc( var( --wpd-avatar-hover ) * -3px );border-radius:50%;background:radial-gradient( circle at var( --wpd-avatar-glare-x ) var( --wpd-avatar-glare-y ),rgba( 99,102,241,calc( 0.35 * var( --wpd-avatar-hover ) ) ) 0%,rgba( 99,102,241,0 ) 70% );filter:blur( 4px );pointer-events:none;z-index:-1;transition:inset 220ms cubic-bezier( 0.2,0.8,0.2,1 ),background 220ms}.wpd-avatar__tile img{width:100%;height:100%;object-fit:cover;display:block;transform:translateZ( 1px )}.wpd-avatar__dot{position:absolute;bottom:0;inset-inline-end:0;width:calc( var( --wpd-avatar-size,32px ) * 0.32 );height:calc( var( --wpd-avatar-size,32px ) * 0.32 );min-width:8px;min-height:8px;border-radius:50%;box-sizing:border-box;border:2px solid var( --wpd-avatar-dot-ring,var( --desktop-mode-window-bg,#fff ) );background:var( --wpd-avatar-dot-color,transparent );z-index:2}.wpd-avatar__dot--online{background:var( --desktop-mode-success,#00a32a )}.wpd-avatar__dot--inactive{background:var( --desktop-mode-warning,#dba617 )}.wpd-avatar__dot--offline{background:var( --desktop-mode-muted,#8c8f94 )}@media ( prefers-reduced-motion:reduce ){.wpd-avatar__tile{transform:none;transition:box-shadow 200ms}.wpd-avatar__tile::after,.wpd-avatar__tile::before{display:none}}`;
5366 const SIZE_MAP = {
5367 xs: 20,
5368 sm: 24,
5369 md: 40,
5370 lg: 64,
5371 xl: 96
5372 };
5373 const VALID_PRESENCE = /* @__PURE__ */ new Set(["online", "inactive", "offline"]);
5374 const _WpdAvatar = class _WpdAvatar extends Component {
5375 constructor() {
5376 super(...arguments);
5377 this._presenceHandler = null;
5378 this._imgFailed = false;
5379 this._onPointerMove = null;
5380 this._onPointerEnter = null;
5381 this._onPointerLeave = null;
5382 this._tiltRaf = 0;
5383 this._pendingTiltX = "0deg";
5384 this._pendingTiltY = "0deg";
5385 this._pendingGlareX = "50%";
5386 this._pendingGlareY = "50%";
5387 }
5388 connectedCallback() {
5389 super.connectedCallback();
5390 this._maybeAttachPresenceListener();
5391 this._attachHoverEffect();
5392 }
5393 disconnectedCallback() {
5394 if (this._presenceHandler) {
5395 document.removeEventListener(
5396 "desktop-mode-presence-changed",
5397 this._presenceHandler
5398 );
5399 this._presenceHandler = null;
5400 }
5401 this._detachHoverEffect();
5402 }
5403 attributeChangedCallback(name, oldValue, newValue) {
5404 super.attributeChangedCallback(name, oldValue, newValue);
5405 if (name === "src") {
5406 this._imgFailed = false;
5407 }
5408 if (name === "user-id" || name === "presence") {
5409 this._maybeAttachPresenceListener();
5410 }
5411 }
5412 render() {
5413 const src = this._attr("src");
5414 const name = this._attr("name") || "";
5415 const altRaw = this._attr("alt");
5416 const alt = altRaw !== null ? altRaw : name;
5417 const sizeRaw = this._attr("size");
5418 const size = this._resolveSize(sizeRaw);
5419 const presence = this._presenceForRender();
5420 const clickable = this._attr("clickable") !== null;
5421 this.style.setProperty("--wpd-avatar-size", `${size}px`);
5422 const initialsBg = src && !this._imgFailed ? "" : this._initialsBg(name);
5423 const inner = src && !this._imgFailed ? html`<img
5424 src=${src}
5425 alt=${alt}
5426 @error=${() => this._onImgError()}
5427 loading="lazy"
5428 />` : this._initials(name);
5429 const dot = presence ? html`<span
5430 class=${`wpd-avatar__dot wpd-avatar__dot--${presence}`}
5431 aria-label=${this._presenceLabel(presence)}
5432 ></span>` : html``;
5433 if (clickable) {
5434 return html`
5435 <button
5436 type="button"
5437 class="wpd-avatar__tile"
5438 aria-label=${alt || "User"}
5439 style=${initialsBg ? `background:${initialsBg};` : ""}
5440 @click=${(e) => this._onClick(e)}
5441 >${inner}</button>
5442 ${dot}
5443 `;
5444 }
5445 return html`
5446 <div
5447 class="wpd-avatar__tile"
5448 role="img"
5449 aria-label=${alt || "User"}
5450 style=${initialsBg ? `background:${initialsBg};` : ""}
5451 >${inner}</div>
5452 ${dot}
5453 `;
5454 }
5455 _attr(name) {
5456 return this.getAttribute(name);
5457 }
5458 _resolveSize(raw) {
5459 if (!raw) {
5460 return 32;
5461 }
5462 if (raw in SIZE_MAP) {
5463 return SIZE_MAP[raw];
5464 }
5465 const n = Number(raw);
5466 return Number.isFinite(n) && n > 0 ? n : 32;
5467 }
5468 _initials(name) {
5469 const trimmed = name.trim();
5470 if (!trimmed) {
5471 return "?";
5472 }
5473 return Array.from(trimmed)[0]?.toUpperCase() ?? "?";
5474 }
5475 _initialsBg(name) {
5476 const hue = hashTitleToHue(name);
5477 return `linear-gradient(135deg, hsl(${hue} 62% 55%), hsl(${(hue + 24) % 360} 58% 42%))`;
5478 }
5479 _presenceForRender() {
5480 const raw = this._attr("presence");
5481 if (raw && VALID_PRESENCE.has(raw)) {
5482 return raw;
5483 }
5484 return null;
5485 }
5486 _presenceLabel(p) {
5487 switch (p) {
5488 case "online":
5489 return "Online";
5490 case "inactive":
5491 return "Inactive";
5492 case "offline":
5493 return "Offline";
5494 }
5495 }
5496 _onImgError() {
5497 this._imgFailed = true;
5498 this.requestUpdate();
5499 }
5500 _onClick(e) {
5501 const userId = this._attr("user-id");
5502 const detail = {
5503 userId: userId !== null ? Number(userId) || null : null,
5504 originalEvent: e
5505 };
5506 this.emit("wpd-avatar-click", detail);
5507 }
5508 /**
5509 * Wire up the pointer-driven tilt + glare. Listens on the host so
5510 * one set of bindings covers both the clickable `<button>` and
5511 * the decorative `<div>` rendering branches. The actual math
5512 * runs in `_handlePointerMove`; this method just owns the
5513 * bind/unbind plumbing.
5514 *
5515 * Bails entirely when `prefers-reduced-motion: reduce` is set —
5516 * the CSS has its own `@media` guard for the visual layer, but
5517 * skipping the JS too saves the per-event work for users who
5518 * won't benefit from it.
5519 */
5520 _attachHoverEffect() {
5521 const reduceMotion = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
5522 if (reduceMotion) {
5523 return;
5524 }
5525 this._onPointerEnter = () => {
5526 this.style.setProperty("--wpd-avatar-hover", "1");
5527 };
5528 this._onPointerLeave = () => {
5529 this.style.setProperty("--wpd-avatar-hover", "0");
5530 this._pendingTiltX = "0deg";
5531 this._pendingTiltY = "0deg";
5532 this._pendingGlareX = "50%";
5533 this._pendingGlareY = "50%";
5534 this._flushTilt();
5535 };
5536 this._onPointerMove = (e) => {
5537 const rect = this.getBoundingClientRect();
5538 if (rect.width === 0 || rect.height === 0) {
5539 return;
5540 }
5541 const nx = (e.clientX - rect.left) / rect.width - 0.5;
5542 const ny = (e.clientY - rect.top) / rect.height - 0.5;
5543 const MAX = 14;
5544 this._pendingTiltY = `${(nx * MAX).toFixed(2)}deg`;
5545 this._pendingTiltX = `${(-ny * MAX).toFixed(2)}deg`;
5546 const gx = Math.max(0, Math.min(100, (nx + 0.5) * 100));
5547 const gy = Math.max(0, Math.min(100, (ny + 0.5) * 100));
5548 this._pendingGlareX = `${gx.toFixed(1)}%`;
5549 this._pendingGlareY = `${gy.toFixed(1)}%`;
5550 if (!this._tiltRaf) {
5551 this._tiltRaf = requestAnimationFrame(() => this._flushTilt());
5552 }
5553 };
5554 this.addEventListener("pointerenter", this._onPointerEnter);
5555 this.addEventListener("pointerleave", this._onPointerLeave);
5556 this.addEventListener("pointermove", this._onPointerMove);
5557 }
5558 _flushTilt() {
5559 this._tiltRaf = 0;
5560 this.style.setProperty("--wpd-avatar-tilt-x", this._pendingTiltX);
5561 this.style.setProperty("--wpd-avatar-tilt-y", this._pendingTiltY);
5562 this.style.setProperty("--wpd-avatar-glare-x", this._pendingGlareX);
5563 this.style.setProperty("--wpd-avatar-glare-y", this._pendingGlareY);
5564 }
5565 _detachHoverEffect() {
5566 if (this._onPointerMove) {
5567 this.removeEventListener("pointermove", this._onPointerMove);
5568 this._onPointerMove = null;
5569 }
5570 if (this._onPointerEnter) {
5571 this.removeEventListener("pointerenter", this._onPointerEnter);
5572 this._onPointerEnter = null;
5573 }
5574 if (this._onPointerLeave) {
5575 this.removeEventListener("pointerleave", this._onPointerLeave);
5576 this._onPointerLeave = null;
5577 }
5578 if (this._tiltRaf) {
5579 cancelAnimationFrame(this._tiltRaf);
5580 this._tiltRaf = 0;
5581 }
5582 }
5583 _maybeAttachPresenceListener() {
5584 const userId = this._attr("user-id");
5585 const explicit = this._attr("presence");
5586 const wantsListener = !!userId && !explicit;
5587 if (wantsListener && !this._presenceHandler) {
5588 this._presenceHandler = (e) => {
5589 const detail = e.detail;
5590 if (!detail) {
5591 return;
5592 }
5593 if (String(detail.userId) !== String(userId)) {
5594 return;
5595 }
5596 if (detail.newStatus && VALID_PRESENCE.has(detail.newStatus)) {
5597 this.setAttribute("presence", detail.newStatus);
5598 }
5599 };
5600 document.addEventListener(
5601 "desktop-mode-presence-changed",
5602 this._presenceHandler
5603 );
5604 } else if (!wantsListener && this._presenceHandler) {
5605 document.removeEventListener(
5606 "desktop-mode-presence-changed",
5607 this._presenceHandler
5608 );
5609 this._presenceHandler = null;
5610 }
5611 }
5612 };
5613 _WpdAvatar.props = ["src", "alt", "name", "size", "presence", "userId", "clickable"];
5614 _WpdAvatar.styles = [avatarStyles];
5615 _WpdAvatar.help = {
5616 title: "Avatar",
5617 summary: "Image-or-initials user tile with an optional presence dot. Falls back to a deterministic-hue letter tile when src is empty. Set user-id to auto-subscribe the dot to desktop-mode-presence-changed.",
5618 status: "stable",
5619 since: "0.22.0",
5620 props: [
5621 { name: "src", type: "string", description: "Image URL. Falls back to initials when empty or load fails." },
5622 { name: "alt", type: "string", description: "Alt text for the image. Defaults to `name` when omitted." },
5623 { name: "name", type: "string", description: "Used for initials + hue fallback when no src." },
5624 {
5625 name: "size",
5626 type: 'number | "xs" | "sm" | "md" | "lg" | "xl"',
5627 description: "Pixel size or named preset. Default 32 (sm-ish). Sets --wpd-avatar-size."
5628 },
5629 {
5630 name: "presence",
5631 type: '"online" | "inactive" | "offline"',
5632 description: "Presence dot color. Omit for no dot."
5633 },
5634 {
5635 name: "user-id",
5636 type: "number",
5637 description: "When set AND presence is unset, auto-subscribes to desktop-mode-presence-changed and updates the dot."
5638 }
5639 ],
5640 events: [
5641 {
5642 name: "wpd-avatar-click",
5643 description: "Fires on click of the tile. Detail carries userId when set.",
5644 detail: "{ userId: number | null }"
5645 }
5646 ],
5647 cssProps: [
5648 { name: "--wpd-avatar-size", description: "Tile size in any CSS length. Set automatically by the size attribute." },
5649 { name: "--wpd-avatar-dot-ring", description: "Background color used as the dot ring (matches surrounding panel by default)." }
5650 ],
5651 example: html`
5652 <wpd-avatar name="Daniel" size="40" presence="online"></wpd-avatar>
5653 `
5654 };
5655 let WpdAvatar = _WpdAvatar;
5656 defineComponent("wpd-avatar", WpdAvatar);
5657 const selectStyles = css`:host{display:flex;flex-direction:column;gap:4px;font-size:13px;color:var( --desktop-mode-text,#1d2327 );min-width:0}:host( [ hidden ] ){display:none}.wpd-select__label{font-size:12px;color:var( --desktop-mode-muted,#646970 )}.wpd-select__wrap{position:relative;display:flex;align-items:center;width:100%}select{appearance:none;-webkit-appearance:none;display:block;width:100%;min-width:0;padding:7px 28px 7px 12px;background:rgba( 0,0,0,0.05 );border:1px solid transparent;border-radius:7px;font:inherit;font-size:13px;color:var( --desktop-mode-text,#1d2327 );cursor:pointer;transition:background-color 0.12s ease,border-color 0.12s ease,box-shadow 0.12s ease}select:hover{background:rgba( 0,0,0,0.08 )}select:focus-visible{outline:none;border-color:var( --wp-admin-theme-color,#2271b1 );box-shadow:0 0 0 1px var( --wp-admin-theme-color,#2271b1 )}select:disabled{opacity:0.5;cursor:not-allowed}.wpd-select__chevron{position:absolute;inset-inline-end:10px;top:50%;transform:translateY( -50% );pointer-events:none;color:var( --desktop-mode-muted,#646970 );display:inline-block}select:hover ~ .wpd-select__chevron,select:focus-visible ~ .wpd-select__chevron{color:var( --desktop-mode-text,#1d2327 )}`;
5658 const optionStyles = css`:host{display:none}`;
5659 const _WpdOption = class _WpdOption extends Component {
5660 render() {
5661 return html``;
5662 }
5663 };
5664 _WpdOption.props = ["value", "disabled"];
5665 _WpdOption.styles = [optionStyles];
5666 _WpdOption.help = {
5667 title: "Option",
5668 summary: "Opaque data carrier for <wpd-select>. Carries its identifier in `value` and its visible label in textContent. Not rendered directly — the parent reads these and builds a native <select>.",
5669 status: "stable",
5670 since: "0.11.0",
5671 props: [
5672 {
5673 name: "value",
5674 type: "string",
5675 description: "Option identifier read by the parent <wpd-select>."
5676 },
5677 {
5678 name: "disabled",
5679 type: "boolean attribute",
5680 description: "Renders the option disabled in the parent <select>."
5681 }
5682 ],
5683 slots: [
5684 { name: "(default)", description: "Label text read from textContent." }
5685 ]
5686 };
5687 let WpdOption = _WpdOption;
5688 defineComponent("wpd-option", WpdOption);
5689 const _WpdSelect = class _WpdSelect extends Component {
5690 constructor() {
5691 super(...arguments);
5692 this._optionObserver = null;
5693 }
5694 /**
5695 * Declarative item-list setter. Replaces the existing
5696 * `<wpd-option>` children with a fresh set; preserves `value`
5697 * when it still matches, otherwise clears to the placeholder.
5698 *
5699 * Same shape as the setter on `<wpd-segmented>` so callers can
5700 * swap tag names (segmented ↔ select) without touching the
5701 * populate code when an option list outgrows the pill bar.
5702 *
5703 * ```js
5704 * select.items = [
5705 * { value: 'eur', label: 'Euro' },
5706 * { value: 'usd', label: 'US Dollar' },
5707 * ];
5708 * ```
5709 *
5710 * @since 0.11.0
5711 */
5712 set items(list) {
5713 const existing = this.querySelectorAll(":scope > wpd-option");
5714 for (const el of Array.from(existing)) {
5715 el.remove();
5716 }
5717 for (const item of list) {
5718 const opt = document.createElement("wpd-option");
5719 opt.setAttribute("value", item.value);
5720 opt.textContent = item.label;
5721 this.appendChild(opt);
5722 }
5723 const current = this.value;
5724 const stillValid = current !== null && list.some((i) => i.value === current);
5725 if (!stillValid && list.length > 0) {
5726 this.value = list[0].value;
5727 }
5728 this.requestUpdate();
5729 }
5730 connectedCallback() {
5731 super.connectedCallback();
5732 ensureAutoId(this);
5733 this._optionObserver = new MutationObserver(() => this.requestUpdate());
5734 this._optionObserver.observe(this, {
5735 childList: true,
5736 subtree: true,
5737 attributes: true,
5738 attributeFilter: ["value", "disabled"],
5739 characterData: true
5740 });
5741 }
5742 disconnectedCallback() {
5743 this._optionObserver?.disconnect();
5744 this._optionObserver = null;
5745 }
5746 render() {
5747 const label = this.label || "";
5748 const current = this.value;
5749 const placeholder = this.placeholder || "";
5750 const disabled = this.disabled !== null;
5751 const name = this.name || "";
5752 if (label) {
5753 this.setAttribute("aria-label", label);
5754 } else {
5755 this.removeAttribute("aria-label");
5756 }
5757 const selectAriaLabel = label || placeholder;
5758 const options = this._readOptions();
5759 const hostId = this.id || "wpd-unnamed";
5760 const selectId = `${hostId}__input`;
5761 return html`
5762 ${label ? html`<label
5763 class="wpd-select__label"
5764 for=${selectId}
5765 >${label}</label>` : html``}
5766 <span class="wpd-select__wrap">
5767 <select
5768 id=${selectId}
5769 ?disabled=${disabled}
5770 aria-label=${selectAriaLabel}
5771 name=${name}
5772 @change=${(e) => this._onChange(e)}
5773 >
5774 ${placeholder && !current ? html`<option value="" disabled selected>
5775 ${placeholder}
5776 </option>` : html``}
5777 ${options.map(
5778 (o) => html`
5779 <option
5780 value=${o.value}
5781 ?disabled=${o.disabled}
5782 ?selected=${o.value === current}
5783 >
5784 ${o.label}
5785 </option>
5786 `
5787 )}
5788 </select>
5789 <!--
5790 Inline SVG — the previous dashicons-classed span
5791 never painted because the global Dashicons font
5792 stylesheet cannot cross the shadow-root boundary.
5793 An inline SVG lives inside the shadow tree, inherits
5794 currentColor via the stroke attribute, and needs
5795 no external CSS.
5796 -->
5797 <svg
5798 class="wpd-select__chevron"
5799 viewBox="0 0 12 12"
5800 width="12"
5801 height="12"
5802 aria-hidden="true"
5803 focusable="false"
5804 >
5805 <path
5806 d="M3 5l3 3 3-3"
5807 stroke="currentColor"
5808 stroke-width="1.4"
5809 stroke-linecap="round"
5810 stroke-linejoin="round"
5811 fill="none"
5812 ></path>
5813 </svg>
5814 </span>
5815 `;
5816 }
5817 _readOptions() {
5818 const out = [];
5819 const children = this.querySelectorAll(":scope > wpd-option");
5820 for (const child of Array.from(children)) {
5821 const value = child.getAttribute("value");
5822 if (value === null) {
5823 continue;
5824 }
5825 out.push({
5826 value,
5827 label: (child.textContent || value).trim(),
5828 disabled: child.hasAttribute("disabled")
5829 });
5830 }
5831 return out;
5832 }
5833 _onChange(e) {
5834 const sel = e.target;
5835 const next = sel.value;
5836 this.value = next;
5837 this.emit("wpd-pick", { value: next });
5838 }
5839 };
5840 _WpdSelect.props = [
5841 "value",
5842 "label",
5843 "placeholder",
5844 "disabled",
5845 "name"
5846 ];
5847 _WpdSelect.styles = [selectStyles];
5848 _WpdSelect.help = {
5849 title: "Select",
5850 summary: "Dropdown picker that wraps a native <select>. Mirrors the <wpd-segmented> contract (set value, listen for wpd-pick) so callers can swap tag names when a list outgrows a pill bar.",
5851 status: "stable",
5852 since: "0.11.0",
5853 props: [
5854 {
5855 name: "value",
5856 type: "string",
5857 description: "Currently selected option value."
5858 },
5859 {
5860 name: "label",
5861 type: "string",
5862 description: "Visible label rendered above the select and forwarded to the native control as aria-label."
5863 },
5864 {
5865 name: "placeholder",
5866 type: "string",
5867 description: "Disabled leading option shown when no value is set."
5868 },
5869 {
5870 name: "disabled",
5871 type: "boolean attribute",
5872 description: "Disables the native select and dims the chrome."
5873 },
5874 {
5875 name: "name",
5876 type: "string",
5877 description: "Forwarded to the native <select name=…> for form submission."
5878 }
5879 ],
5880 slots: [
5881 { name: "(default)", description: '<wpd-option value="…"> children.' }
5882 ],
5883 events: [
5884 {
5885 name: "wpd-pick",
5886 description: "Fires when the user picks a new option.",
5887 detail: "{ value: string }"
5888 }
5889 ],
5890 cssProps: [
5891 { name: "--desktop-mode-text", description: "Label + value colour." },
5892 { name: "--desktop-mode-muted", description: "Placeholder + chevron colour." }
5893 ],
5894 example: html`
5895 <wpd-select value="eur" label="Currency">
5896 <wpd-option value="eur">Euro</wpd-option>
5897 <wpd-option value="usd">US Dollar</wpd-option>
5898 <wpd-option value="jpy">Japanese Yen</wpd-option>
5899 </wpd-select>
5900 `
5901 };
5902 let WpdSelect = _WpdSelect;
5903 defineComponent("wpd-select", WpdSelect);
5904 const multiselectStyles = css`
5905 :host {
5906 display: flex;
5907 flex-direction: column;
5908 gap: 4px;
5909 font-size: 13px;
5910 color: var( --desktop-mode-text, #1d2327 );
5911 min-width: 0;
5912 }
5913
5914 :host( [ hidden ] ) {
5915 display: none;
5916 }
5917
5918 .wpd-multiselect__label {
5919 font-size: 12px;
5920 color: var( --desktop-mode-muted, #646970 );
5921 }
5922
5923 .wpd-multiselect__trigger {
5924 appearance: none;
5925 display: inline-flex;
5926 align-items: center;
5927 justify-content: space-between;
5928 gap: 8px;
5929 width: 100%;
5930 min-width: 0;
5931 padding: 7px 12px 7px 12px;
5932 background: rgba( 0, 0, 0, 0.05 );
5933 border: 1px solid transparent;
5934 border-radius: 7px;
5935 font: inherit;
5936 font-size: 13px;
5937 color: var( --desktop-mode-text, #1d2327 );
5938 cursor: pointer;
5939 text-align: start;
5940 transition: background-color 0.12s ease, border-color 0.12s ease,
5941 box-shadow 0.12s ease;
5942 }
5943
5944 .wpd-multiselect__trigger:hover {
5945 background: rgba( 0, 0, 0, 0.08 );
5946 }
5947
5948 .wpd-multiselect__trigger:focus-visible {
5949 outline: none;
5950 border-color: var( --wp-admin-theme-color, #2271b1 );
5951 box-shadow: 0 0 0 1px var( --wp-admin-theme-color, #2271b1 );
5952 }
5953
5954 .wpd-multiselect__trigger:disabled {
5955 opacity: 0.5;
5956 cursor: not-allowed;
5957 }
5958
5959 .wpd-multiselect__trigger[ data-active='true' ] {
5960 color: var( --wp-admin-theme-color, #2271b1 );
5961 font-weight: 600;
5962 }
5963
5964 .wpd-multiselect__summary {
5965 flex: 1 1 auto;
5966 min-width: 0;
5967 overflow: hidden;
5968 text-overflow: ellipsis;
5969 white-space: nowrap;
5970 }
5971
5972 .wpd-multiselect__chevron {
5973 color: var( --desktop-mode-muted, #646970 );
5974 flex-shrink: 0;
5975 transition: color 0.12s ease, transform 0.18s ease;
5976 }
5977
5978 .wpd-multiselect__trigger:hover .wpd-multiselect__chevron,
5979 .wpd-multiselect__trigger:focus-visible .wpd-multiselect__chevron {
5980 color: var( --desktop-mode-text, #1d2327 );
5981 }
5982
5983 :host( [ open ] ) .wpd-multiselect__chevron {
5984 transform: rotate( 180deg );
5985 }
5986 `;
5987 function _installGlobalPopoverStyles() {
5988 const STYLE_ID = "wpd-multiselect-popover-styles";
5989 if (document.getElementById(STYLE_ID)) {
5990 return;
5991 }
5992 const style = document.createElement("style");
5993 style.id = STYLE_ID;
5994 style.textContent = `
5995 .wpd-multiselect__popover {
5996 position: fixed;
5997 z-index: 100000;
5998 max-height: 320px;
5999 overflow-y: auto;
6000 min-width: 200px;
6001 padding: 4px 0;
6002 background: var( --desktop-mode-window-bg, #fff );
6003 color: var( --desktop-mode-text, #1d2327 );
6004 border: 1px solid var( --desktop-mode-window-border, #c3c4c7 );
6005 border-radius: 8px;
6006 box-shadow: 0 8px 28px rgba( 0, 0, 0, 0.18 );
6007 font: inherit;
6008 font-size: 13px;
6009 }
6010
6011 .wpd-multiselect__clear {
6012 display: block;
6013 width: 100%;
6014 padding: 6px 12px;
6015 font: inherit;
6016 font-size: 11px;
6017 font-weight: 600;
6018 letter-spacing: 0.04em;
6019 text-transform: uppercase;
6020 text-align: start;
6021 border: 0;
6022 border-bottom: 1px solid var( --desktop-mode-window-border, #dcdcde );
6023 background: transparent;
6024 color: var( --wp-admin-theme-color, #2271b1 );
6025 cursor: pointer;
6026 }
6027
6028 .wpd-multiselect__clear:hover {
6029 background: color-mix(
6030 in srgb,
6031 var( --wp-admin-theme-color, #2271b1 ) 10%,
6032 transparent
6033 );
6034 }
6035
6036 .wpd-multiselect__option {
6037 display: flex;
6038 align-items: center;
6039 gap: 8px;
6040 padding: 6px 12px;
6041 cursor: pointer;
6042 user-select: none;
6043 }
6044
6045 .wpd-multiselect__option:hover {
6046 background: rgba( 0, 0, 0, 0.05 );
6047 }
6048
6049 .wpd-multiselect__option[ data-disabled='true' ] {
6050 opacity: 0.5;
6051 cursor: not-allowed;
6052 }
6053
6054 .wpd-multiselect__option > span {
6055 flex: 1 1 auto;
6056 min-width: 0;
6057 overflow: hidden;
6058 text-overflow: ellipsis;
6059 white-space: nowrap;
6060 }
6061
6062 .wpd-multiselect__option > input[ type='checkbox' ] {
6063 margin: 0;
6064 flex-shrink: 0;
6065 accent-color: var( --wp-admin-theme-color, #2271b1 );
6066 }
6067
6068 .wpd-multiselect__empty {
6069 padding: 8px 12px;
6070 color: var( --desktop-mode-muted, #646970 );
6071 font-style: italic;
6072 }
6073
6074 .wpd-multiselect__loading {
6075 display: flex;
6076 align-items: center;
6077 gap: 8px;
6078 padding: 8px 12px;
6079 color: var( --desktop-mode-muted, #646970 );
6080 font-size: 12px;
6081 }
6082
6083 .wpd-multiselect__spinner {
6084 display: inline-block;
6085 width: 12px;
6086 height: 12px;
6087 border-radius: 50%;
6088 border: 2px solid currentColor;
6089 border-top-color: transparent;
6090 animation: wpd-multiselect-spin 0.8s linear infinite;
6091 }
6092
6093 @keyframes wpd-multiselect-spin {
6094 to { transform: rotate( 360deg ); }
6095 }
6096 `;
6097 document.head.appendChild(style);
6098 }
6099 if (typeof document !== "undefined") {
6100 _installGlobalPopoverStyles();
6101 }
6102 const _WpdMultiselect = class _WpdMultiselect extends Component {
6103 constructor() {
6104 super(...arguments);
6105 this._optionObserver = null;
6106 this._popover = null;
6107 this._teardownOpen = null;
6108 this._hasMore = false;
6109 this._loadingMore = false;
6110 }
6111 /**
6112 * Declarative item-list setter. Replaces the existing
6113 * `<wpd-option>` children with a fresh set; preserves any values
6114 * that still match.
6115 *
6116 * @since 0.8.0
6117 */
6118 set items(list) {
6119 const existing = this.querySelectorAll(":scope > wpd-option");
6120 for (const el of Array.from(existing)) {
6121 el.remove();
6122 }
6123 for (const item of list) {
6124 const opt = document.createElement("wpd-option");
6125 opt.setAttribute("value", item.value);
6126 opt.textContent = item.label;
6127 this.appendChild(opt);
6128 }
6129 this._loadingMore = false;
6130 const validSet = new Set(list.map((i) => i.value));
6131 const next = this._readValues().filter((v) => validSet.has(v));
6132 this._writeValueAttribute(next);
6133 this.requestUpdate();
6134 this._refreshPopover();
6135 }
6136 /** Programmatic getter for the parsed selection. */
6137 get values() {
6138 return this._readValues();
6139 }
6140 /**
6141 * Programmatic setter — accepts an array of values; serialises
6142 * back to the `value` attribute as a comma-joined string.
6143 */
6144 set values(next) {
6145 const arr = Array.isArray(next) ? next.map((v) => String(v)).filter((v) => v !== "") : [];
6146 this._writeValueAttribute(arr);
6147 this.requestUpdate();
6148 this._refreshPopover();
6149 }
6150 /** Whether more pages are available (drives the load-more emit). */
6151 get hasMore() {
6152 return this._hasMore;
6153 }
6154 set hasMore(next) {
6155 this._hasMore = !!next;
6156 this._refreshPopover();
6157 }
6158 /**
6159 * Whether a load-more fetch is currently in flight. While true,
6160 * the popover paints a small spinner row and suppresses further
6161 * `wpd-multiselect-load-more` emits.
6162 */
6163 get loadingMore() {
6164 return this._loadingMore;
6165 }
6166 set loadingMore(next) {
6167 this._loadingMore = !!next;
6168 this._refreshPopover();
6169 }
6170 /**
6171 * Append additional options without dropping any already in the
6172 * tree. Used by infinite-scroll consumers — call when the next
6173 * page lands, then set `loadingMore = false` and update
6174 * `hasMore` based on whether more pages remain.
6175 *
6176 * @since 0.8.0
6177 */
6178 appendItems(more) {
6179 this._loadingMore = false;
6180 if (!more || more.length === 0) {
6181 this._refreshPopover();
6182 return;
6183 }
6184 const existing = new Set(
6185 Array.from(this.querySelectorAll(":scope > wpd-option")).map(
6186 (el) => el.getAttribute("value")
6187 )
6188 );
6189 for (const item of more) {
6190 if (existing.has(item.value)) {
6191 continue;
6192 }
6193 const opt = document.createElement("wpd-option");
6194 opt.setAttribute("value", item.value);
6195 opt.textContent = item.label;
6196 this.appendChild(opt);
6197 }
6198 this.requestUpdate();
6199 this._refreshPopover();
6200 }
6201 connectedCallback() {
6202 super.connectedCallback();
6203 ensureAutoId(this);
6204 this._optionObserver = new MutationObserver(() => {
6205 this.requestUpdate();
6206 this._refreshPopover();
6207 });
6208 this._optionObserver.observe(this, {
6209 childList: true,
6210 subtree: true,
6211 attributes: true,
6212 attributeFilter: ["value", "disabled"],
6213 characterData: true
6214 });
6215 }
6216 disconnectedCallback() {
6217 this._optionObserver?.disconnect();
6218 this._optionObserver = null;
6219 this._closePopover();
6220 }
6221 render() {
6222 const label = this.label || "";
6223 const placeholder = this.placeholder || "All";
6224 const disabled = this.disabled !== null;
6225 if (label) {
6226 this.setAttribute("aria-label", label);
6227 } else {
6228 this.removeAttribute("aria-label");
6229 }
6230 const triggerAriaLabel = label || placeholder;
6231 const summary = this._summarize(placeholder);
6232 const isActive = this._readValues().length > 0;
6233 const hostId = this.id || "wpd-unnamed";
6234 const triggerId = `${hostId}__trigger`;
6235 return html`
6236 ${label ? html`<label
6237 class="wpd-multiselect__label"
6238 for=${triggerId}
6239 >${label}</label>` : html``}
6240 <button
6241 id=${triggerId}
6242 type="button"
6243 class="wpd-multiselect__trigger"
6244 aria-haspopup="listbox"
6245 aria-expanded=${this._isOpen() ? "true" : "false"}
6246 aria-label=${triggerAriaLabel}
6247 ?disabled=${disabled}
6248 data-active=${isActive ? "true" : "false"}
6249 @click=${(e) => this._onTriggerClick(e)}
6250 >
6251 <span class="wpd-multiselect__summary">${summary}</span>
6252 <svg
6253 class="wpd-multiselect__chevron"
6254 viewBox="0 0 12 12"
6255 width="12"
6256 height="12"
6257 aria-hidden="true"
6258 focusable="false"
6259 >
6260 <path
6261 d="M3 5l3 3 3-3"
6262 stroke="currentColor"
6263 stroke-width="1.4"
6264 stroke-linecap="round"
6265 stroke-linejoin="round"
6266 fill="none"
6267 />
6268 </svg>
6269 </button>
6270 `;
6271 }
6272 _readOptions() {
6273 const out = [];
6274 const children = this.querySelectorAll(":scope > wpd-option");
6275 for (const child of Array.from(children)) {
6276 const value = child.getAttribute("value");
6277 if (value === null) {
6278 continue;
6279 }
6280 out.push({
6281 value,
6282 label: (child.textContent || value).trim(),
6283 disabled: child.hasAttribute("disabled")
6284 });
6285 }
6286 return out;
6287 }
6288 _readValues() {
6289 const raw = this.value ?? "";
6290 return raw.split(",").map((s) => s.trim()).filter((s) => s !== "");
6291 }
6292 _writeValueAttribute(vals) {
6293 const next = vals.join(",");
6294 this.value = next;
6295 }
6296 _summarize(placeholder) {
6297 const vals = this._readValues();
6298 if (vals.length === 0) {
6299 return placeholder;
6300 }
6301 const opts = this._readOptions();
6302 const byValue = new Map(opts.map((o) => [o.value, o.label]));
6303 if (vals.length === 1) {
6304 return byValue.get(vals[0]) ?? vals[0];
6305 }
6306 return `${vals.length} selected`;
6307 }
6308 _isOpen() {
6309 return this.open !== null;
6310 }
6311 _onTriggerClick(e) {
6312 e.stopPropagation();
6313 e.preventDefault();
6314 const disabled = this.disabled !== null;
6315 if (disabled) {
6316 return;
6317 }
6318 if (this._popover) {
6319 this._closePopover();
6320 } else {
6321 this._openPopover();
6322 }
6323 }
6324 _openPopover() {
6325 if (this._popover) {
6326 return;
6327 }
6328 const popover = document.createElement("div");
6329 popover.className = "wpd-multiselect__popover";
6330 popover.setAttribute("role", "listbox");
6331 popover.setAttribute("aria-multiselectable", "true");
6332 popover.style.setProperty(
6333 "--wp-admin-theme-color",
6334 getComputedStyle(this).getPropertyValue(
6335 "--wp-admin-theme-color"
6336 ) || "#2271b1"
6337 );
6338 document.body.appendChild(popover);
6339 this._popover = popover;
6340 this._refreshPopover();
6341 this._placePopover();
6342 const onDocPointer = (ev) => {
6343 const target = ev.target;
6344 if (!target) {
6345 return;
6346 }
6347 const trigger = this.shadowRoot?.querySelector(
6348 ".wpd-multiselect__trigger"
6349 );
6350 if (popover.contains(target)) {
6351 return;
6352 }
6353 if (trigger && trigger.contains(target)) {
6354 return;
6355 }
6356 this._closePopover();
6357 };
6358 const onKey = (ev) => {
6359 if (ev.key === "Escape") {
6360 ev.stopPropagation();
6361 this._closePopover();
6362 const trigger = this.shadowRoot?.querySelector(
6363 ".wpd-multiselect__trigger"
6364 );
6365 trigger?.focus();
6366 }
6367 };
6368 const onResizeScroll = () => this._placePopover();
6369 const onPopoverScroll = () => {
6370 if (!this._hasMore || this._loadingMore) {
6371 return;
6372 }
6373 const sh = popover.scrollHeight;
6374 const ch = popover.clientHeight;
6375 const st = popover.scrollTop;
6376 if (sh - (st + ch) < 64) {
6377 this.emit("wpd-multiselect-load-more", {});
6378 }
6379 };
6380 setTimeout(() => {
6381 document.addEventListener("pointerdown", onDocPointer, true);
6382 }, 0);
6383 document.addEventListener("keydown", onKey, true);
6384 window.addEventListener("resize", onResizeScroll);
6385 window.addEventListener("scroll", onResizeScroll, true);
6386 popover.addEventListener("scroll", onPopoverScroll);
6387 this._teardownOpen = () => {
6388 document.removeEventListener("pointerdown", onDocPointer, true);
6389 document.removeEventListener("keydown", onKey, true);
6390 window.removeEventListener("resize", onResizeScroll);
6391 window.removeEventListener("scroll", onResizeScroll, true);
6392 popover.removeEventListener("scroll", onPopoverScroll);
6393 };
6394 this.setAttribute("open", "");
6395 this.requestUpdate();
6396 this.emit("wpd-multiselect-open", {});
6397 }
6398 _closePopover() {
6399 if (this._teardownOpen) {
6400 this._teardownOpen();
6401 this._teardownOpen = null;
6402 }
6403 if (this._popover) {
6404 this._popover.remove();
6405 this._popover = null;
6406 this.removeAttribute("open");
6407 this.requestUpdate();
6408 this.emit("wpd-multiselect-close", {});
6409 }
6410 }
6411 _refreshPopover() {
6412 const popover = this._popover;
6413 if (!popover) {
6414 return;
6415 }
6416 const options = this._readOptions();
6417 const selected = new Set(this._readValues());
6418 popover.replaceChildren();
6419 if (options.length === 0) {
6420 const empty = document.createElement("div");
6421 empty.className = "wpd-multiselect__empty";
6422 empty.textContent = "No options";
6423 popover.appendChild(empty);
6424 return;
6425 }
6426 if (selected.size > 0) {
6427 const clear = document.createElement("button");
6428 clear.type = "button";
6429 clear.className = "wpd-multiselect__clear";
6430 clear.textContent = "Clear";
6431 clear.addEventListener("click", (e) => {
6432 e.preventDefault();
6433 e.stopPropagation();
6434 this._writeValueAttribute([]);
6435 this.requestUpdate();
6436 this._refreshPopover();
6437 this._emitPick();
6438 });
6439 popover.appendChild(clear);
6440 }
6441 for (const opt of options) {
6442 const row = document.createElement("label");
6443 row.className = "wpd-multiselect__option";
6444 row.setAttribute("role", "option");
6445 row.setAttribute(
6446 "aria-selected",
6447 selected.has(opt.value) ? "true" : "false"
6448 );
6449 if (opt.disabled) {
6450 row.setAttribute("aria-disabled", "true");
6451 row.dataset.disabled = "true";
6452 }
6453 const cb = document.createElement("input");
6454 cb.type = "checkbox";
6455 cb.checked = selected.has(opt.value);
6456 cb.disabled = opt.disabled;
6457 cb.addEventListener("change", () => {
6458 const cur = new Set(this._readValues());
6459 if (cb.checked) {
6460 cur.add(opt.value);
6461 } else {
6462 cur.delete(opt.value);
6463 }
6464 const ordered = options.map((o) => o.value).filter((v) => cur.has(v));
6465 this._writeValueAttribute(ordered);
6466 row.setAttribute(
6467 "aria-selected",
6468 cb.checked ? "true" : "false"
6469 );
6470 this.requestUpdate();
6471 this._refreshPopover();
6472 this._emitPick();
6473 });
6474 const labelText = document.createElement("span");
6475 labelText.textContent = opt.label;
6476 row.appendChild(cb);
6477 row.appendChild(labelText);
6478 popover.appendChild(row);
6479 }
6480 if (this._loadingMore) {
6481 const loading = document.createElement("div");
6482 loading.className = "wpd-multiselect__loading";
6483 const spinner = document.createElement("span");
6484 spinner.className = "wpd-multiselect__spinner";
6485 spinner.setAttribute("aria-hidden", "true");
6486 const text = document.createElement("span");
6487 text.textContent = "Loading…";
6488 loading.appendChild(spinner);
6489 loading.appendChild(text);
6490 popover.appendChild(loading);
6491 }
6492 }
6493 _placePopover() {
6494 const popover = this._popover;
6495 const trigger = this.shadowRoot?.querySelector(
6496 ".wpd-multiselect__trigger"
6497 );
6498 if (!popover || !trigger) {
6499 return;
6500 }
6501 const rect = trigger.getBoundingClientRect();
6502 const vw = window.innerWidth;
6503 const vh = window.innerHeight;
6504 const minW = Math.max(rect.width, 200);
6505 popover.style.minWidth = `${minW}px`;
6506 let left = rect.left;
6507 if (left + minW > vw - 8) {
6508 left = Math.max(8, vw - minW - 8);
6509 }
6510 popover.style.left = `${left}px`;
6511 popover.style.top = `${rect.bottom + 4}px`;
6512 const popH = popover.offsetHeight || 200;
6513 if (rect.bottom + 4 + popH > vh - 8) {
6514 popover.style.top = `${Math.max(8, rect.top - popH - 4)}px`;
6515 }
6516 }
6517 _emitPick() {
6518 const values = this._readValues();
6519 this.emit("wpd-pick", {
6520 value: values.join(","),
6521 values
6522 });
6523 }
6524 };
6525 _WpdMultiselect.props = [
6526 "value",
6527 "label",
6528 "placeholder",
6529 "disabled",
6530 "name",
6531 "open"
6532 ];
6533 _WpdMultiselect.styles = [multiselectStyles];
6534 _WpdMultiselect.help = {
6535 title: "Multi-select",
6536 summary: "Multi-select dropdown picker that mirrors <wpd-select> ergonomically. Trigger button shows a one-line summary; clicking opens a checkbox popover. value is a comma-joined id list so it round-trips through plain string attributes.",
6537 status: "experimental",
6538 since: "0.8.0",
6539 props: [
6540 {
6541 name: "value",
6542 type: "string (comma-joined ids)",
6543 description: 'Currently selected option values, joined by commas (e.g. "1,4"). Empty string means no selection.'
6544 },
6545 {
6546 name: "label",
6547 type: "string",
6548 description: "Visible label rendered above the trigger and forwarded as aria-label to the trigger button."
6549 },
6550 {
6551 name: "placeholder",
6552 type: "string",
6553 description: 'Trigger summary when no option is checked. Defaults to "All".'
6554 },
6555 {
6556 name: "disabled",
6557 type: "boolean attribute",
6558 description: "Disables the trigger and dims the chrome."
6559 },
6560 {
6561 name: "name",
6562 type: "string",
6563 description: "Forwarded to the hidden form-field for HTML form submission."
6564 },
6565 {
6566 name: "open",
6567 type: "boolean attribute",
6568 description: "Reflects the open state of the popover. Toggle programmatically to open/close, or read from a CSS selector."
6569 }
6570 ],
6571 slots: [
6572 { name: "(default)", description: '<wpd-option value="…"> children.' }
6573 ],
6574 events: [
6575 {
6576 name: "wpd-pick",
6577 description: "Fires when the user toggles any option. Detail carries both shapes — `value` is the comma-joined attribute round-trip, `values` is the parsed array.",
6578 detail: "{ value: string; values: string[] }"
6579 },
6580 {
6581 name: "wpd-multiselect-open",
6582 description: "Fires when the popover opens.",
6583 detail: "{}"
6584 },
6585 {
6586 name: "wpd-multiselect-close",
6587 description: "Fires when the popover closes.",
6588 detail: "{}"
6589 },
6590 {
6591 name: "wpd-multiselect-load-more",
6592 description: "Fires when the user scrolls near the bottom of the popover and `hasMore` is true. Consumer fetches the next page and calls `picker.appendItems(...)` to extend the list. While the fetch is in flight, set `picker.loadingMore = true` to show the spinner row and prevent re-firing.",
6593 detail: "{}"
6594 }
6595 ],
6596 cssProps: [
6597 { name: "--desktop-mode-text", description: "Label + value colour." },
6598 { name: "--desktop-mode-muted", description: "Placeholder + chevron colour." }
6599 ],
6600 example: html`
6601 <wpd-multiselect value="1,4" label="Authors">
6602 <wpd-option value="1">Daniel</wpd-option>
6603 <wpd-option value="4">Peter</wpd-option>
6604 <wpd-option value="9">Pat</wpd-option>
6605 </wpd-multiselect>
6606 `
6607 };
6608 let WpdMultiselect = _WpdMultiselect;
6609 defineComponent("wpd-multiselect", WpdMultiselect);
6610 const styles$3 = css`:host{display:inline;color:inherit;font:inherit}`;
6611 const _instances = /* @__PURE__ */ new Set();
6612 let _ticker = null;
6613 const TICK_INTERVAL_MS = 3e4;
6614 function startTicker() {
6615 if (_ticker !== null) {
6616 return;
6617 }
6618 _ticker = window.setInterval(() => {
6619 for (const i of _instances) {
6620 i.tick();
6621 }
6622 }, TICK_INTERVAL_MS);
6623 }
6624 function stopTickerIfIdle() {
6625 if (_ticker !== null && _instances.size === 0) {
6626 window.clearInterval(_ticker);
6627 _ticker = null;
6628 }
6629 }
6630 function parseDatetime(raw) {
6631 if (!raw) {
6632 return null;
6633 }
6634 const tryDate = (v) => {
6635 const d = new Date(v);
6636 return Number.isNaN(d.getTime()) ? null : d;
6637 };
6638 if (raw.includes("T") || raw.endsWith("Z")) {
6639 return tryDate(raw);
6640 }
6641 return tryDate(raw.replace(" ", "T") + "Z");
6642 }
6643 let _rtfCache = null;
6644 function getRtf() {
6645 if (!_rtfCache) {
6646 const lang = typeof navigator !== "undefined" && navigator.language || "en";
6647 _rtfCache = new Intl.RelativeTimeFormat(lang, { numeric: "auto" });
6648 }
6649 return _rtfCache;
6650 }
6651 function relativeText(date, now) {
6652 const rtf = getRtf();
6653 const diffMs = date.getTime() - now;
6654 const diffSec = Math.round(diffMs / 1e3);
6655 const abs = Math.abs;
6656 if (abs(diffSec) < 45) {
6657 return rtf.format(0, "second");
6658 }
6659 const diffMin = Math.round(diffSec / 60);
6660 if (abs(diffMin) < 45) {
6661 return rtf.format(diffMin, "minute");
6662 }
6663 const diffHour = Math.round(diffMin / 60);
6664 if (abs(diffHour) < 22) {
6665 return rtf.format(diffHour, "hour");
6666 }
6667 const diffDay = Math.round(diffHour / 24);
6668 if (abs(diffDay) < 26) {
6669 return rtf.format(diffDay, "day");
6670 }
6671 const diffMonth = Math.round(diffDay / 30);
6672 if (abs(diffMonth) < 11) {
6673 return rtf.format(diffMonth, "month");
6674 }
6675 const diffYear = Math.round(diffDay / 365);
6676 return rtf.format(diffYear, "year");
6677 }
6678 const _WpdRelativeTime = class _WpdRelativeTime extends Component {
6679 connectedCallback() {
6680 super.connectedCallback();
6681 _instances.add(this);
6682 startTicker();
6683 }
6684 disconnectedCallback() {
6685 _instances.delete(this);
6686 stopTickerIfIdle();
6687 }
6688 /** Public — the shared ticker calls this on every interval. */
6689 tick() {
6690 this.requestUpdate();
6691 }
6692 render() {
6693 const raw = this.datetime;
6694 const date = parseDatetime(raw);
6695 if (!date) {
6696 return html`<span>${raw ?? ""}</span>`;
6697 }
6698 const text = relativeText(date, Date.now());
6699 const absolute = date.toLocaleString();
6700 return html`<time datetime=${date.toISOString()} title=${absolute}
6701 >${text}</time
6702 >`;
6703 }
6704 };
6705 _WpdRelativeTime.props = ["datetime"];
6706 _WpdRelativeTime.styles = [styles$3];
6707 _WpdRelativeTime.help = {
6708 title: "Relative time",
6709 summary: 'Auto-ticking relative timestamp. Renders "5 minutes ago" / "yesterday" / "in 3 hours" via Intl.RelativeTimeFormat and updates itself every 30s while connected. Useful for any list cell that should age live (recycle bin, notifications, activity log) without forcing the surrounding view to repaint.',
6710 status: "experimental",
6711 since: "0.21.0",
6712 props: [
6713 {
6714 name: "datetime",
6715 type: 'ISO 8601 string OR MySQL-style "Y-m-d H:i:s" (treated as UTC)',
6716 description: "The moment the relative copy is anchored to. Accepts the format WordPress hands back from `*_gmt` columns directly."
6717 }
6718 ],
6719 slots: [],
6720 cssProps: [],
6721 example: html`<wpd-relative-time
6722 datetime="${new Date(Date.now() - 1e3 * 60 * 5).toISOString()}"
6723 ></wpd-relative-time>`
6724 };
6725 let WpdRelativeTime = _WpdRelativeTime;
6726 defineComponent("wpd-relative-time", WpdRelativeTime);
6727 const wpdFormStyles = css`:host{display:block;container-type:inline-size;container-name:wpd-form;font-size:13px;color:var( --desktop-mode-text,#1d2327 )}:host( [ hidden ] ){display:none}.header{margin:0 0 18px}.header:empty{display:none}.fields{display:grid;grid-template-columns:1fr;gap:14px 16px;margin:0 0 18px}@container wpd-form ( min-width:480px ){.fields{grid-template-columns:repeat( 2,minmax( 0,1fr ) )}}@container wpd-form ( min-width:760px ){:host( [ columns="3" ] ) .fields{grid-template-columns:repeat( 3,minmax( 0,1fr ) )}}:host( [ columns="1" ] ) .fields{grid-template-columns:1fr}:host( [ columns="2" ] ) .fields{grid-template-columns:repeat( 2,minmax( 0,1fr ) )}::slotted( [ full-width ] ){grid-column:1 / -1}::slotted( [ slot ] ){display:contents}.error{margin:0 0 14px;padding:10px 12px;border-radius:6px;background:rgba( 179,45,46,0.10 );color:#b32d2e;font-size:13px;line-height:1.4}.error[ hidden ]{display:none}.footer{display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:flex-end;border-top:1px solid var( --desktop-mode-border,#dcdcde );padding-top:14px}:host( [ align="start" ] ) .footer{justify-content:flex-start}:host( [ align="stretch" ] ) .footer{justify-content:stretch}:host( [ align="stretch" ] ) .footer .footer-actions{flex:1 1 auto}.footer-leading,.footer-trailing{display:contents}.footer-actions{display:inline-flex;gap:8px;align-items:center;margin-inline-start:auto}:host( [ align="start" ] ) .footer-actions{margin-inline-start:0}:host( [ busy ] ){pointer-events:none}:host( [ busy ] ) .fields{opacity:0.6}:host( [ busy ] ) .footer{pointer-events:auto}.busy-spinner{display:inline-flex;width:14px;height:14px;border-radius:50%;border:2px solid currentColor;border-right-color:transparent;animation:wpd-form-spin 0.7s linear infinite;vertical-align:-2px;margin-inline-end:6px}@keyframes wpd-form-spin{to{transform:rotate( 360deg )}}@media ( prefers-reduced-motion:reduce ){.busy-spinner{animation-duration:2s}}`;
6728 const _WpdForm = class _WpdForm extends Component {
6729 constructor() {
6730 super(...arguments);
6731 this._initial = /* @__PURE__ */ new Map();
6732 this._captured = false;
6733 this._fieldChangeListener = null;
6734 this._enterSubmitListener = null;
6735 }
6736 connectedCallback() {
6737 super.connectedCallback();
6738 queueMicrotask(() => this._captureInitialValues());
6739 this._fieldChangeListener = (e) => this._onAnyFieldInput(e);
6740 this.addEventListener("wpd-input-change", this._fieldChangeListener);
6741 this.addEventListener("wpd-input-commit", this._fieldChangeListener);
6742 this.addEventListener("wpd-checkbox-change", this._fieldChangeListener);
6743 this.addEventListener("wpd-select-change", this._fieldChangeListener);
6744 this.addEventListener("change", this._fieldChangeListener);
6745 this._enterSubmitListener = () => this.submit();
6746 this.addEventListener("wpd-submit", this._enterSubmitListener);
6747 }
6748 disconnectedCallback() {
6749 if (this._fieldChangeListener) {
6750 this.removeEventListener("wpd-input-change", this._fieldChangeListener);
6751 this.removeEventListener("wpd-input-commit", this._fieldChangeListener);
6752 this.removeEventListener("wpd-checkbox-change", this._fieldChangeListener);
6753 this.removeEventListener("wpd-select-change", this._fieldChangeListener);
6754 this.removeEventListener("change", this._fieldChangeListener);
6755 this._fieldChangeListener = null;
6756 }
6757 if (this._enterSubmitListener) {
6758 this.removeEventListener("wpd-submit", this._enterSubmitListener);
6759 this._enterSubmitListener = null;
6760 }
6761 }
6762 render() {
6763 const submitLabel = this["submit-label"] || "Submit";
6764 const resetLabel = this["reset-label"] || "Reset";
6765 const error = this.error || "";
6766 const busy = this.busy !== null;
6767 const showResetRaw = this["show-reset"];
6768 const showReset = showResetRaw !== "false";
6769 return html`
6770 <div class="header" part="header">
6771 <slot name="header"></slot>
6772 </div>
6773 <div class="fields" part="fields">
6774 <slot></slot>
6775 </div>
6776 <slot name="error">
6777 ${error ? html`<p class="error" role="alert" part="error">${error}</p>` : html`<p class="error" role="alert" part="error" hidden></p>`}
6778 </slot>
6779 <footer class="footer" part="footer">
6780 <span class="footer-leading"
6781 ><slot name="footer-leading"></slot
6782 ></span>
6783 <span class="footer-actions">
6784 ${showReset ? html`<wpd-button
6785 variant="ghost"
6786 data-wpd-form-action="reset"
6787 ?disabled=${busy}
6788 @click=${() => this.reset()}
6789 >${resetLabel}</wpd-button>` : html``}
6790 <wpd-button
6791 variant="primary"
6792 data-wpd-form-action="submit"
6793 ?disabled=${busy}
6794 @click=${() => this.submit()}
6795 >
6796 ${busy ? html`<span class="busy-spinner" aria-hidden="true"></span>` : html``}
6797 ${submitLabel}
6798 </wpd-button>
6799 </span>
6800 <span class="footer-trailing"
6801 ><slot name="footer-trailing"></slot
6802 ></span>
6803 </footer>
6804 `;
6805 }
6806 // ─── Public API ──────────────────────────────────────────────────
6807 /**
6808 * Collect every named descendant's current value. Checkboxes
6809 * return `boolean`; everything else returns whatever the field
6810 * surfaces on its `value` property (or attribute as fallback).
6811 */
6812 getValues() {
6813 const out = {};
6814 for (const field of this._namedFields()) {
6815 const name = field.getAttribute("name");
6816 if (!name) {
6817 continue;
6818 }
6819 out[name] = this._readField(field);
6820 }
6821 return out;
6822 }
6823 /**
6824 * Apply a partial values map to the matching named fields.
6825 * Unknown names are skipped silently (fields may not be
6826 * mounted yet).
6827 */
6828 setValues(patch) {
6829 for (const [name, value] of Object.entries(patch)) {
6830 const field = this._fieldByName(name);
6831 if (!field) {
6832 continue;
6833 }
6834 this._writeField(field, value);
6835 }
6836 }
6837 /** Toggle the busy attribute (also re-renders to refresh the spinner). */
6838 setBusy(busy) {
6839 if (busy) {
6840 this.setAttribute("busy", "");
6841 } else {
6842 this.removeAttribute("busy");
6843 }
6844 }
6845 /**
6846 * Set the top-of-form error banner. Pass `null` (or empty
6847 * string) to clear. Equivalent to setting the `error` attribute.
6848 */
6849 setError(message) {
6850 if (message) {
6851 this.setAttribute("error", message);
6852 } else {
6853 this.removeAttribute("error");
6854 }
6855 }
6856 /**
6857 * Mark a single field invalid (or clear it). Useful for
6858 * server-returned per-field errors — e.g. "username already
6859 * exists". The optional `message` is set via the field's
6860 * `error` attribute when supported (currently a no-op for
6861 * fields that don't render one — falls back to the `invalid`
6862 * highlight only).
6863 */
6864 setFieldInvalid(name, invalid = true, message = null) {
6865 const field = this._fieldByName(name);
6866 if (!field) {
6867 return;
6868 }
6869 if (invalid) {
6870 field.setAttribute("invalid", "");
6871 if (message !== null) {
6872 field.setAttribute("error", message);
6873 }
6874 } else {
6875 field.removeAttribute("invalid");
6876 field.removeAttribute("error");
6877 }
6878 }
6879 /** Clear the form-level error AND every per-field invalid mark. */
6880 clearErrors() {
6881 this.setError(null);
6882 for (const field of this._namedFields()) {
6883 field.removeAttribute("invalid");
6884 field.removeAttribute("error");
6885 }
6886 }
6887 /**
6888 * Restore every field to its initial value (the snapshot taken
6889 * at first connection). Fires `wpd-form-reset` afterwards.
6890 */
6891 reset() {
6892 this.clearErrors();
6893 for (const [name, snap] of this._initial.entries()) {
6894 const field = this._fieldByName(name);
6895 if (!field) {
6896 continue;
6897 }
6898 if (snap.checked !== null) {
6899 field.checked = snap.checked;
6900 if (snap.checked) {
6901 field.setAttribute("checked", "");
6902 } else {
6903 field.removeAttribute("checked");
6904 }
6905 continue;
6906 }
6907 this._writeField(field, snap.value);
6908 }
6909 this.dispatchEvent(
6910 new CustomEvent("wpd-form-reset", {
6911 bubbles: true,
6912 composed: true,
6913 detail: { form: this }
6914 })
6915 );
6916 }
6917 /**
6918 * Programmatic submit. Same path the submit button + Enter key
6919 * take. Runs required-field validation, then dispatches a
6920 * cancellable `wpd-form-submit`.
6921 */
6922 submit() {
6923 const failures = [];
6924 for (const field of this._namedFields()) {
6925 const name = field.getAttribute("name");
6926 if (!name) {
6927 continue;
6928 }
6929 const required = field.hasAttribute("required");
6930 if (!required) {
6931 continue;
6932 }
6933 const value = this._readField(field);
6934 const empty = value === null || value === void 0 || value === "" || Array.isArray(value) && value.length === 0;
6935 if (empty) {
6936 field.setAttribute("invalid", "");
6937 const labelAttr = field.getAttribute("label");
6938 failures.push(labelAttr || name);
6939 }
6940 }
6941 if (failures.length > 0) {
6942 const list = failures.join(", ");
6943 this.setError(`Required: ${list}`);
6944 return;
6945 }
6946 const values = this.getValues();
6947 const event = new CustomEvent("wpd-form-submit", {
6948 bubbles: true,
6949 composed: true,
6950 cancelable: true,
6951 detail: { values, form: this }
6952 });
6953 this.dispatchEvent(event);
6954 }
6955 // ─── Internals ───────────────────────────────────────────────────
6956 _captureInitialValues() {
6957 if (this._captured) {
6958 return;
6959 }
6960 const fields = this._namedFields();
6961 if (fields.length === 0) {
6962 return;
6963 }
6964 for (const field of fields) {
6965 const name = field.getAttribute("name");
6966 if (!name) {
6967 continue;
6968 }
6969 const isCheckbox = field.tagName === "WPD-CHECKBOX" || field.tagName === "WPD-CHECKBOX-LABEL" || field.tagName === "INPUT" && field.type === "checkbox";
6970 this._initial.set(name, {
6971 value: this._readField(field),
6972 checked: isCheckbox ? Boolean(field.checked) : null
6973 });
6974 }
6975 this._captured = true;
6976 }
6977 _namedFields() {
6978 return Array.from(
6979 this.querySelectorAll("[name]")
6980 );
6981 }
6982 _fieldByName(name) {
6983 const safe = typeof CSS !== "undefined" && typeof CSS.escape === "function" ? CSS.escape(name) : name.replace(/["\\]/g, "\\$&");
6984 return this.querySelector(`[name="${safe}"]`);
6985 }
6986 _readField(field) {
6987 const tag = field.tagName.toUpperCase();
6988 const isCheckbox = tag === "WPD-CHECKBOX" || tag === "WPD-CHECKBOX-LABEL" || tag === "INPUT" && field.type === "checkbox";
6989 if (isCheckbox) {
6990 if (typeof field.checked === "boolean") {
6991 return field.checked;
6992 }
6993 return field.hasAttribute("checked");
6994 }
6995 if (field.value !== void 0 && field.value !== null) {
6996 return field.value;
6997 }
6998 return field.getAttribute("value") ?? "";
6999 }
7000 _writeField(field, value) {
7001 const tag = field.tagName.toUpperCase();
7002 const isCheckbox = tag === "WPD-CHECKBOX" || tag === "WPD-CHECKBOX-LABEL" || tag === "INPUT" && field.type === "checkbox";
7003 if (isCheckbox) {
7004 const next = Boolean(value);
7005 field.checked = next;
7006 if (next) {
7007 field.setAttribute("checked", "");
7008 } else {
7009 field.removeAttribute("checked");
7010 }
7011 return;
7012 }
7013 const str = value === null || value === void 0 ? "" : String(value);
7014 field.value = str;
7015 field.setAttribute("value", str);
7016 }
7017 _onAnyFieldInput(e) {
7018 const target = e.target;
7019 if (!target) {
7020 return;
7021 }
7022 const name = target.getAttribute?.("name");
7023 if (!name) {
7024 return;
7025 }
7026 this.dispatchEvent(
7027 new CustomEvent("wpd-form-input", {
7028 bubbles: true,
7029 composed: true,
7030 detail: {
7031 name,
7032 value: this._readField(target),
7033 form: this
7034 }
7035 })
7036 );
7037 if (target.hasAttribute("invalid")) {
7038 target.removeAttribute("invalid");
7039 }
7040 }
7041 };
7042 _WpdForm.props = [
7043 "submit-label",
7044 "reset-label",
7045 "error",
7046 "busy",
7047 "columns",
7048 "min-column",
7049 "show-reset",
7050 "align"
7051 ];
7052 _WpdForm.styles = [wpdFormStyles];
7053 _WpdForm.help = {
7054 title: "Form",
7055 summary: "Container-query-driven responsive form. Auto-collects named fields, validates required, exposes setError / setFieldInvalid / setBusy / reset, fires wpd-form-submit with the collected values map.",
7056 status: "experimental",
7057 since: "0.18.0",
7058 props: [
7059 {
7060 name: "submit-label",
7061 type: "string",
7062 default: "Submit",
7063 description: "Label of the primary submit button."
7064 },
7065 {
7066 name: "reset-label",
7067 type: "string",
7068 default: "Reset",
7069 description: "Label of the reset button."
7070 },
7071 {
7072 name: "error",
7073 type: "string",
7074 description: "Top-of-form error banner. Show / hide via attribute OR setError(); equivalent."
7075 },
7076 {
7077 name: "busy",
7078 type: "boolean attribute",
7079 description: "Loading state — disables the form + flashes a spinner."
7080 },
7081 {
7082 name: "columns",
7083 type: '"auto" | "1" | "2" | "3"',
7084 default: "auto",
7085 description: 'Fixed column count, or "auto" for container-query 1↔2 (or up to 3 above 760px).'
7086 },
7087 {
7088 name: "show-reset",
7089 type: "boolean attribute",
7090 default: "true",
7091 description: 'Whether the reset button is rendered. Set to "false" / omit the attribute to hide it.'
7092 },
7093 {
7094 name: "align",
7095 type: '"end" | "start" | "stretch"',
7096 default: "end",
7097 description: "Footer button alignment."
7098 }
7099 ],
7100 slots: [
7101 { name: "(default)", description: "Form fields. `[name]` descendants are auto-collected." },
7102 { name: "header", description: "Heading / lede above the fields." },
7103 { name: "error", description: "Custom error UI; replaces the default banner when slotted." },
7104 { name: "footer-leading", description: "Extras left of the action buttons." },
7105 { name: "footer-trailing", description: "Extras right of the action buttons." }
7106 ],
7107 events: [
7108 {
7109 name: "wpd-form-submit",
7110 description: "Cancellable. Fires on submit after required-field validation passes.",
7111 detail: "{ values: Record<string, unknown>, form: WpdForm }"
7112 },
7113 {
7114 name: "wpd-form-reset",
7115 description: "Fires after fields have been restored to their initial values.",
7116 detail: "{ form: WpdForm }"
7117 },
7118 {
7119 name: "wpd-form-input",
7120 description: "Bubbles every keystroke / change inside any descendant field; useful for live validation.",
7121 detail: "{ name: string, value: unknown, form: WpdForm }"
7122 }
7123 ],
7124 example: html`
7125 <wpd-form submit-label="Add user">
7126 <wpd-text-field name="username" label="Username" required></wpd-text-field>
7127 <wpd-text-field name="email" type="email" label="Email" required></wpd-text-field>
7128 <wpd-text-field name="password" label="Password" full-width></wpd-text-field>
7129 </wpd-form>
7130 `
7131 };
7132 let WpdForm = _WpdForm;
7133 defineComponent("wpd-form", WpdForm);
7134 const textareaStyles = css`:host{display:flex;flex-direction:column;gap:4px;font-size:13px;color:var( --desktop-mode-text,#1d2327 );min-width:0}:host( [ hidden ] ){display:none}.wpd-textarea__label{font-size:12px;color:var( --desktop-mode-muted,#646970 )}textarea{appearance:none;-webkit-appearance:none;display:block;width:100%;min-width:0;box-sizing:border-box;padding:8px 10px;background:var( --desktop-mode-window-bg,#fff );border:1px solid var( --desktop-mode-border,#dcdcde );border-radius:6px;font:inherit;font-size:13px;line-height:1.45;color:var( --desktop-mode-text,#1d2327 );resize:vertical;transition:border-color 0.12s ease,box-shadow 0.12s ease}textarea:hover{border-color:var( --desktop-mode-muted,#8c8f94 )}textarea:focus-visible{outline:none;border-color:var( --wp-admin-theme-color,#2271b1 );box-shadow:0 0 0 1px var( --wp-admin-theme-color,#2271b1 )}textarea:disabled{opacity:0.55;cursor:not-allowed;background:rgba( 0,0,0,0.03 )}textarea[ aria-invalid='true' ]{border-color:#d63638}textarea[ aria-invalid='true' ]:focus-visible{box-shadow:0 0 0 1px #d63638}:host( [ auto-grow ] ) textarea{resize:none;overflow:hidden}`;
7135 const _WpdTextarea = class _WpdTextarea extends Component {
7136 constructor() {
7137 super(...arguments);
7138 this._textareaEl = null;
7139 }
7140 connectedCallback() {
7141 super.connectedCallback();
7142 ensureAutoId(this);
7143 }
7144 render() {
7145 const label = this._attr("label") || "";
7146 const value = this._attr("value") ?? "";
7147 const placeholder = this._attr("placeholder") || "";
7148 const disabled = this._boolAttr("disabled");
7149 const readonly = this._boolAttr("readonly");
7150 const ariaLabel = this._attr("aria-label") || label;
7151 const name = this._attr("name") || "";
7152 const rows = Number(this._attr("rows")) || 3;
7153 const maxLength = this._attr("maxlength");
7154 const minLength = this._attr("minlength");
7155 const invalid = this._boolAttr("invalid");
7156 const hostId = this.id || "wpd-unnamed";
7157 const fieldId = `${hostId}__field`;
7158 return html`
7159 ${label ? html`<label class="wpd-textarea__label" for=${fieldId}>${label}</label>` : html``}
7160 <textarea
7161 id=${fieldId}
7162 part="textarea"
7163 .value=${value}
7164 placeholder=${placeholder}
7165 ?disabled=${disabled}
7166 ?readonly=${readonly}
7167 rows=${rows}
7168 maxlength=${maxLength ?? ""}
7169 minlength=${minLength ?? ""}
7170 name=${name}
7171 aria-invalid=${invalid ? "true" : "false"}
7172 aria-label=${ariaLabel || ""}
7173 @input=${(e) => this._onInput(e)}
7174 @change=${(e) => this._onChange(e)}
7175 @keydown=${(e) => this._onKeyDown(e)}
7176 ></textarea>
7177 `;
7178 }
7179 _attr(name) {
7180 return this.getAttribute(name);
7181 }
7182 _boolAttr(name) {
7183 return this.getAttribute(name) !== null;
7184 }
7185 _onInput(e) {
7186 const ta = e.target;
7187 this._textareaEl = ta;
7188 this.setAttribute("value", ta.value);
7189 this.emit("wpd-input-change", { value: ta.value });
7190 if (this._boolAttr("auto-grow")) {
7191 this._autosize(ta);
7192 }
7193 }
7194 _onChange(e) {
7195 const ta = e.target;
7196 this.emit("wpd-input-commit", { value: ta.value });
7197 }
7198 _onKeyDown(e) {
7199 if (!this._boolAttr("submit-on-enter")) {
7200 return;
7201 }
7202 if (e.key === "Enter" && !e.shiftKey && !e.altKey && !e.metaKey && !e.ctrlKey) {
7203 e.preventDefault();
7204 const ta = e.target;
7205 this.emit("wpd-submit", { value: ta.value });
7206 }
7207 }
7208 /**
7209 * Grow the textarea height to fit content, capped at `max-rows`.
7210 * Resets to scroll-height each input then clamps; cheap because
7211 * the browser caches layout.
7212 */
7213 _autosize(ta) {
7214 const maxRows = Number(this._attr("max-rows")) || 8;
7215 const cs = window.getComputedStyle(ta);
7216 const fontSize = parseFloat(cs.fontSize) || 13;
7217 const lineHeightRaw = cs.lineHeight;
7218 const lineHeight = lineHeightRaw === "normal" ? fontSize * 1.45 : parseFloat(lineHeightRaw) || fontSize * 1.45;
7219 const paddingTop = parseFloat(cs.paddingTop) || 0;
7220 const paddingBottom = parseFloat(cs.paddingBottom) || 0;
7221 const max = lineHeight * maxRows + paddingTop + paddingBottom;
7222 ta.style.height = "auto";
7223 const next = Math.min(ta.scrollHeight, max);
7224 ta.style.height = `${next}px`;
7225 }
7226 /** Public helper for callers that programmatically set `.value` and want autosize to re-run. */
7227 refreshAutosize() {
7228 if (this._textareaEl && this._boolAttr("auto-grow")) {
7229 this._autosize(this._textareaEl);
7230 }
7231 }
7232 /** Imperatively focus the underlying textarea. */
7233 focusInput() {
7234 const root = this.shadowRoot ?? this;
7235 const ta = root.querySelector("textarea");
7236 ta?.focus();
7237 }
7238 /** Imperatively clear the value. */
7239 clear() {
7240 this.setAttribute("value", "");
7241 const root = this.shadowRoot ?? this;
7242 const ta = root.querySelector("textarea");
7243 if (ta) {
7244 ta.value = "";
7245 if (this._boolAttr("auto-grow")) {
7246 this._autosize(ta);
7247 }
7248 }
7249 }
7250 };
7251 _WpdTextarea.props = [
7252 "label",
7253 "value",
7254 "placeholder",
7255 "disabled",
7256 "readonly",
7257 "ariaLabel",
7258 "name",
7259 "rows",
7260 "maxlength",
7261 "minlength",
7262 "invalid",
7263 "autoGrow",
7264 "maxRows",
7265 "submitOnEnter"
7266 ];
7267 _WpdTextarea.styles = [textareaStyles];
7268 _WpdTextarea.help = {
7269 title: "Textarea",
7270 summary: "Multi-line text input. Same event shape as wpd-text-field. Optional auto-grow up to max-rows; optional submit-on-enter (Enter sends, Shift+Enter newlines).",
7271 status: "stable",
7272 since: "0.22.0",
7273 props: [
7274 { name: "label", type: "string", description: "Visible label above the textarea." },
7275 { name: "value", type: "string", description: "Current value; reflected two-way." },
7276 { name: "placeholder", type: "string", description: "Native placeholder." },
7277 { name: "disabled", type: "boolean attribute" },
7278 { name: "readonly", type: "boolean attribute" },
7279 { name: "aria-label", type: "string", description: "Accessible label when no visible label is rendered." },
7280 { name: "name", type: "string", description: "Forwarded to native textarea for form submission." },
7281 { name: "rows", type: "integer (string)", default: "3", description: "Initial visible row count." },
7282 { name: "maxlength", type: "integer (string)" },
7283 { name: "minlength", type: "integer (string)" },
7284 { name: "invalid", type: "boolean attribute", description: "Sets aria-invalid + error styling." },
7285 { name: "auto-grow", type: "boolean attribute", description: "Grows up to max-rows as the user types." },
7286 { name: "max-rows", type: "integer (string)", default: "8" },
7287 {
7288 name: "submit-on-enter",
7289 type: "boolean attribute",
7290 description: "Enter fires wpd-submit; Shift+Enter inserts a newline."
7291 }
7292 ],
7293 events: [
7294 { name: "wpd-input-change", description: "Fires on every keystroke.", detail: "{ value: string }" },
7295 { name: "wpd-input-commit", description: "Fires on blur / native change.", detail: "{ value: string }" },
7296 {
7297 name: "wpd-submit",
7298 description: "Fires on Enter (without Shift) when submit-on-enter is set.",
7299 detail: "{ value: string }"
7300 }
7301 ],
7302 example: html`
7303 <wpd-textarea label="Message" rows="3" auto-grow max-rows="8" submit-on-enter></wpd-textarea>
7304 `
7305 };
7306 let WpdTextarea = _WpdTextarea;
7307 defineComponent("wpd-textarea", WpdTextarea);
7308 let _mountsPromise = null;
7309 function loadMounts() {
7310 if (!_mountsPromise) {
7311 _mountsPromise = Promise.resolve().then(() => userEditRender);
7312 }
7313 return _mountsPromise;
7314 }
7315 class WpdUserProfile extends HTMLElement {
7316 constructor() {
7317 super(...arguments);
7318 this._initialized = false;
7319 this._mountedFor = null;
7320 }
7321 static get observedAttributes() {
7322 return ["user-id"];
7323 }
7324 connectedCallback() {
7325 if (!this._initialized) {
7326 this._initialized = true;
7327 this._renderShell();
7328 }
7329 void this._mountIfNeeded();
7330 }
7331 attributeChangedCallback(name, oldValue, newValue) {
7332 if (name !== "user-id" || oldValue === newValue) {
7333 return;
7334 }
7335 if (this._initialized) {
7336 void this._mountIfNeeded();
7337 }
7338 }
7339 /**
7340 * Build the layout shell (sidebar + main column + activity
7341 * region). Same class names as the inline Profile tab in the
7342 * Users window so the existing posts-window.css rules style
7343 * both contexts identically.
7344 */
7345 _renderShell() {
7346 this.classList.add("desktop-mode-user-profile");
7347 this.innerHTML = `
7348 <div class="desktop-mode-users__edit-layout" data-wpd-user-profile-layout>
7349 <aside class="desktop-mode-users__edit-aside" data-wpd-user-profile-aside></aside>
7350 <main class="desktop-mode-users__edit-main">
7351 <div data-wpd-user-profile-form></div>
7352 <div class="desktop-mode-users__edit-activity" data-wpd-user-profile-activity></div>
7353 </main>
7354 </div>
7355 `;
7356 }
7357 async _mountIfNeeded() {
7358 const userIdAttr = this.getAttribute("user-id");
7359 const userId = userIdAttr ? parseInt(userIdAttr, 10) : 0;
7360 if (!Number.isFinite(userId) || userId <= 0) {
7361 return;
7362 }
7363 if (userId === this._mountedFor) {
7364 return;
7365 }
7366 this._mountedFor = userId;
7367 const formHost = this.querySelector(
7368 "[data-wpd-user-profile-form]"
7369 );
7370 const asideHost = this.querySelector(
7371 "[data-wpd-user-profile-aside]"
7372 );
7373 const activityHost = this.querySelector(
7374 "[data-wpd-user-profile-activity]"
7375 );
7376 if (!formHost || !asideHost || !activityHost) {
7377 return;
7378 }
7379 const mounts = await loadMounts();
7380 void mounts.mountProfileFormAt(formHost, userId);
7381 void mounts.mountProfileAsideAt(asideHost, userId, false);
7382 void mounts.mountProfileActivityAt(activityHost, userId, false);
7383 }
7384 }
7385 if (typeof customElements !== "undefined" && !customElements.get("wpd-user-profile")) {
7386 customElements.define("wpd-user-profile", WpdUserProfile);
7387 }
7388 const FALLBACK_BASE = "http://localhost/";
7389 function joinRestUrl(restRoot, path) {
7390 const base = typeof window !== "undefined" && window.location ? window.location.href : FALLBACK_BASE;
7391 const url = new URL(restRoot, base);
7392 const trimmed = path.replace(/^\/+/, "");
7393 const queryAt = trimmed.indexOf("?");
7394 const route = queryAt === -1 ? trimmed : trimmed.slice(0, queryAt);
7395 const extraQuery = queryAt === -1 ? "" : trimmed.slice(queryAt + 1);
7396 if (url.searchParams.has("rest_route")) {
7397 const existing = url.searchParams.get("rest_route") ?? "/";
7398 const prefix = existing.endsWith("/") ? existing : existing + "/";
7399 url.searchParams.set("rest_route", prefix + route);
7400 } else {
7401 const pathname = url.pathname.endsWith("/") ? url.pathname : url.pathname + "/";
7402 url.pathname = pathname + route;
7403 }
7404 if (extraQuery) {
7405 const extras = new URLSearchParams(extraQuery);
7406 extras.forEach((value, key) => {
7407 url.searchParams.append(key, value);
7408 });
7409 }
7410 return url.toString();
7411 }
7412 function broadcastTermChange(taxonomy, action, id) {
7413 const api = window.wp?.desktop;
7414 if (api && typeof api.broadcast === "function") {
7415 api.broadcast("desktop-mode.term.changed", {
7416 source: "posts-window",
7417 taxonomy,
7418 action,
7419 id
7420 });
7421 }
7422 }
7423 function createPostsWindowClient(windowId) {
7424 const getConfig = () => {
7425 const store = window.desktopModeWindowConfig;
7426 const cfg = store ? store[windowId] : void 0;
7427 if (!cfg) {
7428 throw new Error(
7429 `[${windowId}] config blob is missing — was the window opened without registration? See the matching \`desktop_mode_register_window()\` call in \`includes/{posts,pages}-window/window.php\`.`
7430 );
7431 }
7432 return cfg;
7433 };
7434 const shellFetch = (input, init) => {
7435 return trackedFetch(input, init, { windowId });
7436 };
7437 const request = async (url, init = {}) => {
7438 const cfg = getConfig();
7439 const response = await shellFetch(url, {
7440 ...init,
7441 credentials: "same-origin",
7442 headers: {
7443 "X-WP-Nonce": cfg.restNonce,
7444 Accept: "application/json",
7445 ...init.body ? { "Content-Type": "application/json" } : {},
7446 ...init.headers ?? {}
7447 }
7448 });
7449 if (!response.ok) {
7450 let message = `${response.status} ${response.statusText}`;
7451 try {
7452 const json = await response.json();
7453 if (json && typeof json.message === "string") {
7454 message = json.message;
7455 }
7456 } catch {
7457 }
7458 throw new Error(message);
7459 }
7460 const data = init.expectJson === false ? null : await response.json();
7461 return { data, headers: response.headers };
7462 };
7463 const fetchPosts = async (params = {}) => {
7464 const cfg = getConfig();
7465 const url = new URL(cfg.postsUrl);
7466 for (const [key, value] of Object.entries(cfg.queryArgs ?? {})) {
7467 if (typeof value === "string" && value !== "") {
7468 url.searchParams.set(key, value);
7469 }
7470 }
7471 if (params.page) {
7472 url.searchParams.set("page", String(params.page));
7473 }
7474 if (params.perPage) {
7475 url.searchParams.set("per_page", String(params.perPage));
7476 }
7477 if (params.search) {
7478 url.searchParams.set("search", params.search);
7479 }
7480 if (params.status) {
7481 url.searchParams.set("status", params.status);
7482 } else {
7483 url.searchParams.set("status", "any");
7484 }
7485 if (params.orderby) {
7486 url.searchParams.set("orderby", params.orderby);
7487 }
7488 if (params.order) {
7489 url.searchParams.set("order", params.order);
7490 }
7491 const appendIds = (key, v) => {
7492 const list = Array.isArray(v) ? v : [v];
7493 for (const id of list) {
7494 if (Number.isFinite(id) && id > 0) {
7495 url.searchParams.append(`${key}[]`, String(id));
7496 }
7497 }
7498 };
7499 if (params.author) {
7500 appendIds("author", params.author);
7501 }
7502 if (params.tag) {
7503 appendIds("tags", params.tag);
7504 }
7505 const { data, headers } = await request(
7506 url.toString(),
7507 { method: "GET" }
7508 );
7509 return {
7510 items: Array.isArray(data) ? data : [],
7511 total: parseInt(headers.get("X-WP-Total") ?? "0", 10) || 0,
7512 totalPages: parseInt(headers.get("X-WP-TotalPages") ?? "0", 10) || 0
7513 };
7514 };
7515 const trashPost = async (id) => {
7516 const cfg = getConfig();
7517 try {
7518 await request(`${cfg.postsUrl}/${id}`, {
7519 method: "DELETE"
7520 });
7521 return { id, ok: true };
7522 } catch (err) {
7523 return {
7524 id,
7525 ok: false,
7526 error: err instanceof Error ? err.message : String(err)
7527 };
7528 }
7529 };
7530 const buildEditPostUrl = (id) => {
7531 const cfg = getConfig();
7532 const sep = cfg.editPostUrlBase.includes("?") ? "&" : "?";
7533 return `${cfg.editPostUrlBase}${sep}post=${id}&action=edit`;
7534 };
7535 const searchTags = async (query, signal) => {
7536 const cfg = getConfig();
7537 const url = new URL(joinRestUrl(cfg.restRoot, "wp/v2/tags"));
7538 url.searchParams.set("per_page", "20");
7539 url.searchParams.set("_fields", "id,name,slug,count");
7540 url.searchParams.set("orderby", "count");
7541 url.searchParams.set("order", "desc");
7542 if (query) {
7543 url.searchParams.set("search", query);
7544 url.searchParams.set("orderby", "name");
7545 url.searchParams.set("order", "asc");
7546 }
7547 const { data } = await request(url.toString(), {
7548 method: "GET",
7549 signal
7550 });
7551 return Array.isArray(data) ? data : [];
7552 };
7553 const createTag = async (name) => {
7554 const cfg = getConfig();
7555 const url = joinRestUrl(cfg.restRoot, "wp/v2/tags");
7556 try {
7557 const { data } = await request(url, {
7558 method: "POST",
7559 body: JSON.stringify({ name })
7560 });
7561 broadcastTermChange("post_tag", "created", data.id);
7562 return data;
7563 } catch (err) {
7564 const message = err instanceof Error ? err.message : String(err);
7565 if (/term[\s_]?exists/i.test(message)) {
7566 const matches = await searchTags(name);
7567 const exact = matches.find(
7568 (t) => t.name.toLowerCase() === name.toLowerCase()
7569 );
7570 if (exact) {
7571 return exact;
7572 }
7573 }
7574 throw err;
7575 }
7576 };
7577 const updatePostTags = async (postId, tagIds) => {
7578 const cfg = getConfig();
7579 const url = `${cfg.postsUrl}/${postId}`;
7580 const { data } = await request(url, {
7581 method: "POST",
7582 body: JSON.stringify({ tags: tagIds })
7583 });
7584 return data;
7585 };
7586 const fetchAllCategories = async (signal) => {
7587 const cfg = getConfig();
7588 const url = new URL(joinRestUrl(cfg.restRoot, "wp/v2/categories"));
7589 url.searchParams.set("per_page", "100");
7590 url.searchParams.set("_fields", "id,name,slug,parent");
7591 url.searchParams.set("orderby", "name");
7592 url.searchParams.set("order", "asc");
7593 const { data } = await request(url.toString(), {
7594 method: "GET",
7595 signal
7596 });
7597 return Array.isArray(data) ? data : [];
7598 };
7599 const fetchAuthorOptions = async (signal) => {
7600 const cfg = getConfig();
7601 const url = new URL(joinRestUrl(cfg.restRoot, "wp/v2/users"));
7602 url.searchParams.set("per_page", "100");
7603 url.searchParams.set("who", "authors");
7604 url.searchParams.set("_fields", "id,name");
7605 url.searchParams.set("orderby", "name");
7606 url.searchParams.set("order", "asc");
7607 try {
7608 const { data } = await request(url.toString(), {
7609 method: "GET",
7610 signal
7611 });
7612 return Array.isArray(data) ? data : [];
7613 } catch {
7614 return [];
7615 }
7616 };
7617 const fetchTagOptions = async (page = 1, perPage = 50, signal) => {
7618 const cfg = getConfig();
7619 const url = new URL(joinRestUrl(cfg.restRoot, "wp/v2/tags"));
7620 url.searchParams.set("per_page", String(Math.max(1, perPage)));
7621 url.searchParams.set("page", String(Math.max(1, page)));
7622 url.searchParams.set("_fields", "id,name,count");
7623 url.searchParams.set("orderby", "count");
7624 url.searchParams.set("order", "desc");
7625 try {
7626 const { data, headers } = await request(
7627 url.toString(),
7628 { method: "GET", signal }
7629 );
7630 return {
7631 items: Array.isArray(data) ? data : [],
7632 totalPages: parseInt(headers.get("X-WP-TotalPages") ?? "0", 10) || 0
7633 };
7634 } catch {
7635 return { items: [], totalPages: 0 };
7636 }
7637 };
7638 const createCategory = async (name, parent = 0, opts = {}) => {
7639 const cfg = getConfig();
7640 const url = joinRestUrl(cfg.restRoot, "wp/v2/categories");
7641 const body = { name, parent };
7642 if (opts.slug) {
7643 body.slug = opts.slug;
7644 }
7645 if (opts.description) {
7646 body.description = opts.description;
7647 }
7648 try {
7649 const { data } = await request(url, {
7650 method: "POST",
7651 body: JSON.stringify(body)
7652 });
7653 broadcastTermChange("category", "created", data.id);
7654 return data;
7655 } catch (err) {
7656 const message = err instanceof Error ? err.message : String(err);
7657 if (/term[\s_]?exists/i.test(message)) {
7658 const matches = await fetchAllCategories();
7659 const exact = matches.find(
7660 (t) => t.name.toLowerCase() === name.toLowerCase() && t.parent === parent
7661 );
7662 if (exact) {
7663 return exact;
7664 }
7665 }
7666 throw err;
7667 }
7668 };
7669 const updatePostCategories = async (postId, categoryIds) => {
7670 const cfg = getConfig();
7671 const url = `${cfg.postsUrl}/${postId}`;
7672 const { data } = await request(
7673 url,
7674 {
7675 method: "POST",
7676 body: JSON.stringify({ categories: categoryIds })
7677 }
7678 );
7679 return data;
7680 };
7681 const fetchTerms = async (taxonomy, params = {}) => {
7682 const cfg = getConfig();
7683 const url = new URL(joinRestUrl(cfg.restRoot, `wp/v2/${taxonomy}`));
7684 url.searchParams.set("per_page", String(params.perPage ?? 50));
7685 url.searchParams.set("page", String(params.page ?? 1));
7686 url.searchParams.set(
7687 "_fields",
7688 "id,name,slug,parent,count,description,desktop_mode_count,desktop_mode_is_default"
7689 );
7690 url.searchParams.set("orderby", params.orderby ?? "name");
7691 url.searchParams.set("order", params.order ?? "asc");
7692 if (params.search) {
7693 url.searchParams.set("search", params.search);
7694 }
7695 if (typeof params.parent === "number" && params.parent >= 0) {
7696 url.searchParams.set("parent", String(params.parent));
7697 }
7698 const { data, headers } = await request(
7699 url.toString(),
7700 { method: "GET" }
7701 );
7702 const items = Array.isArray(data) ? data.map((t) => {
7703 const anyCount = t.desktop_mode_count;
7704 const isDefault = t.desktop_mode_is_default === true;
7705 return {
7706 id: t.id ?? 0,
7707 name: t.name ?? "",
7708 slug: t.slug ?? "",
7709 parent: t.parent ?? 0,
7710 count: typeof anyCount === "number" ? anyCount : t.count ?? 0,
7711 description: t.description ?? "",
7712 isDefault
7713 };
7714 }) : [];
7715 return {
7716 items,
7717 total: parseInt(headers.get("X-WP-Total") ?? "0", 10) || 0,
7718 totalPages: parseInt(headers.get("X-WP-TotalPages") ?? "0", 10) || 0
7719 };
7720 };
7721 const fetchTagCooccurrence = async (taxonomy = "tags", limit = 8) => {
7722 const cfg = getConfig();
7723 const url = new URL(
7724 joinRestUrl(
7725 cfg.restRoot,
7726 "desktop-mode/v1/tag-cooccurrence"
7727 )
7728 );
7729 url.searchParams.set(
7730 "taxonomy",
7731 taxonomy === "tags" ? "post_tag" : "category"
7732 );
7733 url.searchParams.set("limit", String(limit));
7734 const { data } = await request(url.toString(), { method: "GET" });
7735 const out = /* @__PURE__ */ new Map();
7736 const pairs = data && typeof data === "object" && !Array.isArray(data) ? data.pairs : void 0;
7737 if (!pairs) {
7738 return out;
7739 }
7740 for (const [key, neighbors] of Object.entries(pairs)) {
7741 const id = parseInt(key, 10);
7742 if (!Number.isFinite(id) || id <= 0) {
7743 continue;
7744 }
7745 const clean = [];
7746 for (const raw of neighbors) {
7747 const nid = Number(raw?.id);
7748 const sh = Number(raw?.shared);
7749 if (Number.isFinite(nid) && nid > 0 && Number.isFinite(sh) && sh > 0) {
7750 clean.push({ id: nid, shared: sh });
7751 }
7752 }
7753 if (clean.length > 0) {
7754 out.set(id, clean);
7755 }
7756 }
7757 return out;
7758 };
7759 const updateTerm = async (taxonomy, id, patch) => {
7760 const cfg = getConfig();
7761 const url = joinRestUrl(cfg.restRoot, `wp/v2/${taxonomy}/${id}`);
7762 const { data } = await request(url, {
7763 method: "POST",
7764 body: JSON.stringify(patch)
7765 });
7766 broadcastTermChange(
7767 taxonomy === "categories" ? "category" : "post_tag",
7768 "updated",
7769 id
7770 );
7771 return {
7772 id: data.id ?? id,
7773 name: data.name ?? "",
7774 slug: data.slug ?? "",
7775 parent: data.parent ?? 0,
7776 count: data.count ?? 0,
7777 description: data.description ?? "",
7778 isDefault: data.isDefault ?? false
7779 };
7780 };
7781 const deleteTerm = async (taxonomy, id) => {
7782 const cfg = getConfig();
7783 const url = new URL(
7784 joinRestUrl(cfg.restRoot, `wp/v2/${taxonomy}/${id}`)
7785 );
7786 url.searchParams.set("force", "true");
7787 await request(url.toString(), { method: "DELETE" });
7788 broadcastTermChange(
7789 taxonomy === "categories" ? "category" : "post_tag",
7790 "deleted",
7791 id
7792 );
7793 };
7794 return {
7795 windowId,
7796 getConfig,
7797 fetchPosts,
7798 trashPost,
7799 buildEditPostUrl,
7800 searchTags,
7801 createTag,
7802 updatePostTags,
7803 fetchAllCategories,
7804 fetchAuthorOptions,
7805 fetchTagOptions,
7806 createCategory,
7807 updatePostCategories,
7808 fetchTerms,
7809 fetchTagCooccurrence,
7810 updateTerm,
7811 deleteTerm
7812 };
7813 }
7814 function createUsersWindowClient(windowId = "desktop-mode-users") {
7815 const getConfig = () => {
7816 const store = window.desktopModeWindowConfig;
7817 const cfg = store?.[windowId];
7818 if (!cfg) {
7819 throw new Error(
7820 `[${windowId}] config blob is missing — was the window opened without registration? See \`includes/users-window/window.php\`.`
7821 );
7822 }
7823 return cfg;
7824 };
7825 const shellFetch = (input, init, options) => {
7826 return trackedFetch(input, init, {
7827 windowId,
7828 source: options?.source ?? "users-window/rest",
7829 silent: options?.silent
7830 });
7831 };
7832 const fetchUsers = async (params) => {
7833 const cfg = getConfig();
7834 const baseUrl = cfg.usersUrl || cfg.postsUrl;
7835 const url = new URL(baseUrl);
7836 for (const [key, value] of Object.entries(cfg.queryArgs ?? {})) {
7837 if (typeof value === "string" && value !== "") {
7838 url.searchParams.set(key, value);
7839 }
7840 }
7841 url.searchParams.set("page", String(Math.max(1, params.page)));
7842 url.searchParams.set(
7843 "per_page",
7844 String(Math.max(1, params.perPage))
7845 );
7846 if (params.search) {
7847 url.searchParams.set("search", params.search);
7848 }
7849 if (params.roles && params.roles.length > 0) {
7850 for (const r of params.roles) {
7851 url.searchParams.append("roles", r);
7852 }
7853 }
7854 if (params.orderby) {
7855 url.searchParams.set("orderby", params.orderby);
7856 }
7857 if (params.order) {
7858 url.searchParams.set("order", params.order);
7859 }
7860 const res = await shellFetch(
7861 url.toString(),
7862 {
7863 method: "GET",
7864 credentials: "same-origin",
7865 headers: {
7866 Accept: "application/json",
7867 "X-WP-Nonce": cfg.restNonce
7868 }
7869 },
7870 { source: "users-window/list" }
7871 );
7872 if (!res.ok) {
7873 throw new Error(
7874 `[users-window] list fetch failed: ${res.status}`
7875 );
7876 }
7877 const items = await res.json();
7878 const total = parseInt(res.headers.get("X-WP-Total") ?? "0", 10);
7879 const totalPages = parseInt(
7880 res.headers.get("X-WP-TotalPages") ?? "0",
7881 10
7882 );
7883 return { items, total, totalPages };
7884 };
7885 const fetchOneUser = async (id) => {
7886 const cfg = getConfig();
7887 const baseUrl = cfg.usersUrl || cfg.postsUrl;
7888 const url = new URL(`${baseUrl.replace(/\/$/, "")}/${id}`);
7889 for (const [key, value] of Object.entries(cfg.queryArgs ?? {})) {
7890 if (typeof value === "string" && value !== "") {
7891 url.searchParams.set(key, value);
7892 }
7893 }
7894 const res = await shellFetch(
7895 url.toString(),
7896 {
7897 method: "GET",
7898 credentials: "same-origin",
7899 headers: {
7900 Accept: "application/json",
7901 "X-WP-Nonce": cfg.restNonce
7902 }
7903 },
7904 { source: "users-window/one", silent: true }
7905 );
7906 if (res.status === 404) {
7907 return null;
7908 }
7909 if (!res.ok) {
7910 throw new Error(
7911 `[users-window] one fetch failed: ${res.status}`
7912 );
7913 }
7914 return await res.json();
7915 };
7916 const bulkSetRole = async (ids, role) => {
7917 const cfg = getConfig();
7918 const url = cfg.bulkRoleUrl ?? joinRestUrl(cfg.restRoot, "desktop-mode/v1/users/bulk-role");
7919 const res = await shellFetch(
7920 url,
7921 {
7922 method: "POST",
7923 credentials: "same-origin",
7924 headers: {
7925 "Content-Type": "application/json",
7926 "X-WP-Nonce": cfg.restNonce
7927 },
7928 body: JSON.stringify({ ids, role })
7929 },
7930 { source: "users-window/bulk-role" }
7931 );
7932 if (!res.ok) {
7933 throw new Error(
7934 `[users-window] bulk-role failed: ${res.status}`
7935 );
7936 }
7937 return await res.json();
7938 };
7939 const sendPasswordReset = async (id) => {
7940 const cfg = getConfig();
7941 const base = cfg.sendResetUrlBase ?? joinRestUrl(cfg.restRoot, "desktop-mode/v1/users/");
7942 const res = await shellFetch(
7943 joinRestUrl(base, `${id}/send-password-reset`),
7944 {
7945 method: "POST",
7946 credentials: "same-origin",
7947 headers: {
7948 "Content-Type": "application/json",
7949 "X-WP-Nonce": cfg.restNonce
7950 }
7951 },
7952 { source: "users-window/send-password-reset" }
7953 );
7954 if (!res.ok) {
7955 const body = await res.json().catch(() => ({}));
7956 return {
7957 ok: false,
7958 error: typeof body.code === "string" ? body.code : `http_${res.status}`
7959 };
7960 }
7961 const data = await res.json();
7962 return { ok: data.ok === true, email: data.email };
7963 };
7964 const resendWelcome = async (id) => {
7965 const cfg = getConfig();
7966 const base = cfg.sendResetUrlBase ?? joinRestUrl(cfg.restRoot, "desktop-mode/v1/users/");
7967 const res = await shellFetch(
7968 joinRestUrl(base, `${id}/resend-welcome`),
7969 {
7970 method: "POST",
7971 credentials: "same-origin",
7972 headers: {
7973 "Content-Type": "application/json",
7974 "X-WP-Nonce": cfg.restNonce
7975 }
7976 },
7977 { source: "users-window/resend-welcome" }
7978 );
7979 if (!res.ok) {
7980 const body = await res.json().catch(() => ({}));
7981 return {
7982 ok: false,
7983 error: typeof body.code === "string" ? body.code : `http_${res.status}`
7984 };
7985 }
7986 const data = await res.json();
7987 return { ok: data.ok === true, email: data.email };
7988 };
7989 const createUser = async (body) => {
7990 const cfg = getConfig();
7991 const url = cfg.createUserUrl ?? joinRestUrl(cfg.restRoot, "desktop-mode/v1/users");
7992 const res = await shellFetch(
7993 url,
7994 {
7995 method: "POST",
7996 credentials: "same-origin",
7997 headers: {
7998 "Content-Type": "application/json",
7999 "X-WP-Nonce": cfg.restNonce
8000 },
8001 body: JSON.stringify(body)
8002 },
8003 { source: "users-window/create" }
8004 );
8005 if (!res.ok) {
8006 const data2 = await res.json().catch(() => ({}));
8007 const code = data2.code;
8008 const message = data2.message;
8009 return {
8010 ok: false,
8011 error: typeof code === "string" ? code : `http_${res.status}`,
8012 message: typeof message === "string" ? message : void 0
8013 };
8014 }
8015 const data = await res.json();
8016 return {
8017 ok: data.ok === true,
8018 user_id: data.user_id,
8019 email: data.email
8020 };
8021 };
8022 const bulkDeleteUsers = async (ids, reassign) => {
8023 const cfg = getConfig();
8024 const url = cfg.bulkDeleteUrl ?? joinRestUrl(cfg.restRoot, "desktop-mode/v1/users/bulk-delete");
8025 const body = { ids };
8026 if (typeof reassign === "number" && reassign > 0) {
8027 body.reassign = reassign;
8028 }
8029 const res = await shellFetch(
8030 url,
8031 {
8032 method: "POST",
8033 credentials: "same-origin",
8034 headers: {
8035 "Content-Type": "application/json",
8036 "X-WP-Nonce": cfg.restNonce
8037 },
8038 body: JSON.stringify(body)
8039 },
8040 { source: "users-window/bulk-delete" }
8041 );
8042 if (!res.ok) {
8043 throw new Error(
8044 `[users-window] bulk-delete failed: ${res.status}`
8045 );
8046 }
8047 return await res.json();
8048 };
8049 return {
8050 windowId,
8051 getConfig,
8052 fetchUsers,
8053 fetchOneUser,
8054 bulkSetRole,
8055 sendPasswordReset,
8056 resendWelcome,
8057 createUser,
8058 bulkDeleteUsers
8059 };
8060 }
8061 const styles$2 = css`:host{display:inline-flex}:host( [ fill-cell ] ){display:flex;width:100%}button{appearance:none;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:var( --wpd-button-padding,6px 12px );border-radius:var( --wpd-button-border-radius,6px );font:inherit;font-weight:500;cursor:pointer;transition:background-color 0.12s ease,color 0.12s ease,border-color 0.12s ease;background:var( --wpd-button-bg,transparent );color:var( --wpd-button-fg,var( --desktop-mode-text,#1d2327 ) );border:var( --wpd-button-border,1px solid var( --desktop-mode-border,#c3c4c7 ) )}:host( [ fill-cell ] ) button{width:100%;min-height:var( --wpd-button-min-height,44px )}button:disabled{opacity:0.5;cursor:not-allowed}button:hover:not(:disabled ){background:rgba( 0,0,0,0.04 )}:host( [ variant='primary' ] ) button{background:var( --wpd-button-bg,var( --wp-admin-theme-color,#2271b1 ) );color:var( --wpd-button-fg,#fff );border:var( --wpd-button-border,1px solid transparent )}:host( [ variant='primary' ] ) button:hover:not(:disabled ){filter:brightness( 1.06 );background:var( --wpd-button-bg,var( --wp-admin-theme-color,#2271b1 ) )}:host( [ variant='secondary' ] ) button{background:var( --wpd-button-bg,rgba( 0,0,0,0.06 ) );color:var( --wpd-button-fg,var( --desktop-mode-text,#1d2327 ) );border:var( --wpd-button-border,1px solid transparent )}:host( [ variant='secondary' ] ) button:hover:not(:disabled ){background:var( --wpd-button-bg-hover,rgba( 0,0,0,0.1 ) )}:host( [ variant='danger' ] ) button{background:var( --wpd-button-bg,transparent );color:var( --wpd-button-fg,#d63638 );border:var( --wpd-button-border,1px solid currentColor )}:host( [ variant='danger' ] ) button:hover:not(:disabled ){background:#d63638;color:#fff}:host( [ variant='link' ] ) button{background:transparent;color:var( --wpd-button-fg,var( --wp-admin-theme-color,#2271b1 ) );border:0;padding:0;text-decoration:underline}:host( [ busy ] ) button{pointer-events:none;opacity:0.75}`;
8062 const _WpdButton = class _WpdButton extends Component {
8063 render() {
8064 const disabled = this.disabled !== null;
8065 const type = this.type || "button";
8066 return html`
8067 <button part="button" type=${type} ?disabled=${disabled}>
8068 <slot></slot>
8069 </button>
8070 `;
8071 }
8072 };
8073 _WpdButton.props = ["variant", "disabled", "type", "busy", "fill-cell"];
8074 _WpdButton.styles = [styles$2];
8075 _WpdButton.help = {
8076 title: "Button",
8077 summary: "Thin wrapper around <button> with consistent variant styling and a slot for the label.",
8078 status: "stable",
8079 since: "0.9.0",
8080 props: [
8081 {
8082 name: "variant",
8083 type: "'primary' | 'secondary' | 'ghost' | 'danger' | 'link'",
8084 default: "ghost",
8085 description: "Visual weight of the button. Use primary for the single attention-grabbing action per surface."
8086 },
8087 {
8088 name: "disabled",
8089 type: "boolean attribute",
8090 description: "Disable pointer + keyboard interaction and dim the chrome."
8091 },
8092 {
8093 name: "type",
8094 type: "'button' | 'submit' | 'reset'",
8095 default: "button",
8096 description: "Forwarded to the underlying native <button>."
8097 },
8098 {
8099 name: "busy",
8100 type: "boolean attribute",
8101 description: "Marks the button as in-progress (e.g., awaiting a fetch)."
8102 },
8103 {
8104 name: "fill-cell",
8105 type: "boolean attribute",
8106 description: "Grow to fill the parent flex/grid cell. Useful for tiled keypads."
8107 }
8108 ],
8109 slots: [{ name: "(default)", description: "Button label." }],
8110 parts: [{ name: "button", description: "Underlying <button> element." }],
8111 cssProps: [
8112 { name: "--wpd-button-bg", description: "Background color." },
8113 { name: "--wpd-button-fg", description: "Text color." },
8114 { name: "--wpd-button-border", description: "Border shorthand." },
8115 { name: "--wpd-button-border-radius", default: "6px" },
8116 { name: "--wpd-button-padding", default: "6px 12px" },
8117 {
8118 name: "--wpd-button-min-height",
8119 description: "Minimum height when fill-cell is set."
8120 }
8121 ],
8122 example: html`
8123 <wpd-cluster gap="8">
8124 <wpd-button variant="primary">Primary</wpd-button>
8125 <wpd-button variant="secondary">Secondary</wpd-button>
8126 <wpd-button variant="ghost">Ghost</wpd-button>
8127 <wpd-button variant="danger">Danger</wpd-button>
8128 <wpd-button variant="link">Link</wpd-button>
8129 </wpd-cluster>
8130 `
8131 };
8132 let WpdButton = _WpdButton;
8133 defineComponent("wpd-button", WpdButton);
8134 const segmentedStyles = css`:host{display:inline-flex;padding:3px;background:var( --wpd-segmented-bg,rgba( 0,0,0,0.05 ) );border-radius:7px;gap:2px}`;
8135 const segmentStyles = css`:host{flex:1 1 auto;min-width:0}button{appearance:none;display:block;width:100%;padding:8px 12px;background:transparent;border:0;font:inherit;font-size:13px;color:var( --desktop-mode-muted,#646970 );cursor:pointer;border-radius:5px;transition:background-color 0.12s ease,color 0.12s ease;white-space:nowrap}:host( [ aria-checked='true' ] ) button{background:var( --desktop-mode-window-bg,#fff );color:var( --desktop-mode-text,#1d2327 );box-shadow:0 1px 3px rgba( 0,0,0,0.12 );font-weight:500}`;
8136 const _WpdSegment = class _WpdSegment extends Component {
8137 render() {
8138 this.setAttribute("role", "radio");
8139 return html`
8140 <button type="button" @click=${() => this._onPick()}>
8141 <slot></slot>
8142 </button>
8143 `;
8144 }
8145 _onPick() {
8146 this.emit("wpd-segment-pick", {
8147 value: this.value
8148 });
8149 }
8150 };
8151 _WpdSegment.props = ["value"];
8152 _WpdSegment.styles = [segmentStyles];
8153 _WpdSegment.help = {
8154 title: "Segment",
8155 summary: "Single pill inside a <wpd-segmented> group. Value identifies it for selection; aria-checked is mirrored by the parent.",
8156 status: "stable",
8157 since: "0.9.0",
8158 props: [
8159 {
8160 name: "value",
8161 type: "string",
8162 description: "Identifier this segment contributes to the parent group selection."
8163 }
8164 ],
8165 slots: [
8166 { name: "(default)", description: "Visible segment label." }
8167 ],
8168 events: [
8169 {
8170 name: "wpd-segment-pick",
8171 description: "Internal event bubbled to the parent <wpd-segmented>. Consumers should listen for wpd-pick on the group instead.",
8172 detail: "{ value: string }"
8173 }
8174 ]
8175 };
8176 let WpdSegment = _WpdSegment;
8177 defineComponent("wpd-segment", WpdSegment);
8178 const _WpdSegmented = class _WpdSegmented extends Component {
8179 connectedCallback() {
8180 super.connectedCallback();
8181 this.addEventListener("wpd-segment-pick", (e) => {
8182 const detail = e.detail;
8183 e.stopPropagation();
8184 this.value = detail.value;
8185 this.emit("wpd-pick", { value: detail.value });
8186 });
8187 }
8188 /**
8189 * Declarative item-list setter. Replaces the existing
8190 * `<wpd-segment>` children with a fresh set built from a
8191 * `{ value, label }` array; preserves the current selection
8192 * when the value still matches an entry, otherwise falls back
8193 * to the first item.
8194 *
8195 * Collapses the pre-0.11 imperative dance (clear children,
8196 * `createElement`, set `textContent`, `appendChild`, then
8197 * `setAttribute('value', …)` on the group — order matters) to
8198 * a single assignment:
8199 *
8200 * ```js
8201 * segmented.items = [
8202 * { value: 'm', label: 'm' },
8203 * { value: 'km', label: 'km' },
8204 * ];
8205 * ```
8206 *
8207 * @since 0.11.0
8208 */
8209 set items(list) {
8210 const existing = this.querySelectorAll(":scope > wpd-segment");
8211 for (const el of Array.from(existing)) {
8212 el.remove();
8213 }
8214 for (const item of list) {
8215 const seg = document.createElement("wpd-segment");
8216 seg.setAttribute("value", item.value);
8217 seg.textContent = item.label;
8218 this.appendChild(seg);
8219 }
8220 const current = this.value;
8221 const stillValid = current !== null && list.some((i) => i.value === current);
8222 if (!stillValid && list.length > 0) {
8223 this.value = list[0].value;
8224 } else {
8225 this.requestUpdate();
8226 }
8227 }
8228 render() {
8229 const label = this.label || "";
8230 if (label) {
8231 this.setAttribute("aria-label", label);
8232 }
8233 this.setAttribute("role", "radiogroup");
8234 const current = this.value;
8235 queueMicrotask(() => {
8236 const segs = this.querySelectorAll("wpd-segment");
8237 for (const seg of Array.from(segs)) {
8238 const v = seg.getAttribute("value");
8239 seg.setAttribute(
8240 "aria-checked",
8241 v === current ? "true" : "false"
8242 );
8243 }
8244 });
8245 return html`<slot></slot>`;
8246 }
8247 };
8248 _WpdSegmented.props = ["value", "label"];
8249 _WpdSegmented.styles = [segmentedStyles];
8250 _WpdSegmented.help = {
8251 title: "Segmented",
8252 summary: "iOS-style segmented radio group. Pill-shaped bar of equal-width <wpd-segment> children where exactly one is active.",
8253 status: "stable",
8254 since: "0.9.0",
8255 props: [
8256 {
8257 name: "value",
8258 type: "string",
8259 description: "Currently selected segment value. Mirrored onto child aria-checked."
8260 },
8261 {
8262 name: "label",
8263 type: "string",
8264 description: "aria-label for the radiogroup."
8265 }
8266 ],
8267 slots: [
8268 { name: "(default)", description: '<wpd-segment value="…"> children.' }
8269 ],
8270 events: [
8271 {
8272 name: "wpd-pick",
8273 description: "Fires when the selected segment changes.",
8274 detail: "{ value: string }"
8275 }
8276 ],
8277 cssProps: [
8278 { name: "--desktop-mode-window-bg", description: "Pill background." },
8279 { name: "--desktop-mode-text", description: "Active label colour." },
8280 { name: "--desktop-mode-muted", description: "Inactive label colour." }
8281 ],
8282 example: html`
8283 <wpd-segmented value="md" label="Dock size">
8284 <wpd-segment value="sm">Small</wpd-segment>
8285 <wpd-segment value="md">Medium</wpd-segment>
8286 <wpd-segment value="lg">Large</wpd-segment>
8287 </wpd-segmented>
8288 `
8289 };
8290 let WpdSegmented = _WpdSegmented;
8291 defineComponent("wpd-segmented", WpdSegmented);
8292 const menuStyles = css`:host{display:block;min-width:220px;padding:4px;background:var( --desktop-mode-window-bg,#fff );color:var( --desktop-mode-text,#1d2327 );border:1px solid var( --desktop-mode-window-border,#c3c4c7 );border-radius:8px;box-shadow:0 8px 24px rgba( 0,0,0,0.18 ),0 2px 6px rgba( 0,0,0,0.08 )}:host( [ hidden ] ){display:none}`;
8293 const menuItemStyles = css`:host{display:block}button{display:flex;align-items:center;gap:10px;width:100%;min-height:32px;padding:6px 10px;border:none;border-radius:6px;background:transparent;color:inherit;font:inherit;font-size:13px;line-height:1.3;text-align:start;cursor:pointer;transition:background-color 0.12s ease,color 0.12s ease}button:hover,button:focus-visible{background:rgba( 0,0,0,0.06 );color:#000;outline:none}button:focus-visible{outline:2px solid var( --wp-admin-theme-color,#2271b1 );outline-offset:-2px}.wpd-menu-item__icon{flex-shrink:0;width:18px;height:18px;font-size:18px;line-height:1;color:var( --wp-admin-theme-color,#2271b1 )}.wpd-menu-item__icon[ hidden ]{display:none}.wpd-menu-item__label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.wpd-menu-item__check{flex-shrink:0;width:16px;height:16px;border-radius:3px;border:1.5px solid rgba( 0,0,0,0.25 );position:relative;background:transparent;transition:background-color 0.12s ease,border-color 0.12s ease}.wpd-menu-item__check[ hidden ]{display:none}:host( [ checked ] ) .wpd-menu-item__check{background:var( --wp-admin-theme-color,#2271b1 );border-color:var( --wp-admin-theme-color,#2271b1 )}:host( [ checked ] ) .wpd-menu-item__check::after{content:'';position:absolute;top:1px;left:4px;width:4px;height:8px;border:solid #fff;border-width:0 2px 2px 0;transform:rotate( 45deg )}`;
8294 const _WpdMenu = class _WpdMenu extends Component {
8295 connectedCallback() {
8296 super.connectedCallback();
8297 this.setAttribute("role", "menu");
8298 }
8299 render() {
8300 return html`<slot></slot>`;
8301 }
8302 };
8303 _WpdMenu.styles = [menuStyles];
8304 _WpdMenu.help = {
8305 title: "Menu",
8306 summary: "Popover menu used in window title bars and other overflow triggers. Presentation-only: the consumer owns open/close state via the `hidden` attribute and any outside-click dismissal.",
8307 status: "stable",
8308 since: "0.9.0",
8309 slots: [
8310 { name: "(default)", description: "<wpd-menu-item> children." }
8311 ],
8312 cssProps: [
8313 { name: "--desktop-mode-window-bg", description: "Menu background." },
8314 { name: "--desktop-mode-window-border", description: "Menu border." },
8315 { name: "--desktop-mode-text", description: "Item text colour." }
8316 ],
8317 example: html`
8318 <wpd-menu>
8319 <wpd-menu-item value="new" icon="dashicons-plus">Open another window</wpd-menu-item>
8320 <wpd-menu-item value="startup" role="menuitemcheckbox" checked>Open on startup</wpd-menu-item>
8321 <wpd-menu-item value="close">Close window</wpd-menu-item>
8322 </wpd-menu>
8323 `
8324 };
8325 let WpdMenu = _WpdMenu;
8326 defineComponent("wpd-menu", WpdMenu);
8327 const _WpdMenuItem = class _WpdMenuItem extends Component {
8328 connectedCallback() {
8329 super.connectedCallback();
8330 if (!this.hasAttribute("role")) {
8331 this.setAttribute("role", "menuitem");
8332 }
8333 }
8334 render() {
8335 const icon = this.icon || "";
8336 const isCheckbox = this.getAttribute("role") === "menuitemcheckbox";
8337 const checked = this.checked !== null;
8338 if (isCheckbox) {
8339 this.setAttribute("aria-checked", checked ? "true" : "false");
8340 }
8341 return html`
8342 <button type="button" @click=${(e) => this._onPick(e)}>
8343 <span
8344 class="wpd-menu-item__check"
8345 ?hidden=${!isCheckbox}
8346 ></span>
8347 <span
8348 class="wpd-menu-item__icon dashicons ${icon}"
8349 aria-hidden="true"
8350 ?hidden=${isCheckbox || !icon}
8351 ></span>
8352 <span class="wpd-menu-item__label">
8353 <slot></slot>
8354 </span>
8355 </button>
8356 `;
8357 }
8358 _onPick(e) {
8359 e.preventDefault();
8360 this.emit("wpd-menu-item-click", {
8361 value: this.value
8362 });
8363 }
8364 };
8365 _WpdMenuItem.props = ["icon", "value", "checked"];
8366 _WpdMenuItem.styles = [menuItemStyles];
8367 _WpdMenuItem.help = {
8368 title: "Menu item",
8369 summary: 'Single row inside a <wpd-menu>. Supports three looks: plain label, left-aligned dashicon (icon="dashicons-…"), or a checkbox indicator (role="menuitemcheckbox" + checked).',
8370 status: "stable",
8371 since: "0.9.0",
8372 props: [
8373 {
8374 name: "icon",
8375 type: "string (dashicons class)",
8376 description: 'Dashicons class rendered on the left. Ignored when role="menuitemcheckbox".'
8377 },
8378 {
8379 name: "value",
8380 type: "string",
8381 description: "Identifier emitted in wpd-menu-item-click.detail.value."
8382 },
8383 {
8384 name: "checked",
8385 type: "boolean attribute",
8386 description: 'Visible check indicator. Only honoured when role="menuitemcheckbox".'
8387 }
8388 ],
8389 slots: [
8390 { name: "(default)", description: "Menu item label." }
8391 ],
8392 events: [
8393 {
8394 name: "wpd-menu-item-click",
8395 description: "Fires when the item is clicked; bubbles so the <wpd-menu> parent can delegate.",
8396 detail: "{ value: string | null }"
8397 }
8398 ]
8399 };
8400 let WpdMenuItem = _WpdMenuItem;
8401 defineComponent("wpd-menu-item", WpdMenuItem);
8402 function wpdConfirmGlobal$1(options) {
8403 const fn = window.wp?.desktop?.confirm;
8404 if (typeof fn !== "function") {
8405 return Promise.reject(
8406 new Error(
8407 "[desktop-mode] wp.desktop.confirm is missing — the main desktop bundle must load before the posts-window script."
8408 )
8409 );
8410 }
8411 return fn(options);
8412 }
8413 const _introShown = /* @__PURE__ */ Object.create(null);
8414 document.addEventListener("desktop-mode-intros-reset", () => {
8415 for (const slug of Object.keys(_introShown)) {
8416 _introShown[slug] = false;
8417 }
8418 });
8419 function maybeShowIntro(client) {
8420 let cfg;
8421 try {
8422 cfg = client.getConfig();
8423 } catch {
8424 return;
8425 }
8426 const slug = cfg.introSlug || cfg.mode || "posts";
8427 if (_introShown[slug]) {
8428 return;
8429 }
8430 if (cfg.introSeen) {
8431 return;
8432 }
8433 _introShown[slug] = true;
8434 const dialogPromise = slug === "pages" ? Promise.resolve().then(() => pagesIntroDialog).then(
8435 (m) => m.showPagesIntroDialog()
8436 ) : showPostsIntroDialog();
8437 void dialogPromise.then((result) => {
8438 if (result === "cancel") {
8439 _introShown[slug] = false;
8440 return;
8441 }
8442 void markIntroSeen(cfg, slug, client);
8443 if (result === "settings") {
8444 openOsSettingsFeatures();
8445 }
8446 }).catch(() => {
8447 _introShown[slug] = false;
8448 });
8449 }
8450 async function markIntroSeen(cfg, slug, client) {
8451 if (!cfg.introUrl) {
8452 return;
8453 }
8454 try {
8455 await trackedFetch(
8456 cfg.introUrl,
8457 {
8458 method: "POST",
8459 credentials: "same-origin",
8460 headers: {
8461 "Content-Type": "application/json",
8462 "X-WP-Nonce": cfg.restNonce
8463 },
8464 body: JSON.stringify({ slug })
8465 },
8466 {
8467 windowId: client.windowId,
8468 source: `${slug}-window/intro`
8469 }
8470 );
8471 cfg.introSeen = true;
8472 } catch {
8473 }
8474 }
8475 function openOsSettingsFeatures() {
8476 const api = window.wp?.desktop;
8477 api?.openOsSettings?.();
8478 }
8479 const ROOT$1 = "[data-desktop-mode-posts-root]";
8480 const STATUS$1 = "[data-desktop-mode-posts-status]";
8481 const SEARCH$1 = "[data-desktop-mode-posts-search]";
8482 const REFRESH$1 = "[data-desktop-mode-posts-refresh]";
8483 const NEW_BTN$1 = "[data-desktop-mode-posts-new]";
8484 const TABLE$1 = "[data-desktop-mode-posts-table]";
8485 const BULK$1 = "[data-desktop-mode-posts-bulk]";
8486 const COUNT$1 = "[data-desktop-mode-posts-count]";
8487 const PAGE_INDICATOR$1 = "[data-desktop-mode-posts-page-indicator]";
8488 const PREV$1 = "[data-desktop-mode-posts-prev]";
8489 const NEXT$1 = "[data-desktop-mode-posts-next]";
8490 const PER_PAGE$1 = "[data-desktop-mode-posts-per-page]";
8491 const TOOLBAR_TRAILING_EXTRAS = "[data-desktop-mode-posts-toolbar-extras]";
8492 const BULK_ACTIONS_HOST$1 = "[data-desktop-mode-posts-bulk-actions]";
8493 const HOOK_FILTER_COLUMNS = "desktop_mode.postsWindow.columns";
8494 const HOOK_FILTER_STATUS_SEGMENTS = "desktop_mode.postsWindow.statusSegments";
8495 const HOOK_FILTER_BULK_ACTIONS = "desktop_mode.postsWindow.bulkActions";
8496 const HOOK_FILTER_TOOLBAR_TRAILING = "desktop_mode.postsWindow.toolbarTrailing";
8497 const HOOK_ACTION_OPENED = "desktop_mode.postsWindow.opened";
8498 const HOOK_ACTION_DATA_LOADED = "desktop_mode.postsWindow.dataLoaded";
8499 const SEARCH_DEBOUNCE_MS$1 = 250;
8500 const STATUS_LABELS = {
8501 publish: __("Published"),
8502 future: __("Scheduled"),
8503 draft: __("Draft"),
8504 pending: __("Pending"),
8505 private: __("Private"),
8506 trash: __("Trash")
8507 };
8508 function statusBadgeColor(status) {
8509 switch (status) {
8510 case "publish":
8511 return { bg: "#e6f4ea", fg: "#1d6f42" };
8512 case "draft":
8513 return { bg: "#fdecea", fg: "#a02622" };
8514 case "pending":
8515 return { bg: "#fef7e0", fg: "#8a6d00" };
8516 case "private":
8517 return { bg: "#e8f0fe", fg: "#1a52a8" };
8518 case "future":
8519 return { bg: "#ede7f6", fg: "#5b3aa0" };
8520 case "trash":
8521 return { bg: "#f1f1f2", fg: "#50575e" };
8522 default:
8523 return { bg: "#f1f1f2", fg: "#50575e" };
8524 }
8525 }
8526 function decodeTitle(raw) {
8527 const ta = document.createElement("textarea");
8528 ta.innerHTML = raw;
8529 return ta.value;
8530 }
8531 function authorOf(row) {
8532 const embedded = row._embedded?.author?.[0];
8533 if (embedded) {
8534 const avatars = embedded.avatar_urls ?? {};
8535 return {
8536 id: embedded.id,
8537 name: embedded.name,
8538 avatar: avatars["48"] ?? avatars["96"] ?? avatars["24"]
8539 };
8540 }
8541 return { id: row.author, name: __("Unknown") };
8542 }
8543 function termRecordsOf(row, taxonomy) {
8544 const groups = row._embedded?.["wp:term"] ?? [];
8545 for (const group of groups) {
8546 if (group.length === 0) {
8547 continue;
8548 }
8549 if (group[0].taxonomy === taxonomy) {
8550 return group.map((t) => ({ id: t.id, name: t.name }));
8551 }
8552 }
8553 return [];
8554 }
8555 function featuredMediaOf(row) {
8556 const media = row._embedded?.["wp:featuredmedia"]?.[0];
8557 if (!media) {
8558 return null;
8559 }
8560 const sizes = media.media_details?.sizes ?? {};
8561 const small = sizes.thumbnail?.source_url ?? sizes.medium?.source_url ?? media.source_url;
8562 return { url: small, alt: media.alt_text ?? "" };
8563 }
8564 function cacheKey(rowId, columnKey) {
8565 return `${rowId}|${columnKey}`;
8566 }
8567 function memoCell(cache, rowId, columnKey, build) {
8568 const key = cacheKey(rowId, columnKey);
8569 const cached = cache.get(key);
8570 if (cached) {
8571 return cached;
8572 }
8573 const built = build();
8574 cache.set(key, built);
8575 return built;
8576 }
8577 const REQUIRED_COLUMN_KEYS = /* @__PURE__ */ new Set(["title"]);
8578 function getHiddenColumns() {
8579 try {
8580 const api = window.wp?.desktop;
8581 if (api && typeof api.getOsSettings === "function") {
8582 const snap = api.getOsSettings();
8583 if (Array.isArray(snap.nativePostsHiddenColumns)) {
8584 return new Set(snap.nativePostsHiddenColumns);
8585 }
8586 }
8587 } catch {
8588 }
8589 return /* @__PURE__ */ new Set();
8590 }
8591 const EMPTY_FILTER_DATA = { authors: [], tags: [] };
8592 function buildAllColumns(cache, client, filterData = EMPTY_FILTER_DATA) {
8593 const cols = _buildBaseColumns(cache, filterData, client);
8594 const hooks = window.wp?.hooks;
8595 return hooks && typeof hooks.applyFilters === "function" ? hooks.applyFilters(
8596 HOOK_FILTER_COLUMNS,
8597 cols
8598 ) : cols;
8599 }
8600 function buildColumns$1(cache, client, filterData = EMPTY_FILTER_DATA) {
8601 const all = buildAllColumns(cache, client, filterData);
8602 const hidden = getHiddenColumns();
8603 if (hidden.size === 0) {
8604 return all;
8605 }
8606 return all.filter(
8607 (col) => REQUIRED_COLUMN_KEYS.has(col.key) || !hidden.has(col.key)
8608 );
8609 }
8610 function _buildBaseColumns(cache, filterData, client) {
8611 let mode = "posts";
8612 try {
8613 const cfg = client.getConfig();
8614 if (cfg.mode === "pages") {
8615 mode = "pages";
8616 }
8617 } catch {
8618 }
8619 const titleCol = {
8620 key: "title",
8621 label: __("Title"),
8622 sortable: true,
8623 sticky: true,
8624 render: (_v, row) => memoCell(cache, row.id, "title", () => buildTitleCell(row, client))
8625 };
8626 const authorCol = {
8627 key: "author",
8628 label: __("Author"),
8629 sortable: true,
8630 width: "180px",
8631 filterRender: (host, ctx) => renderMultiSelectFilter(host, ctx, filterData.authors, {
8632 label: __("All authors"),
8633 ariaLabel: __("Filter by author")
8634 }),
8635 render: (_v, row) => memoCell(cache, row.id, "author", () => buildAuthorCell(row))
8636 };
8637 const dateCol = {
8638 key: "date",
8639 label: __("Date"),
8640 sortable: true,
8641 width: "170px",
8642 sortValue: (row) => Date.parse(row.date_gmt + "Z") || 0,
8643 render: (_v, row) => memoCell(cache, row.id, "date", () => buildDateCell(row))
8644 };
8645 if (mode === "pages") {
8646 const parentCol = {
8647 key: "parent",
8648 label: __("Parent"),
8649 width: "200px",
8650 render: (_v, row) => memoCell(cache, row.id, "parent", () => buildParentCell(row))
8651 };
8652 const templateCol = {
8653 key: "template",
8654 label: __("Template"),
8655 width: "180px",
8656 render: (_v, row) => memoCell(cache, row.id, "template", () => buildTemplateCell(row, client))
8657 };
8658 const slugCol = {
8659 key: "slug",
8660 label: __("Slug"),
8661 width: "200px",
8662 render: (_v, row) => memoCell(cache, row.id, "slug", () => buildSlugCell(row))
8663 };
8664 const commentsCol = {
8665 key: "comments",
8666 label: __("Comments"),
8667 width: "110px",
8668 sortValue: (row) => typeof row.desktop_mode_comment_count === "number" ? row.desktop_mode_comment_count : 0,
8669 render: (_v, row) => memoCell(
8670 cache,
8671 row.id,
8672 "comments",
8673 () => buildCommentsCell(row)
8674 )
8675 };
8676 return [
8677 titleCol,
8678 authorCol,
8679 parentCol,
8680 templateCol,
8681 slugCol,
8682 commentsCol,
8683 dateCol
8684 ];
8685 }
8686 return [
8687 titleCol,
8688 authorCol,
8689 {
8690 key: "categories",
8691 label: __("Categories"),
8692 width: "260px",
8693 render: (_v, row) => memoCell(
8694 cache,
8695 row.id,
8696 "categories",
8697 () => buildCategoriesCell(row, client)
8698 )
8699 },
8700 {
8701 key: "tags",
8702 // Drop the fixed width so the column flexes with the
8703 // available space; pin a minimum that comfortably holds
8704 // ~4 chips on one line so the cell doesn't collapse the
8705 // tags into a vertical stack on narrow tables.
8706 label: __("Tags"),
8707 minWidth: "360px",
8708 filterRender: (host, ctx) => renderMultiSelectFilter(
8709 host,
8710 ctx,
8711 filterData.tags.map((t) => ({ id: t.id, name: t.name })),
8712 {
8713 label: __("All tags"),
8714 ariaLabel: __("Filter by tag"),
8715 dataKey: "tags",
8716 hasMore: !!filterData.tagsHasMore,
8717 onLoadMore: filterData.loadMoreTags
8718 }
8719 ),
8720 render: (_v, row) => memoCell(cache, row.id, "tags", () => buildTagsCell(row, client))
8721 },
8722 dateCol
8723 ];
8724 }
8725 const _parentTitleByPageRoster = /* @__PURE__ */ new Map();
8726 function buildParentCell(row) {
8727 const cell = document.createElement("span");
8728 cell.className = "desktop-mode-posts__parent";
8729 const pid = typeof row.parent === "number" ? row.parent : 0;
8730 if (pid === 0) {
8731 cell.classList.add("desktop-mode-posts__parent--top");
8732 cell.textContent = "—";
8733 cell.setAttribute("aria-label", __("Top-level page"));
8734 return cell;
8735 }
8736 cell.classList.add("desktop-mode-posts__parent--child");
8737 const titleFromRoster = _parentTitleByPageRoster.get(pid);
8738 if (titleFromRoster) {
8739 cell.textContent = `↳ ${titleFromRoster}`;
8740 } else {
8741 cell.textContent = sprintf(__("↳ #%d"), pid);
8742 }
8743 return cell;
8744 }
8745 function refreshParentTitleRoster(rows) {
8746 _parentTitleByPageRoster.clear();
8747 for (const row of rows) {
8748 _parentTitleByPageRoster.set(row.id, decodeTitle(row.title.rendered));
8749 }
8750 }
8751 function buildTemplateCell(row, client) {
8752 const cell = document.createElement("span");
8753 cell.className = "desktop-mode-posts__template";
8754 const slug = typeof row.template === "string" ? row.template : "";
8755 let label = slug;
8756 try {
8757 const cfg = client.getConfig();
8758 const map = cfg.pageTemplates ?? {};
8759 label = map[slug] ?? (slug === "" ? __("Default template") : slug);
8760 } catch {
8761 label = slug === "" ? __("Default template") : slug;
8762 }
8763 cell.textContent = label;
8764 if (slug !== "") {
8765 cell.title = slug;
8766 }
8767 return cell;
8768 }
8769 function buildSlugCell(row) {
8770 const cell = document.createElement("button");
8771 cell.type = "button";
8772 cell.className = "desktop-mode-posts__slug";
8773 const slug = typeof row.slug === "string" ? row.slug : "";
8774 cell.textContent = slug || "—";
8775 cell.disabled = slug === "";
8776 cell.title = slug ? __("Click to copy slug") : "";
8777 Object.assign(cell.style, {
8778 appearance: "none",
8779 background: "transparent",
8780 border: "none",
8781 padding: "2px 6px",
8782 font: "inherit",
8783 color: "inherit",
8784 cursor: slug ? "copy" : "default",
8785 textAlign: "left",
8786 fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace',
8787 fontSize: "12px",
8788 borderRadius: "4px",
8789 maxWidth: "100%",
8790 overflow: "hidden",
8791 textOverflow: "ellipsis",
8792 whiteSpace: "nowrap"
8793 });
8794 cell.addEventListener("click", (e) => {
8795 e.stopPropagation();
8796 if (!slug) {
8797 return;
8798 }
8799 void navigator.clipboard?.writeText(slug).then(() => {
8800 cell.textContent = __("Copied!");
8801 cell.style.color = "var(--wp-admin-theme-color, #2271b1)";
8802 setTimeout(() => {
8803 cell.textContent = slug;
8804 cell.style.color = "";
8805 }, 1200);
8806 }).catch(() => {
8807 });
8808 });
8809 return cell;
8810 }
8811 function buildCommentsCell(row) {
8812 const cell = document.createElement("span");
8813 cell.className = "desktop-mode-posts__comments";
8814 Object.assign(cell.style, {
8815 display: "inline-flex",
8816 alignItems: "center",
8817 gap: "6px",
8818 fontVariantNumeric: "tabular-nums"
8819 });
8820 const count = typeof row.desktop_mode_comment_count === "number" ? row.desktop_mode_comment_count : null;
8821 if (count === null) {
8822 cell.textContent = "—";
8823 cell.style.color = "var(--wp-admin-theme-fg-muted, #8c8f94)";
8824 return cell;
8825 }
8826 const icon = document.createElement("span");
8827 icon.className = "dashicons dashicons-admin-comments";
8828 icon.setAttribute("aria-hidden", "true");
8829 Object.assign(icon.style, {
8830 fontSize: "16px",
8831 width: "16px",
8832 height: "16px",
8833 color: count > 0 ? "var(--wp-admin-theme-color, #2271b1)" : "var(--wp-admin-theme-fg-muted, #8c8f94)"
8834 });
8835 const label = document.createElement("span");
8836 label.textContent = String(count);
8837 if (count === 0) {
8838 label.style.color = "var(--wp-admin-theme-fg-muted, #8c8f94)";
8839 }
8840 cell.appendChild(icon);
8841 cell.appendChild(label);
8842 cell.setAttribute(
8843 "aria-label",
8844 // translators: %d is the comment count for a row.
8845 `${sprintf(__("%d comments"), count)}`
8846 );
8847 return cell;
8848 }
8849 function renderMultiSelectFilter(host, ctx, all, opts) {
8850 const HOST_KEY = "wpdPostsFilterMounted";
8851 const tagged = host;
8852 const optionsForPicker = all.map((o) => ({
8853 value: String(o.id),
8854 label: o.name
8855 }));
8856 const nextSig = optionsForPicker.map((o) => `${o.value}:${o.label}`).join("|");
8857 if (tagged[HOST_KEY]) {
8858 const state = tagged[HOST_KEY];
8859 if (state.listSig !== nextSig) {
8860 state.picker.items = optionsForPicker;
8861 state.listSig = nextSig;
8862 }
8863 if (state.picker.getAttribute("value") !== ctx.value) {
8864 state.picker.setAttribute("value", ctx.value);
8865 }
8866 state.picker.hasMore = !!opts.hasMore;
8867 return;
8868 }
8869 const picker = document.createElement("wpd-multiselect");
8870 picker.setAttribute("placeholder", opts.label);
8871 picker.setAttribute("aria-label", opts.ariaLabel);
8872 picker.setAttribute("data-noclick", "");
8873 picker.setAttribute("value", ctx.value);
8874 if (opts.dataKey) {
8875 picker.setAttribute("data-key", opts.dataKey);
8876 }
8877 host.appendChild(picker);
8878 picker.items = optionsForPicker;
8879 picker.hasMore = !!opts.hasMore;
8880 picker.addEventListener("wpd-pick", (e) => {
8881 const detail = e.detail;
8882 const next = detail?.value ?? "";
8883 ctx.value = next;
8884 ctx.setValue(next);
8885 });
8886 if (opts.onLoadMore) {
8887 const onLoadMore = opts.onLoadMore;
8888 picker.addEventListener("wpd-multiselect-load-more", () => {
8889 picker.loadingMore = true;
8890 onLoadMore();
8891 });
8892 }
8893 tagged[HOST_KEY] = { picker, listSig: nextSig };
8894 }
8895 function mountKebabColumnToggles(body, cache, repaintColumns, client) {
8896 const winEl = body.closest(".desktop-mode-window");
8897 const panel = winEl?.querySelector(
8898 ".desktop-mode-window__menu-panel"
8899 );
8900 if (!panel) {
8901 return null;
8902 }
8903 const SECTION_CLASS = "desktop-mode-posts-window__menu-columns";
8904 const ITEM_CLASS = "desktop-mode-posts-window__menu-column-item";
8905 const VALUE_PREFIX = "desktop-mode-posts-column:";
8906 panel.querySelectorAll(`.${SECTION_CLASS}, .${ITEM_CLASS}`).forEach((n) => n.remove());
8907 const allCols = buildAllColumns(cache, client);
8908 const togglable = allCols.filter(
8909 (c) => !REQUIRED_COLUMN_KEYS.has(c.key)
8910 );
8911 if (togglable.length === 0) {
8912 return null;
8913 }
8914 const sectionLabel = document.createElement("div");
8915 sectionLabel.className = SECTION_CLASS;
8916 sectionLabel.setAttribute("role", "presentation");
8917 sectionLabel.textContent = __("Show columns");
8918 panel.appendChild(sectionLabel);
8919 const itemEls = /* @__PURE__ */ new Map();
8920 for (const col of togglable) {
8921 const item = document.createElement("wpd-menu-item");
8922 item.setAttribute("role", "menuitemcheckbox");
8923 item.setAttribute("value", VALUE_PREFIX + col.key);
8924 item.classList.add("desktop-mode-window__menu-item");
8925 item.classList.add(ITEM_CLASS);
8926 item.textContent = col.label || col.key;
8927 panel.appendChild(item);
8928 itemEls.set(col.key, item);
8929 }
8930 const paintChecked = () => {
8931 const hidden = getHiddenColumns();
8932 for (const [key, el] of itemEls) {
8933 if (hidden.has(key)) {
8934 el.removeAttribute("checked");
8935 } else {
8936 el.setAttribute("checked", "");
8937 }
8938 }
8939 };
8940 paintChecked();
8941 const onClick = (e) => {
8942 const detail = e.detail;
8943 const value = detail?.value;
8944 if (typeof value !== "string" || !value.startsWith(VALUE_PREFIX)) {
8945 return;
8946 }
8947 const key = value.slice(VALUE_PREFIX.length);
8948 if (!itemEls.has(key) || REQUIRED_COLUMN_KEYS.has(key)) {
8949 return;
8950 }
8951 const hidden = getHiddenColumns();
8952 if (hidden.has(key)) {
8953 hidden.delete(key);
8954 } else {
8955 hidden.add(key);
8956 }
8957 const next = Array.from(hidden).sort();
8958 const api = window.wp?.desktop;
8959 if (api && typeof api.updateOsSettings === "function") {
8960 api.updateOsSettings(
8961 { nativePostsHiddenColumns: next },
8962 { windowId: "desktop-mode-posts" }
8963 );
8964 }
8965 paintChecked();
8966 repaintColumns();
8967 };
8968 panel.addEventListener("wpd-menu-item-click", onClick);
8969 return {
8970 refresh: paintChecked,
8971 dispose: () => {
8972 panel.removeEventListener("wpd-menu-item-click", onClick);
8973 sectionLabel.remove();
8974 for (const el of itemEls.values()) {
8975 el.remove();
8976 }
8977 itemEls.clear();
8978 }
8979 };
8980 }
8981 function defaultStatusSegments$1() {
8982 return [
8983 { value: "", label: __("All") },
8984 { value: "publish", label: __("Published") },
8985 { value: "draft", label: __("Drafts") },
8986 { value: "pending", label: __("Pending") },
8987 { value: "future", label: __("Scheduled") },
8988 { value: "trash", label: __("Trash") }
8989 ];
8990 }
8991 function defaultBulkActions(client) {
8992 return [
8993 {
8994 id: "trash",
8995 label: __("Move to trash"),
8996 icon: "dashicons-trash",
8997 variant: "danger",
8998 /* translators: %d: row count. */
8999 confirm: __("Move %d post(s) to the trash?"),
9000 run: async (ids, ctx) => {
9001 const data = ctx.table.data ?? [];
9002 const trashable = ids.filter((id) => {
9003 const row = data.find((r) => r.id === id);
9004 return row && row.status !== "trash";
9005 });
9006 if (trashable.length === 0) {
9007 return;
9008 }
9009 const results = await Promise.all(
9010 trashable.map((id) => client.trashPost(id))
9011 );
9012 const errors = results.filter((r) => !r.ok);
9013 if (errors.length > 0) {
9014 console.error("[posts-window] some trashes failed", errors);
9015 }
9016 const okIds = results.filter((r) => r.ok).map((r) => r.id);
9017 const api = window.wp?.desktop;
9018 if (api && typeof api.broadcast === "function") {
9019 api.broadcast("desktop-mode.post.changed", {
9020 source: "posts-window",
9021 action: "trashed",
9022 ids: okIds
9023 });
9024 }
9025 }
9026 }
9027 ];
9028 }
9029 function resolveBulkActions(client) {
9030 const hooks = window.wp?.hooks;
9031 const defaults = defaultBulkActions(client);
9032 if (!hooks || typeof hooks.applyFilters !== "function") {
9033 return defaults;
9034 }
9035 try {
9036 const out = hooks.applyFilters(HOOK_FILTER_BULK_ACTIONS, defaults);
9037 return Array.isArray(out) ? out : defaults;
9038 } catch (err) {
9039 console.error(
9040 "[posts-window] bulk-actions filter threw; falling back to defaults:",
9041 err
9042 );
9043 return defaults;
9044 }
9045 }
9046 function resolveStatusSegments() {
9047 const hooks = window.wp?.hooks;
9048 const defaults = defaultStatusSegments$1();
9049 if (!hooks || typeof hooks.applyFilters !== "function") {
9050 return defaults;
9051 }
9052 try {
9053 const out = hooks.applyFilters(HOOK_FILTER_STATUS_SEGMENTS, defaults);
9054 return Array.isArray(out) && out.length > 0 ? out : defaults;
9055 } catch (err) {
9056 console.error(
9057 "[posts-window] status-segments filter threw; falling back to defaults:",
9058 err
9059 );
9060 return defaults;
9061 }
9062 }
9063 function resolveToolbarTrailing(ctx) {
9064 const hooks = window.wp?.hooks;
9065 if (!hooks || typeof hooks.applyFilters !== "function") {
9066 return [];
9067 }
9068 try {
9069 const out = hooks.applyFilters(HOOK_FILTER_TOOLBAR_TRAILING, [], ctx);
9070 if (!Array.isArray(out)) {
9071 return [];
9072 }
9073 return out.filter((el) => el instanceof HTMLElement);
9074 } catch (err) {
9075 console.error(
9076 "[posts-window] toolbar-trailing filter threw; ignoring:",
9077 err
9078 );
9079 return [];
9080 }
9081 }
9082 function buildTitleCell(row, client) {
9083 const cell = document.createElement("span");
9084 cell.style.cssText = "display:flex;flex-direction:column;gap:4px;min-width:0;";
9085 const titleRow = document.createElement("span");
9086 titleRow.style.cssText = "display:flex;align-items:center;gap:8px;min-width:0;";
9087 const link = document.createElement("a");
9088 link.href = client.buildEditPostUrl(row.id);
9089 link.setAttribute("data-noclick", "");
9090 const title = decodeTitle(row.title.rendered) || __("(no title)");
9091 link.textContent = title;
9092 link.title = title;
9093 link.style.cssText = "font-weight:600;color:inherit;text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:340px;";
9094 link.addEventListener("mouseenter", () => {
9095 link.style.textDecoration = "underline";
9096 });
9097 link.addEventListener("mouseleave", () => {
9098 link.style.textDecoration = "none";
9099 });
9100 link.addEventListener("click", (e) => {
9101 e.preventDefault();
9102 e.stopPropagation();
9103 openAdminUrl(link.href, {
9104 title,
9105 icon: "dashicons-admin-post"
9106 });
9107 });
9108 titleRow.appendChild(link);
9109 const lock = row.desktop_mode_lock ?? null;
9110 if (lock) {
9111 const lockBadge = document.createElement("span");
9112 lockBadge.style.cssText = [
9113 "display:inline-flex",
9114 "align-items:center",
9115 "gap:4px",
9116 "padding:2px 8px",
9117 "border-radius:10px",
9118 "font-size:11px",
9119 "font-weight:600",
9120 "background:rgba(179, 45, 46, 0.1)",
9121 "color:#b32d2e",
9122 "white-space:nowrap",
9123 "flex-shrink:0"
9124 ].join(";");
9125 const lockIcon = document.createElement("span");
9126 lockIcon.setAttribute("aria-hidden", "true");
9127 lockIcon.style.cssText = [
9128 "font-family:dashicons",
9129 "font-size:14px",
9130 "line-height:1",
9131 "display:inline-block",
9132 "speak:none",
9133 "-webkit-font-smoothing:antialiased"
9134 ].join(";");
9135 lockIcon.textContent = "";
9136 lockBadge.appendChild(lockIcon);
9137 const lockText = document.createElement("span");
9138 lockText.textContent = lock.userName;
9139 lockBadge.appendChild(lockText);
9140 const tipFmt = __("%s is currently editing", "desktop-mode");
9141 lockBadge.title = sprintf(tipFmt, lock.userName);
9142 titleRow.appendChild(lockBadge);
9143 }
9144 let cfgForBadges = null;
9145 try {
9146 cfgForBadges = client.getConfig();
9147 } catch {
9148 cfgForBadges = null;
9149 }
9150 if (cfgForBadges && cfgForBadges.mode === "pages") {
9151 if (typeof cfgForBadges.frontPageId === "number" && cfgForBadges.frontPageId === row.id) {
9152 titleRow.appendChild(
9153 buildAssignmentBadge(
9154 __("Front page"),
9155 "dashicons-admin-home",
9156 "#0a4b78",
9157 "rgba(34,113,177,0.12)"
9158 )
9159 );
9160 }
9161 if (typeof cfgForBadges.postsPageId === "number" && cfgForBadges.postsPageId === row.id) {
9162 titleRow.appendChild(
9163 buildAssignmentBadge(
9164 __("Posts page"),
9165 "dashicons-admin-post",
9166 "#5b3aa0",
9167 "rgba(91,58,160,0.12)"
9168 )
9169 );
9170 }
9171 }
9172 if (row.status && row.status !== "publish") {
9173 const badge = document.createElement("span");
9174 const colors = statusBadgeColor(row.status);
9175 badge.textContent = STATUS_LABELS[row.status] ?? row.status;
9176 badge.style.cssText = [
9177 "display:inline-flex",
9178 "align-items:center",
9179 "padding:2px 8px",
9180 "border-radius:10px",
9181 "font-size:11px",
9182 "font-weight:600",
9183 "text-transform:uppercase",
9184 "letter-spacing:0.04em",
9185 `background:${colors.bg}`,
9186 `color:${colors.fg}`,
9187 "white-space:nowrap",
9188 "flex-shrink:0"
9189 ].join(";");
9190 titleRow.appendChild(badge);
9191 }
9192 if (cfgForBadges?.mode === "pages" && typeof row.link === "string" && row.link && row.status === "publish") {
9193 const view = document.createElement("a");
9194 view.href = row.link;
9195 view.target = "_blank";
9196 view.rel = "noreferrer noopener";
9197 view.textContent = __("View");
9198 view.title = row.link;
9199 view.setAttribute("data-noclick", "");
9200 view.style.cssText = [
9201 "font-size:11px",
9202 "color:var(--wp-admin-theme-color, #2271b1)",
9203 "text-decoration:none",
9204 "flex-shrink:0"
9205 ].join(";");
9206 view.addEventListener("click", (e) => e.stopPropagation());
9207 view.addEventListener("mouseenter", () => {
9208 view.style.textDecoration = "underline";
9209 });
9210 view.addEventListener("mouseleave", () => {
9211 view.style.textDecoration = "none";
9212 });
9213 titleRow.appendChild(view);
9214 }
9215 cell.appendChild(titleRow);
9216 return cell;
9217 }
9218 function buildAssignmentBadge(label, dashicon, fg, bg) {
9219 const badge = document.createElement("span");
9220 badge.style.cssText = [
9221 "display:inline-flex",
9222 "align-items:center",
9223 "gap:4px",
9224 "padding:2px 8px",
9225 "border-radius:10px",
9226 "font-size:11px",
9227 "font-weight:600",
9228 `background:${bg}`,
9229 `color:${fg}`,
9230 "white-space:nowrap",
9231 "flex-shrink:0"
9232 ].join(";");
9233 const icon = document.createElement("span");
9234 icon.className = `dashicons ${dashicon}`;
9235 icon.setAttribute("aria-hidden", "true");
9236 icon.style.cssText = "font-size:13px;width:13px;height:13px;line-height:1;";
9237 const text = document.createElement("span");
9238 text.textContent = label;
9239 badge.appendChild(icon);
9240 badge.appendChild(text);
9241 return badge;
9242 }
9243 function buildAuthorCell(row) {
9244 const a = authorOf(row);
9245 const wrap = document.createElement("span");
9246 wrap.style.cssText = "display:inline-flex;align-items:center;gap:8px;min-width:0;";
9247 const avatar = document.createElement("wpd-avatar");
9248 avatar.setAttribute("size", "24");
9249 if (a.name) {
9250 avatar.setAttribute("name", a.name);
9251 }
9252 if (a.id > 0) {
9253 avatar.setAttribute("user-id", String(a.id));
9254 }
9255 if (a.avatar) {
9256 applyAvatarSrc(avatar, a.avatar);
9257 }
9258 wrap.appendChild(avatar);
9259 const name = document.createElement("span");
9260 name.textContent = a.name;
9261 name.style.cssText = "overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
9262 wrap.appendChild(name);
9263 return wrap;
9264 }
9265 function buildTagsCell(row, client) {
9266 const wrap = document.createElement("span");
9267 wrap.style.cssText = "display:inline-flex;align-items:center;width:100%;min-width:0;";
9268 const picker = document.createElement("wpd-tag-input");
9269 picker.setAttribute("creatable", "");
9270 picker.setAttribute("removable", "");
9271 picker.setAttribute("min-query", "0");
9272 picker.setAttribute("placeholder", __("Add tag…"));
9273 picker.setAttribute("add-label", __("Tag"));
9274 picker.setAttribute("data-noclick", "");
9275 const seed = termRecordsOf(row, "post_tag").map((t) => ({
9276 id: t.id,
9277 label: t.name
9278 }));
9279 picker.value = seed;
9280 const cellState = {
9281 // Mirror of `picker.value` we mutate optimistically. Keeping
9282 // it here (rather than reading back from the picker) avoids
9283 // double-source-of-truth bugs when two events fire in the
9284 // same tick.
9285 tags: seed.slice(),
9286 // AbortController for the in-flight suggest fetch.
9287 suggestAbort: null,
9288 suggestDebounce: null,
9289 // Last query the user typed — used to drop stale responses
9290 // even after AbortController has fired.
9291 lastQuery: ""
9292 };
9293 const setValue = (next) => {
9294 cellState.tags = next.slice();
9295 picker.value = next;
9296 };
9297 picker.addEventListener("wpd-tag-suggest", (e) => {
9298 const detail = e.detail;
9299 const query = detail?.query ?? "";
9300 cellState.lastQuery = query;
9301 if (cellState.suggestDebounce !== null) {
9302 window.clearTimeout(cellState.suggestDebounce);
9303 cellState.suggestDebounce = null;
9304 }
9305 cellState.suggestDebounce = window.setTimeout(async () => {
9306 cellState.suggestDebounce = null;
9307 if (cellState.suggestAbort) {
9308 cellState.suggestAbort.abort();
9309 }
9310 const ac = new AbortController();
9311 cellState.suggestAbort = ac;
9312 try {
9313 const matches = await client.searchTags(query, ac.signal);
9314 if (cellState.lastQuery !== query) {
9315 return;
9316 }
9317 const existingIds = new Set(cellState.tags.map((t) => t.id));
9318 picker.suggestions = matches.filter((m) => !existingIds.has(m.id)).map((m) => ({ id: m.id, label: m.name }));
9319 } catch (err) {
9320 if (err?.name === "AbortError") {
9321 return;
9322 }
9323 picker.suggestions = [];
9324 console.warn(
9325 "[posts-window] tag search failed",
9326 err
9327 );
9328 } finally {
9329 picker.suggestionsLoading = false;
9330 }
9331 }, 200);
9332 });
9333 picker.addEventListener("wpd-tag-add", async (e) => {
9334 const detail = e.detail;
9335 if (!detail?.tag) {
9336 return;
9337 }
9338 const optimistic = {
9339 id: detail.tag.id,
9340 label: detail.tag.label,
9341 pending: true
9342 };
9343 const next = [...cellState.tags, optimistic];
9344 setValue(next);
9345 try {
9346 let resolvedTag = null;
9347 if (detail.isNew || typeof detail.tag.id !== "number") {
9348 resolvedTag = await client.createTag(detail.tag.label);
9349 } else {
9350 resolvedTag = {
9351 id: Number(detail.tag.id),
9352 name: detail.tag.label,
9353 slug: ""
9354 };
9355 }
9356 const desiredIds = [
9357 ...cellState.tags.filter((t) => !t.pending).map((t) => Number(t.id)),
9358 resolvedTag.id
9359 ];
9360 await client.updatePostTags(row.id, desiredIds);
9361 setValue(
9362 cellState.tags.map((t) => {
9363 if (t.label.toLowerCase() === detail.tag.label.toLowerCase()) {
9364 return {
9365 id: resolvedTag.id,
9366 label: resolvedTag.name
9367 };
9368 }
9369 return t;
9370 })
9371 );
9372 const api = window.wp?.desktop;
9373 if (api && typeof api.broadcast === "function") {
9374 api.broadcast("desktop-mode.post.changed", {
9375 source: "posts-window",
9376 action: "tagged",
9377 ids: [row.id]
9378 });
9379 }
9380 } catch (err) {
9381 setValue(
9382 cellState.tags.filter(
9383 (t) => t.label.toLowerCase() !== detail.tag.label.toLowerCase()
9384 )
9385 );
9386 showTagError(
9387 sprintf(
9388 /* translators: %s: tag label */
9389 __('Couldn’t add tag "%s".'),
9390 detail.tag.label
9391 ),
9392 err
9393 );
9394 }
9395 });
9396 picker.addEventListener("wpd-tag-remove", async (e) => {
9397 const detail = e.detail;
9398 if (!detail?.tag) {
9399 return;
9400 }
9401 const removed = detail.tag;
9402 const previous = cellState.tags.slice();
9403 setValue(
9404 cellState.tags.map(
9405 (t) => t.label === removed.label ? { ...t, pending: true } : t
9406 )
9407 );
9408 try {
9409 const desiredIds = previous.filter((t) => t.label !== removed.label).map((t) => Number(t.id)).filter((n) => Number.isFinite(n));
9410 await client.updatePostTags(row.id, desiredIds);
9411 setValue(
9412 previous.filter((t) => t.label !== removed.label)
9413 );
9414 const api = window.wp?.desktop;
9415 if (api && typeof api.broadcast === "function") {
9416 api.broadcast("desktop-mode.post.changed", {
9417 source: "posts-window",
9418 action: "untagged",
9419 ids: [row.id]
9420 });
9421 }
9422 } catch (err) {
9423 setValue(previous);
9424 showTagError(
9425 sprintf(
9426 /* translators: %s: tag label */
9427 __('Couldn’t remove tag "%s".'),
9428 removed.label
9429 ),
9430 err
9431 );
9432 }
9433 });
9434 wrap.appendChild(picker);
9435 return wrap;
9436 }
9437 function showTagError(title, err) {
9438 const reason = err instanceof Error ? err.message : String(err);
9439 const api = window.wp?.desktop;
9440 if (api && typeof api.showToast === "function") {
9441 api.showToast({
9442 message: `${title} ${reason}`.trim(),
9443 duration: 6e3
9444 });
9445 return;
9446 }
9447 console.error(title, err);
9448 }
9449 function buildCategoriesCell(row, client) {
9450 const wrap = document.createElement("span");
9451 wrap.className = "wpd-cat-cell-dropzone";
9452 wrap.style.cssText = "display:inline-flex;align-items:center;width:100%;min-width:0;border-radius:6px;transition:background-color 0.12s ease, box-shadow 0.12s ease;";
9453 const picker = document.createElement(
9454 "wpd-category-picker"
9455 );
9456 picker.setAttribute("placeholder", __("Search categories…"));
9457 picker.setAttribute("add-label", __("Categorize"));
9458 picker.setAttribute("data-noclick", "");
9459 _activePickers.add(picker);
9460 picker.value = row.categories ?? [];
9461 const seedItems = termRecordsOf(row, "category").map(
9462 (t) => ({ id: t.id, name: t.name, parent: 0 })
9463 );
9464 picker.items = seedItems;
9465 const cellState = {
9466 categoryIds: (row.categories ?? []).slice()
9467 };
9468 const setValue = (next) => {
9469 cellState.categoryIds = next.slice();
9470 picker.value = next;
9471 };
9472 void getCategoriesTree(client).then((tree) => {
9473 if (!picker.isConnected) {
9474 return;
9475 }
9476 picker.items = tree;
9477 }).catch((err) => {
9478 console.warn("[posts-window] category tree fetch failed", err);
9479 });
9480 picker.addEventListener("wpd-categories-open", () => {
9481 void primePickerFromCache(picker);
9482 });
9483 picker.addEventListener(
9484 "wpd-categories-create",
9485 async (e) => {
9486 const detail = e.detail;
9487 const parent = detail?.parent ?? 0;
9488 if (!detail || !detail.name) {
9489 picker.failCreating(parent);
9490 return;
9491 }
9492 try {
9493 const created = await client.createCategory(detail.name, parent);
9494 _categoryTreePromise = null;
9495 const nextItems = [
9496 ...picker.items,
9497 {
9498 id: created.id,
9499 name: created.name,
9500 parent: created.parent
9501 }
9502 ];
9503 picker.items = nextItems;
9504 const nextValue = [...cellState.categoryIds, created.id];
9505 setValue(nextValue);
9506 picker.endCreating(parent);
9507 try {
9508 await client.updatePostCategories(row.id, nextValue);
9509 const api = window.wp?.desktop;
9510 if (api && typeof api.broadcast === "function") {
9511 api.broadcast("desktop-mode.post.changed", {
9512 source: "posts-window",
9513 action: "categorized",
9514 ids: [row.id]
9515 });
9516 }
9517 } catch (err) {
9518 setValue(cellState.categoryIds.filter((id) => id !== created.id));
9519 showTagError(__("Couldn’t assign new category."), err);
9520 }
9521 } catch (err) {
9522 picker.failCreating(
9523 parent,
9524 err instanceof Error ? err.message : String(err)
9525 );
9526 showTagError(__("Couldn’t create category."), err);
9527 }
9528 }
9529 );
9530 picker.addEventListener("wpd-categories-change", async (e) => {
9531 const detail = e.detail;
9532 if (!detail || !Array.isArray(detail.value)) {
9533 return;
9534 }
9535 const previous = cellState.categoryIds.slice();
9536 const next = detail.value.slice();
9537 setValue(next);
9538 try {
9539 await client.updatePostCategories(row.id, next);
9540 const api = window.wp?.desktop;
9541 if (api && typeof api.broadcast === "function") {
9542 api.broadcast("desktop-mode.post.changed", {
9543 source: "posts-window",
9544 action: "categorized",
9545 ids: [row.id]
9546 });
9547 }
9548 } catch (err) {
9549 setValue(previous);
9550 showTagError(__("Couldn’t update categories."), err);
9551 }
9552 });
9553 picker.addEventListener("wpd-categories-delete", async (e) => {
9554 const detail = e.detail;
9555 if (!detail || typeof detail.id !== "number") {
9556 return;
9557 }
9558 const ok = await wpdConfirmGlobal$1({
9559 title: __("Delete category?"),
9560 message: sprintf(
9561 /* translators: %s: category name. */
9562 __(
9563 'Delete the category "%s"? Posts assigned only to it will fall back to Uncategorized.'
9564 ),
9565 detail.name
9566 ),
9567 confirmLabel: __("Delete"),
9568 danger: true
9569 });
9570 if (!ok) {
9571 return;
9572 }
9573 try {
9574 await client.deleteTerm("categories", detail.id);
9575 if (cellState.categoryIds.includes(detail.id)) {
9576 const next = cellState.categoryIds.filter(
9577 (id) => id !== detail.id
9578 );
9579 setValue(next);
9580 try {
9581 await client.updatePostCategories(row.id, next);
9582 } catch (err) {
9583 showTagError(
9584 __("Couldn’t update post categories after delete."),
9585 err
9586 );
9587 }
9588 }
9589 } catch (err) {
9590 showTagError(__("Couldn’t delete category."), err);
9591 }
9592 });
9593 picker.addEventListener("wpd-chain-segment-dragstart", (e) => {
9594 const detail = e.detail;
9595 if (!detail || !detail.dragEvent || !detail.dragEvent.dataTransfer) {
9596 return;
9597 }
9598 const ids = [];
9599 for (const seg of detail.segments) {
9600 if (typeof seg.id === "number") {
9601 ids.push(seg.id);
9602 }
9603 }
9604 if (ids.length === 0) {
9605 return;
9606 }
9607 const dt = detail.dragEvent.dataTransfer;
9608 dt.setData(
9609 "application/x-desktop-mode-categories",
9610 JSON.stringify({
9611 ids,
9612 source: "posts-window",
9613 sourcePostId: row.id
9614 })
9615 );
9616 dt.setData("text/plain", ids.join(","));
9617 dt.effectAllowed = "copy";
9618 });
9619 let dropEnterCount = 0;
9620 const setDropTargetActive = (on) => {
9621 if (on) {
9622 wrap.style.backgroundColor = "color-mix(in srgb, var(--wp-admin-theme-color, #2271b1) 12%, transparent)";
9623 wrap.style.boxShadow = "inset 0 0 0 2px var(--wp-admin-theme-color, #2271b1)";
9624 } else {
9625 wrap.style.backgroundColor = "";
9626 wrap.style.boxShadow = "";
9627 }
9628 };
9629 const acceptsCategoriesDrag = (e) => {
9630 const types = e.dataTransfer?.types;
9631 if (!types) {
9632 return false;
9633 }
9634 return Array.from(types).includes(
9635 "application/x-desktop-mode-categories"
9636 );
9637 };
9638 wrap.addEventListener("dragenter", (e) => {
9639 if (!acceptsCategoriesDrag(e)) {
9640 return;
9641 }
9642 e.preventDefault();
9643 dropEnterCount++;
9644 setDropTargetActive(true);
9645 });
9646 wrap.addEventListener("dragover", (e) => {
9647 if (!acceptsCategoriesDrag(e)) {
9648 return;
9649 }
9650 e.preventDefault();
9651 if (e.dataTransfer) {
9652 e.dataTransfer.dropEffect = "copy";
9653 }
9654 });
9655 wrap.addEventListener("dragleave", () => {
9656 if (dropEnterCount > 0) {
9657 dropEnterCount--;
9658 }
9659 if (dropEnterCount === 0) {
9660 setDropTargetActive(false);
9661 }
9662 });
9663 wrap.addEventListener("drop", async (e) => {
9664 dropEnterCount = 0;
9665 setDropTargetActive(false);
9666 if (!acceptsCategoriesDrag(e)) {
9667 return;
9668 }
9669 e.preventDefault();
9670 const json = e.dataTransfer?.getData(
9671 "application/x-desktop-mode-categories"
9672 );
9673 if (!json) {
9674 return;
9675 }
9676 let parsed;
9677 try {
9678 parsed = JSON.parse(json);
9679 } catch {
9680 return;
9681 }
9682 const payload = parsed;
9683 if (!payload || !Array.isArray(payload.ids)) {
9684 return;
9685 }
9686 const incoming = [];
9687 for (const v of payload.ids) {
9688 if (typeof v === "number" && Number.isFinite(v)) {
9689 incoming.push(v);
9690 }
9691 }
9692 if (incoming.length === 0) {
9693 return;
9694 }
9695 if (payload.sourcePostId === row.id && incoming.every((id) => cellState.categoryIds.includes(id))) {
9696 return;
9697 }
9698 const merged = Array.from(
9699 /* @__PURE__ */ new Set([...cellState.categoryIds, ...incoming])
9700 );
9701 if (merged.length === cellState.categoryIds.length) {
9702 return;
9703 }
9704 const previous = cellState.categoryIds.slice();
9705 setValue(merged);
9706 try {
9707 await client.updatePostCategories(row.id, merged);
9708 const api = window.wp?.desktop;
9709 if (api && typeof api.broadcast === "function") {
9710 api.broadcast("desktop-mode.post.changed", {
9711 source: "posts-window",
9712 action: "categorized",
9713 ids: [row.id]
9714 });
9715 }
9716 } catch (err) {
9717 setValue(previous);
9718 showTagError(__("Couldn’t add category."), err);
9719 }
9720 });
9721 wrap.appendChild(picker);
9722 return wrap;
9723 }
9724 let _categoryTreePromise = null;
9725 function getCategoriesTree(client) {
9726 if (!_categoryTreePromise) {
9727 _categoryTreePromise = client.fetchAllCategories().then(
9728 (terms) => terms.map((t) => ({
9729 id: t.id,
9730 name: t.name,
9731 parent: t.parent
9732 }))
9733 );
9734 }
9735 return _categoryTreePromise;
9736 }
9737 function clearCategoryTreeCache() {
9738 _categoryTreePromise = null;
9739 }
9740 const _activePickers = /* @__PURE__ */ new Set();
9741 function broadcastFreshCategoryTreeToPickers(client) {
9742 void getCategoriesTree(client).then((tree) => {
9743 for (const picker of _activePickers) {
9744 if (picker.isConnected) {
9745 picker.items = tree;
9746 } else {
9747 _activePickers.delete(picker);
9748 }
9749 }
9750 }).catch(() => {
9751 });
9752 }
9753 async function primePickerFromCache(picker) {
9754 if (!_categoryTreePromise) {
9755 return;
9756 }
9757 try {
9758 picker.items = await _categoryTreePromise;
9759 } catch {
9760 }
9761 }
9762 function buildDateCell(row) {
9763 const wrap = document.createElement("span");
9764 wrap.style.cssText = "display:flex;flex-direction:column;line-height:1.2;";
9765 const time = document.createElement("wpd-relative-time");
9766 time.setAttribute("datetime", row.date);
9767 wrap.appendChild(time);
9768 if (row.modified_gmt && row.modified_gmt !== row.date_gmt) {
9769 const meta = document.createElement("span");
9770 meta.textContent = __("modified");
9771 meta.style.cssText = "font-size:11px;color:#646970;";
9772 wrap.appendChild(meta);
9773 }
9774 return wrap;
9775 }
9776 function buildSubRow(row) {
9777 const wrap = document.createElement("div");
9778 wrap.style.cssText = "display:flex;gap:16px;padding:12px 16px;background:#fafafa;align-items:flex-start;";
9779 const featured = featuredMediaOf(row);
9780 if (featured) {
9781 const img = document.createElement("img");
9782 img.src = featured.url;
9783 img.alt = featured.alt;
9784 img.loading = "lazy";
9785 img.style.cssText = "width:96px;height:96px;border-radius:6px;object-fit:cover;flex-shrink:0;";
9786 wrap.appendChild(img);
9787 }
9788 const text = document.createElement("div");
9789 text.style.cssText = "flex:1;min-width:0;display:flex;flex-direction:column;gap:6px;";
9790 const heading = document.createElement("div");
9791 heading.style.cssText = "font-size:13px;color:#646970;text-transform:uppercase;letter-spacing:0.04em;";
9792 heading.textContent = __("Excerpt");
9793 text.appendChild(heading);
9794 const excerpt = document.createElement("div");
9795 excerpt.style.cssText = "color:#1d2327;line-height:1.5;";
9796 const raw = row.excerpt?.rendered ?? "";
9797 if (raw) {
9798 const stripped = raw.replace(/<[^>]+>/g, "").trim();
9799 excerpt.textContent = stripped || __("(no excerpt)");
9800 } else {
9801 excerpt.textContent = __("(no excerpt)");
9802 excerpt.style.color = "#a7aaad";
9803 }
9804 text.appendChild(excerpt);
9805 wrap.appendChild(text);
9806 return wrap;
9807 }
9808 async function renderPostsWindow(body, client) {
9809 const root = body.querySelector(ROOT$1);
9810 const table = body.querySelector(TABLE$1);
9811 if (!root || !table) {
9812 return;
9813 }
9814 maybeShowIntro(client);
9815 const catsHost = body.querySelector(
9816 "[data-desktop-mode-posts-cats-host]"
9817 );
9818 const tagsHost = body.querySelector(
9819 "[data-desktop-mode-posts-tags-host]"
9820 );
9821 let catsTeardown = null;
9822 let tagsTeardown = null;
9823 const tabsEl = body.querySelector(".desktop-mode-posts__tabs");
9824 if (tabsEl) {
9825 tabsEl.addEventListener("wpd-tab-change", (e) => {
9826 const detail = e.detail;
9827 const value = detail?.value;
9828 if (value === "categories" && catsHost && !catsTeardown) {
9829 void Promise.resolve().then(() => categoriesMindmap).then(
9830 async ({ mountCategoriesMindmap: mountCategoriesMindmap2 }) => {
9831 catsTeardown = await mountCategoriesMindmap2(catsHost, client);
9832 }
9833 );
9834 }
9835 if (value === "tags" && tagsHost && !tagsTeardown) {
9836 void Promise.resolve().then(() => tagsCloud).then(
9837 async ({ mountTagsCloud: mountTagsCloud2 }) => {
9838 tagsTeardown = await mountTagsCloud2(tagsHost, client);
9839 }
9840 );
9841 }
9842 });
9843 }
9844 const cfg = client.getConfig();
9845 const view = {
9846 page: 1,
9847 perPage: Math.max(1, cfg.defaultPerPage || 20),
9848 search: "",
9849 status: "",
9850 orderby: "date",
9851 order: "desc",
9852 author: [],
9853 tag: [],
9854 searchDebounce: null
9855 };
9856 const cellCache = /* @__PURE__ */ new Map();
9857 const filterData = { authors: [], tags: [] };
9858 table.columns = buildColumns$1(cellCache, client, filterData);
9859 table.getRowId = (row) => row.id;
9860 table.subTable = (row) => buildSubRow(row);
9861 table.sort = { key: "date", direction: "desc" };
9862 let totalPages = 0;
9863 let totalRows = 0;
9864 let refreshSeq = 0;
9865 const perPageEl = root.querySelector(PER_PAGE$1);
9866 if (perPageEl) {
9867 perPageEl.value = String(view.perPage);
9868 }
9869 const indicator = root.querySelector(PAGE_INDICATOR$1);
9870 const prevBtn = root.querySelector(PREV$1);
9871 const nextBtn = root.querySelector(NEXT$1);
9872 const bulkBar = root.querySelector(BULK$1);
9873 const countEl = root.querySelector(COUNT$1);
9874 const bulkActionsHost = root.querySelector(BULK_ACTIONS_HOST$1);
9875 const trailingExtras = root.querySelector(
9876 TOOLBAR_TRAILING_EXTRAS
9877 );
9878 const statusHost = root.querySelector(STATUS$1);
9879 const statusSegments = resolveStatusSegments();
9880 if (statusHost) {
9881 statusHost.replaceChildren();
9882 for (const seg of statusSegments) {
9883 const el = document.createElement("wpd-segment");
9884 el.setAttribute("value", seg.value);
9885 el.textContent = seg.label;
9886 statusHost.appendChild(el);
9887 }
9888 statusHost.setAttribute("value", view.status);
9889 }
9890 const updatePager = () => {
9891 if (indicator) {
9892 if (totalRows === 0) {
9893 indicator.textContent = __("No posts");
9894 } else {
9895 indicator.textContent = sprintf(
9896 /* translators: 1: current page, 2: total pages, 3: total posts. */
9897 __("Page %1$d of %2$d · %3$d posts"),
9898 view.page,
9899 Math.max(totalPages, 1),
9900 totalRows
9901 );
9902 }
9903 }
9904 if (prevBtn) {
9905 prevBtn.toggleAttribute("disabled", view.page <= 1);
9906 }
9907 if (nextBtn) {
9908 nextBtn.toggleAttribute("disabled", view.page >= totalPages);
9909 }
9910 };
9911 const updateBulkBar = () => {
9912 if (!bulkBar || !countEl) {
9913 return;
9914 }
9915 const sel = Array.from(table.selection ?? []);
9916 if (sel.length === 0) {
9917 bulkBar.hidden = true;
9918 return;
9919 }
9920 bulkBar.hidden = false;
9921 countEl.textContent = sprintf(
9922 /* translators: %d: selected row count. */
9923 __("%d selected"),
9924 sel.length
9925 );
9926 };
9927 const buildParams = () => ({
9928 page: view.page,
9929 perPage: view.perPage,
9930 search: view.search || void 0,
9931 status: view.status || void 0,
9932 orderby: view.orderby,
9933 order: view.order,
9934 author: view.author.length > 0 ? view.author : void 0,
9935 tag: view.tag.length > 0 ? view.tag : void 0
9936 });
9937 const ctx = {
9938 body,
9939 table,
9940 refresh: () => refresh(),
9941 getSelectedIds: () => Array.from(table.selection ?? []).map((id) => Number(id)),
9942 getSelectedRows: () => {
9943 const ids = new Set(ctx.getSelectedIds());
9944 return (table.data ?? []).filter((r) => ids.has(r.id));
9945 },
9946 getCurrentParams: () => buildParams()
9947 };
9948 const refresh = async () => {
9949 const mySeq = ++refreshSeq;
9950 table.toggleAttribute("loading", true);
9951 try {
9952 const result = await client.fetchPosts(buildParams());
9953 if (mySeq !== refreshSeq) {
9954 return;
9955 }
9956 if (result.items.length === 0 && view.page > 1 && result.totalPages > 0 && view.page > result.totalPages) {
9957 view.page = 1;
9958 await refresh();
9959 return;
9960 }
9961 cellCache.clear();
9962 refreshParentTitleRoster(result.items);
9963 table.data = result.items;
9964 totalRows = result.total;
9965 totalPages = result.totalPages;
9966 updatePager();
9967 const hooks2 = window.wp?.hooks;
9968 if (hooks2 && typeof hooks2.doAction === "function") {
9969 hooks2.doAction(HOOK_ACTION_DATA_LOADED, {
9970 items: result.items,
9971 total: result.total,
9972 totalPages: result.totalPages,
9973 page: view.page
9974 });
9975 }
9976 document.dispatchEvent(
9977 new CustomEvent("desktop-mode-posts-window-data-loaded", {
9978 detail: {
9979 items: result.items,
9980 total: result.total,
9981 totalPages: result.totalPages,
9982 page: view.page
9983 }
9984 })
9985 );
9986 } catch (err) {
9987 if (mySeq !== refreshSeq) {
9988 return;
9989 }
9990 console.error("[posts-window] list failed", err);
9991 table.data = [];
9992 totalRows = 0;
9993 totalPages = 0;
9994 updatePager();
9995 } finally {
9996 if (mySeq === refreshSeq) {
9997 table.toggleAttribute("loading", false);
9998 updateBulkBar();
9999 }
10000 }
10001 };
10002 const goToFirstPage = () => {
10003 if (view.page !== 1) {
10004 view.page = 1;
10005 }
10006 };
10007 root.querySelector(STATUS$1)?.addEventListener("wpd-pick", (e) => {
10008 const value = e.detail?.value ?? "";
10009 view.status = value;
10010 goToFirstPage();
10011 void refresh();
10012 });
10013 root.querySelector(SEARCH$1)?.addEventListener(
10014 "wpd-input-change",
10015 (e) => {
10016 const value = e.detail?.value ?? "";
10017 view.search = value;
10018 if (view.searchDebounce !== null) {
10019 window.clearTimeout(view.searchDebounce);
10020 }
10021 view.searchDebounce = window.setTimeout(() => {
10022 goToFirstPage();
10023 void refresh();
10024 }, SEARCH_DEBOUNCE_MS$1);
10025 }
10026 );
10027 body.addEventListener("click", (e) => {
10028 const target = e.target;
10029 if (!target) {
10030 return;
10031 }
10032 if (target.closest(REFRESH$1)) {
10033 void refresh();
10034 return;
10035 }
10036 if (target.closest(NEW_BTN$1)) {
10037 openAdminUrl(cfg.newPostUrl, {
10038 title: __("Add New Post"),
10039 icon: "dashicons-admin-post"
10040 });
10041 return;
10042 }
10043 if (target.closest(PREV$1)) {
10044 if (view.page > 1) {
10045 view.page -= 1;
10046 void refresh();
10047 }
10048 return;
10049 }
10050 if (target.closest(NEXT$1)) {
10051 if (view.page < totalPages) {
10052 view.page += 1;
10053 void refresh();
10054 }
10055 }
10056 });
10057 const bulkActions = resolveBulkActions(client);
10058 if (bulkActionsHost) {
10059 bulkActionsHost.replaceChildren();
10060 for (const action of bulkActions) {
10061 bulkActionsHost.appendChild(buildBulkActionButton(action, ctx));
10062 }
10063 }
10064 if (trailingExtras) {
10065 const extras = resolveToolbarTrailing(ctx);
10066 trailingExtras.replaceChildren(...extras);
10067 }
10068 perPageEl?.addEventListener("change", () => {
10069 const next = parseInt(perPageEl.value, 10);
10070 if (!Number.isFinite(next) || next < 1) {
10071 return;
10072 }
10073 view.perPage = next;
10074 goToFirstPage();
10075 void refresh();
10076 });
10077 table.addEventListener("wpd-table-selection-change", () => {
10078 updateBulkBar();
10079 });
10080 table.addEventListener("wpd-table-sort-change", (e) => {
10081 const detail = e.detail;
10082 if (!detail || !detail.sort) {
10083 view.orderby = "date";
10084 view.order = "desc";
10085 } else {
10086 view.orderby = mapColumnToOrderby(detail.sort.key);
10087 view.order = detail.sort.direction;
10088 }
10089 void refresh();
10090 });
10091 const parseIds = (raw) => raw.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => Number.isFinite(n) && n > 0);
10092 const sameIds = (a, b) => a.length === b.length && a.every((v, i) => v === b[i]);
10093 table.addEventListener("wpd-table-filter-change", (e) => {
10094 const detail = e.detail;
10095 const filters = detail?.filters ?? {};
10096 const nextAuthor = parseIds(filters.author ?? "");
10097 const nextTag = parseIds(filters.tags ?? "");
10098 const changed = !sameIds(nextAuthor, view.author) || !sameIds(nextTag, view.tag);
10099 if (!changed) {
10100 return;
10101 }
10102 view.author = nextAuthor;
10103 view.tag = nextTag;
10104 view.page = 1;
10105 void refresh();
10106 });
10107 activeRunBulkAction = async (action, actionCtx) => {
10108 const ids = actionCtx.getSelectedIds();
10109 if (ids.length === 0) {
10110 return;
10111 }
10112 if (action.confirm) {
10113 const ok = await wpdConfirmGlobal$1({
10114 message: sprintf(
10115 /* translators: %d: row count. */
10116 action.confirm,
10117 ids.length
10118 ),
10119 danger: true
10120 });
10121 if (!ok) {
10122 return;
10123 }
10124 }
10125 try {
10126 const result = await action.run(ids, actionCtx);
10127 if (result === false) {
10128 return;
10129 }
10130 } catch (err) {
10131 console.error(
10132 `[posts-window] bulk action "${action.id}" failed`,
10133 err
10134 );
10135 }
10136 table.clearSelection();
10137 await refresh();
10138 };
10139 const broadcastUnsubs = [];
10140 if (window.wp?.desktop && typeof window.wp.desktop.subscribe === "function") {
10141 const onChange = (payload) => {
10142 const detail = payload;
10143 if (detail?.source === "posts-window") {
10144 return;
10145 }
10146 void refresh();
10147 };
10148 broadcastUnsubs.push(
10149 window.wp.desktop.subscribe("desktop-mode.post.changed", onChange)
10150 );
10151 const onTermChange = (payload) => {
10152 const detail = payload;
10153 if (detail?.taxonomy === "category") {
10154 clearCategoryTreeCache();
10155 broadcastFreshCategoryTreeToPickers(client);
10156 }
10157 };
10158 broadcastUnsubs.push(
10159 window.wp.desktop.subscribe(
10160 "desktop-mode.term.changed",
10161 onTermChange
10162 )
10163 );
10164 }
10165 const repaintColumns = () => {
10166 cellCache.clear();
10167 table.columns = buildColumns$1(cellCache, client, filterData);
10168 };
10169 void client.fetchAuthorOptions().then((authors) => {
10170 filterData.authors = authors;
10171 repaintColumns();
10172 });
10173 let tagPage = 0;
10174 let tagTotalPages = 1;
10175 let tagFetching = false;
10176 const TAG_PAGE_SIZE = 50;
10177 const fetchNextTagPage = async () => {
10178 if (tagFetching || tagPage >= tagTotalPages) {
10179 return;
10180 }
10181 tagFetching = true;
10182 try {
10183 const next = tagPage + 1;
10184 const res = await client.fetchTagOptions(next, TAG_PAGE_SIZE);
10185 tagPage = next;
10186 tagTotalPages = Math.max(tagTotalPages, res.totalPages || next);
10187 const seen = new Set(filterData.tags.map((t) => t.id));
10188 for (const item of res.items) {
10189 if (!seen.has(item.id)) {
10190 filterData.tags.push(item);
10191 seen.add(item.id);
10192 }
10193 }
10194 filterData.tagsHasMore = tagPage < tagTotalPages;
10195 repaintColumns();
10196 } finally {
10197 tagFetching = false;
10198 }
10199 };
10200 filterData.loadMoreTags = () => {
10201 void fetchNextTagPage();
10202 };
10203 void fetchNextTagPage();
10204 const teardownKebabColumns = mountKebabColumnToggles(
10205 body,
10206 cellCache,
10207 repaintColumns,
10208 client
10209 );
10210 let unsubOsSettings = null;
10211 if (window.wp?.desktop && typeof window.wp.desktop.subscribeOsSettings === "function") {
10212 let lastHidden = JSON.stringify(
10213 Array.from(getHiddenColumns()).sort()
10214 );
10215 unsubOsSettings = window.wp.desktop.subscribeOsSettings(() => {
10216 const next = JSON.stringify(
10217 Array.from(getHiddenColumns()).sort()
10218 );
10219 if (next === lastHidden) {
10220 return;
10221 }
10222 lastHidden = next;
10223 repaintColumns();
10224 teardownKebabColumns?.refresh();
10225 });
10226 }
10227 const onWindowClosed = (e) => {
10228 const detail = e.detail;
10229 if (detail?.windowId !== "desktop-mode-posts") {
10230 return;
10231 }
10232 document.removeEventListener("desktop-mode-window-closed", onWindowClosed);
10233 for (const unsub of broadcastUnsubs) {
10234 try {
10235 unsub();
10236 } catch {
10237 }
10238 }
10239 broadcastUnsubs.length = 0;
10240 teardownKebabColumns?.dispose();
10241 unsubOsSettings?.();
10242 catsTeardown?.();
10243 catsTeardown = null;
10244 tagsTeardown?.();
10245 tagsTeardown = null;
10246 if (view.searchDebounce !== null) {
10247 window.clearTimeout(view.searchDebounce);
10248 view.searchDebounce = null;
10249 }
10250 clearCategoryTreeCache();
10251 };
10252 document.addEventListener("desktop-mode-window-closed", onWindowClosed);
10253 await refresh();
10254 const hooks = window.wp?.hooks;
10255 if (hooks && typeof hooks.doAction === "function") {
10256 hooks.doAction(HOOK_ACTION_OPENED, ctx);
10257 }
10258 document.dispatchEvent(
10259 new CustomEvent("desktop-mode-posts-window-opened", {
10260 detail: ctx
10261 })
10262 );
10263 }
10264 function buildBulkActionButton(action, ctx) {
10265 const btn = document.createElement("wpd-button");
10266 btn.setAttribute("variant", action.variant ?? "secondary");
10267 btn.setAttribute("data-desktop-mode-posts-bulk-action", action.id);
10268 if (action.icon) {
10269 const icon = document.createElement("span");
10270 icon.className = `dashicons ${action.icon}`;
10271 icon.setAttribute("aria-hidden", "true");
10272 btn.appendChild(icon);
10273 }
10274 btn.appendChild(document.createTextNode(" " + action.label));
10275 btn.addEventListener("click", () => {
10276 void runBulkActionFor(action, ctx);
10277 });
10278 return btn;
10279 }
10280 let activeRunBulkAction = async () => {
10281 };
10282 async function runBulkActionFor(action, ctx) {
10283 await activeRunBulkAction(action, ctx);
10284 }
10285 function openAdminUrl(url, opts = {}) {
10286 const api = window.wp?.desktop;
10287 if (!api || !api.windowManager || !api.deriveWindowId) {
10288 window.location.href = url;
10289 return;
10290 }
10291 const id = api.deriveWindowId(url);
10292 api.windowManager.open({
10293 id,
10294 baseId: id,
10295 url,
10296 title: opts.title ?? url,
10297 icon: opts.icon ?? "dashicons-admin-generic"
10298 });
10299 }
10300 function mapColumnToOrderby(key) {
10301 switch (key) {
10302 case "title":
10303 return "title";
10304 case "author":
10305 return "author";
10306 case "date":
10307 return "date";
10308 case "modified":
10309 return "modified";
10310 case "comments":
10311 return "comment_count";
10312 default:
10313 return "date";
10314 }
10315 }
10316 const registry = window.desktopModeNativeWindows ?? (window.desktopModeNativeWindows = {});
10317 registry["desktop-mode-posts"] = (body) => {
10318 const client = createPostsWindowClient("desktop-mode-posts");
10319 return renderPostsWindow(body, client).catch((err) => {
10320 console.error("[posts-window] render failed:", err);
10321 });
10322 };
10323 registry["desktop-mode-pages"] = (body) => {
10324 const client = createPostsWindowClient("desktop-mode-pages");
10325 return renderPostsWindow(body, client).catch((err) => {
10326 console.error("[pages-window] render failed:", err);
10327 });
10328 };
10329 registry["desktop-mode-users"] = (body) => {
10330 const client = createUsersWindowClient("desktop-mode-users");
10331 return Promise.resolve().then(() => usersRender).then((m) => m.renderUsersWindow(body, client)).catch((err) => {
10332 console.error("[users-window] render failed:", err);
10333 });
10334 };
10335 registry["desktop-mode-user-edit"] = (body) => {
10336 const profile = body.querySelector(
10337 "wpd-user-profile[data-wpd-user-profile-host]"
10338 );
10339 if (!profile) {
10340 return;
10341 }
10342 void Promise.resolve().then(() => userEditTarget).then((target) => {
10343 const pending = target.readUserEditTarget();
10344 let userId = pending.userId && pending.userId > 0 ? pending.userId : 0;
10345 if (userId <= 0) {
10346 try {
10347 userId = window.desktopModeWindowConfig?.["desktop-mode-user-edit"]?.currentUserId ?? 0;
10348 } catch {
10349 userId = 0;
10350 }
10351 }
10352 if (userId > 0) {
10353 profile.setAttribute("user-id", String(userId));
10354 }
10355 target.clearUserEditTarget();
10356 target.subscribeUserEditTarget((next) => {
10357 if (!profile.isConnected) {
10358 return;
10359 }
10360 if (next.userId && next.userId > 0 && next.userId !== userId) {
10361 userId = next.userId;
10362 profile.setAttribute("user-id", String(userId));
10363 target.clearUserEditTarget();
10364 }
10365 });
10366 });
10367 };
10368 function createUserEditClient(windowId = "desktop-mode-user-edit") {
10369 const getConfig = () => {
10370 const store = window.desktopModeWindowConfig;
10371 const cfg = store?.[windowId];
10372 if (!cfg) {
10373 throw new Error(
10374 `[${windowId}] config blob is missing — was the window opened without registration? See \`includes/user-edit-window/window.php\`.`
10375 );
10376 }
10377 return cfg;
10378 };
10379 const shellFetch = (input, init, source) => {
10380 return trackedFetch(input, init, {
10381 windowId,
10382 source: source ?? "user-edit-window/rest"
10383 });
10384 };
10385 const fetchUser = async (id) => {
10386 const cfg = getConfig();
10387 const base = cfg.usersUrl ?? joinRestUrl(cfg.restRoot, "wp/v2/users");
10388 const url = joinRestUrl(base, `${id}?context=edit`);
10389 const res = await shellFetch(
10390 url,
10391 {
10392 method: "GET",
10393 credentials: "same-origin",
10394 headers: {
10395 Accept: "application/json",
10396 "X-WP-Nonce": cfg.restNonce
10397 }
10398 },
10399 "user-edit-window/load"
10400 );
10401 if (!res.ok) {
10402 throw new Error(`[user-edit] load failed: ${res.status}`);
10403 }
10404 return await res.json();
10405 };
10406 const saveUser = async (id, patch) => {
10407 const cfg = getConfig();
10408 const base = cfg.usersUrl ?? joinRestUrl(cfg.restRoot, "wp/v2/users");
10409 const res = await shellFetch(
10410 joinRestUrl(base, `${id}?context=edit`),
10411 {
10412 method: "POST",
10413 // PUT == POST for WP REST when X-HTTP-Method-Override is unsupported.
10414 credentials: "same-origin",
10415 headers: {
10416 "Content-Type": "application/json",
10417 "X-WP-Nonce": cfg.restNonce,
10418 "X-HTTP-Method-Override": "PUT"
10419 },
10420 body: JSON.stringify(patch)
10421 },
10422 "user-edit-window/save"
10423 );
10424 if (!res.ok) {
10425 const data = await res.json().catch(() => ({}));
10426 const fieldErrors = {};
10427 const params = data.data?.params;
10428 if (params && typeof params === "object") {
10429 for (const [k, v] of Object.entries(params)) {
10430 fieldErrors[k] = String(v);
10431 }
10432 }
10433 return {
10434 ok: false,
10435 error: data.code ?? `http_${res.status}`,
10436 message: data.message,
10437 fieldErrors
10438 };
10439 }
10440 const user = await res.json();
10441 return { ok: true, user };
10442 };
10443 const fetchInsights = async (id, opts = {}) => {
10444 const cfg = getConfig();
10445 const base = cfg.insightsUrlBase ?? joinRestUrl(cfg.restRoot, "desktop-mode/v1/users/");
10446 const url = new URL(joinRestUrl(base, `${id}/insights`));
10447 if (opts.fresh) {
10448 url.searchParams.set("fresh", "1");
10449 }
10450 const res = await shellFetch(
10451 url.toString(),
10452 {
10453 method: "GET",
10454 credentials: "same-origin",
10455 headers: {
10456 Accept: "application/json",
10457 "X-WP-Nonce": cfg.restNonce
10458 }
10459 },
10460 "user-edit-window/insights"
10461 );
10462 if (!res.ok) {
10463 throw new Error(`[user-edit] insights failed: ${res.status}`);
10464 }
10465 return await res.json();
10466 };
10467 return {
10468 windowId,
10469 getConfig,
10470 fetchUser,
10471 saveUser,
10472 fetchInsights
10473 };
10474 }
10475 const styles$1 = css`:host{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var( --desktop-mode-text,#1d2327 );cursor:pointer}label{display:inline-flex;align-items:center;gap:6px;cursor:pointer}input[ type='checkbox' ]{accent-color:var( --wp-admin-theme-color,#2271b1 );cursor:pointer}`;
10476 const _WpdCheckboxLabel = class _WpdCheckboxLabel extends Component {
10477 render() {
10478 const label = this.label || "";
10479 const checked = this.checked !== null;
10480 return html`
10481 <label>
10482 <input
10483 type="checkbox"
10484 ?checked=${checked}
10485 @change=${(e) => this._onChange(e)}
10486 />
10487 <span class="wpd-checkbox-label__text">${label}</span>
10488 </label>
10489 `;
10490 }
10491 _onChange(e) {
10492 const next = e.target.checked;
10493 if (next) {
10494 this.setAttribute("checked", "");
10495 } else {
10496 this.removeAttribute("checked");
10497 }
10498 this.emit("wpd-checkbox-change", { checked: next });
10499 }
10500 };
10501 _WpdCheckboxLabel.props = ["label", "checked"];
10502 _WpdCheckboxLabel.styles = [styles$1];
10503 _WpdCheckboxLabel.help = {
10504 title: "Checkbox label",
10505 summary: "Opinionated label-row variant of <wpd-checkbox>: label text + checkbox in a single aligned row. Use when you want the shipped layout without any layout work.",
10506 status: "stable",
10507 since: "0.9.0",
10508 props: [
10509 {
10510 name: "label",
10511 type: "string",
10512 description: "Visible label text, paired with the checkbox via a native <label>."
10513 },
10514 {
10515 name: "checked",
10516 type: "boolean attribute",
10517 description: "Reflects and controls the checked state."
10518 }
10519 ],
10520 events: [
10521 {
10522 name: "wpd-checkbox-change",
10523 description: "Fires when the user toggles the checkbox.",
10524 detail: "{ checked: boolean }"
10525 }
10526 ],
10527 cssProps: [
10528 { name: "--desktop-mode-text", description: "Label colour." }
10529 ],
10530 example: html`
10531 <wpd-checkbox-label label="Reduce motion" checked></wpd-checkbox-label>
10532 `
10533 };
10534 let WpdCheckboxLabel = _WpdCheckboxLabel;
10535 defineComponent("wpd-checkbox-label", WpdCheckboxLabel);
10536 const styles = css`:host{display:inline-flex;align-items:center;justify-content:center;width:var( --wpd-icon-size,16px );height:var( --wpd-icon-size,16px );color:inherit;line-height:1}:host( [ hidden ] ){display:none}.wpd-icon__glyph{font-size:var( --wpd-icon-size,16px );width:var( --wpd-icon-size,16px );height:var( --wpd-icon-size,16px );line-height:1;color:inherit;display:inline-flex;align-items:center;justify-content:center}.wpd-icon__glyph--char{font-family:dashicons;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;speak:none}.wpd-icon__glyph.dashicons{font-family:dashicons}`;
10537 let _cache = null;
10538 function parseCssContentToChar(raw) {
10539 let value = raw.trim();
10540 if (value === "") {
10541 return null;
10542 }
10543 if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
10544 value = value.slice(1, -1);
10545 }
10546 const escaped = value.match(/^\\([0-9a-f]{1,6})\s?$/i);
10547 if (escaped) {
10548 return String.fromCodePoint(parseInt(escaped[1], 16));
10549 }
10550 return value || null;
10551 }
10552 function buildMap() {
10553 const map = /* @__PURE__ */ new Map();
10554 if (typeof document === "undefined") {
10555 return map;
10556 }
10557 const sheets = Array.from(document.styleSheets ?? []);
10558 for (const sheet of sheets) {
10559 let rules = null;
10560 try {
10561 rules = sheet.cssRules;
10562 } catch {
10563 continue;
10564 }
10565 if (!rules) {
10566 continue;
10567 }
10568 for (const rule of Array.from(rules)) {
10569 const styleRule = rule;
10570 if (!styleRule || !styleRule.selectorText) {
10571 continue;
10572 }
10573 const match = styleRule.selectorText.match(
10574 /\.dashicons-([a-z0-9-]+)::?before/i
10575 );
10576 if (!match) {
10577 continue;
10578 }
10579 const content = styleRule.style?.content;
10580 if (!content) {
10581 continue;
10582 }
10583 const char = parseCssContentToChar(content);
10584 if (char) {
10585 map.set(match[1], char);
10586 }
10587 }
10588 }
10589 return map;
10590 }
10591 function resolveDashicon(name) {
10592 if (!_cache) {
10593 _cache = buildMap();
10594 }
10595 const slug = name.startsWith("dashicons-") ? name.slice("dashicons-".length) : name;
10596 return _cache.get(slug) ?? null;
10597 }
10598 function refreshDashiconCache() {
10599 _cache = buildMap();
10600 }
10601 let _scheduled = false;
10602 function primeOnLoad() {
10603 if (_scheduled || typeof window === "undefined") {
10604 return;
10605 }
10606 _scheduled = true;
10607 const refresh = () => {
10608 refreshDashiconCache();
10609 };
10610 if (document.readyState === "loading") {
10611 document.addEventListener("DOMContentLoaded", refresh, { once: true });
10612 }
10613 window.addEventListener("load", refresh, { once: true });
10614 }
10615 primeOnLoad();
10616 const _WpdIcon = class _WpdIcon extends Component {
10617 render() {
10618 const rawName = this.name || "";
10619 const slug = rawName.startsWith("dashicons-") ? rawName.slice("dashicons-".length) : rawName;
10620 const size = this.size;
10621 if (size && /^\d+$/.test(size)) {
10622 this.style.setProperty("--wpd-icon-size", `${size}px`);
10623 }
10624 const char = resolveDashicon(slug);
10625 if (char) {
10626 return html`<span
10627 class="wpd-icon__glyph wpd-icon__glyph--char dashicons dashicons-${slug}"
10628 aria-hidden="true"
10629 >${char}</span>`;
10630 }
10631 return html`<span
10632 class="wpd-icon__glyph dashicons dashicons-${slug}"
10633 aria-hidden="true"
10634 ></span>`;
10635 }
10636 };
10637 _WpdIcon.props = ["name", "size"];
10638 _WpdIcon.styles = [styles];
10639 _WpdIcon.help = {
10640 title: "Icon",
10641 summary: 'Dashicon wrapper that inherits theme colour + sizing from its context. Accepts either the dashicon suffix ("calculator") or the full class ("dashicons-calculator"). Marked aria-hidden; wrap in a button/link with its own label for accessible use.',
10642 status: "stable",
10643 since: "0.10.0",
10644 props: [
10645 {
10646 name: "name",
10647 type: "string",
10648 description: "Dashicon identifier, with or without the `dashicons-` prefix."
10649 },
10650 {
10651 name: "size",
10652 type: "integer (px)",
10653 default: "16",
10654 description: "Glyph size in pixels."
10655 }
10656 ],
10657 cssProps: [
10658 { name: "--wpd-icon-size", default: "16px" }
10659 ],
10660 example: html`
10661 <wpd-cluster gap="8" align="center">
10662 <wpd-icon name="admin-post"></wpd-icon>
10663 <wpd-icon name="calculator" size="20"></wpd-icon>
10664 <wpd-icon name="dashicons-star-filled" size="32"></wpd-icon>
10665 </wpd-cluster>
10666 `
10667 };
10668 let WpdIcon = _WpdIcon;
10669 defineComponent("wpd-icon", WpdIcon);
10670 const textFieldStyles = css`:host{display:flex;flex-direction:column;gap:4px;font-size:13px;color:var( --desktop-mode-text,#1d2327 );min-width:0}:host( [ hidden ] ){display:none}.wpd-text-field__label{font-size:12px;color:var( --desktop-mode-muted,#646970 )}.wpd-text-field__row{position:relative;display:flex;align-items:center;width:100%}input{appearance:none;-webkit-appearance:none;display:block;width:100%;min-width:0;box-sizing:border-box;padding:7px 10px;background:var( --desktop-mode-window-bg,#fff );border:1px solid var( --desktop-mode-border,#dcdcde );border-radius:6px;font:inherit;font-size:13px;color:var( --desktop-mode-text,#1d2327 );transition:border-color 0.12s ease,box-shadow 0.12s ease}.wpd-text-field__suffix{position:absolute;inset-inline-end:10px;top:50%;transform:translateY( -50% );pointer-events:none;font-size:12px;color:var( --desktop-mode-muted,#646970 )}.wpd-text-field__row--has-reveal input{padding-inline-end:36px}.wpd-text-field__reveal{position:absolute;inset-inline-end:0;top:0;bottom:0;width:34px;display:flex;align-items:center;justify-content:center;padding:0;border:none;background:transparent;color:var( --desktop-mode-muted,#646970 );cursor:pointer;border-radius:0 6px 6px 0;transition:color 0.12s ease}.wpd-text-field__reveal:hover{color:var( --wp-admin-theme-color,#2271b1 )}.wpd-text-field__reveal:focus-visible{outline:2px solid var( --wp-admin-theme-color,#2271b1 );outline-offset:-2px;border-radius:0 6px 6px 0}.wpd-text-field__reveal:disabled{opacity:0.45;cursor:not-allowed}.wpd-text-field__input--masked{-webkit-text-security:disc;text-security:disc}@supports not ( ( -webkit-text-security:disc ) or ( text-security:disc ) ){.wpd-text-field__input--masked{font-family:text-security-disc,"password",monospace;letter-spacing:0.2em}}input:hover{border-color:var( --desktop-mode-muted,#8c8f94 )}input:focus-visible{outline:none;border-color:var( --wp-admin-theme-color,#2271b1 );box-shadow:0 0 0 1px var( --wp-admin-theme-color,#2271b1 )}input:disabled{opacity:0.55;cursor:not-allowed;background:rgba( 0,0,0,0.03 )}input[ aria-invalid='true' ]{border-color:#d63638}input[ aria-invalid='true' ]:focus-visible{box-shadow:0 0 0 1px #d63638}input[ type='number' ]::-webkit-inner-spin-button,input[ type='number' ]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}input[ type='number' ]{-moz-appearance:textfield}`;
10671 const _WpdTextField = class _WpdTextField extends Component {
10672 constructor() {
10673 super(...arguments);
10674 this._revealed = false;
10675 }
10676 connectedCallback() {
10677 super.connectedCallback();
10678 ensureAutoId(this);
10679 }
10680 render() {
10681 const label = this.label || "";
10682 const value = this.value ?? "";
10683 const placeholder = this.placeholder || "";
10684 const disabled = this.disabled !== null;
10685 const readonly = this.readonly !== null;
10686 const declaredAutocomplete = this.autocomplete;
10687 const declaredType = this.type || "text";
10688 const isPassword = declaredType === "password";
10689 let autocomplete = declaredAutocomplete || "off";
10690 if (isPassword && (!declaredAutocomplete || autocomplete === "off")) {
10691 autocomplete = "new-password";
10692 }
10693 const maxLength = this.maxlength;
10694 const minLength = this.minlength;
10695 const pattern = this.pattern || "";
10696 const name = this.name || "";
10697 const suffix = this.suffix || "";
10698 const invalid = this.invalid !== null;
10699 const reveal = this.reveal !== null;
10700 const isPasswordIntent = declaredType === "password";
10701 const isMasked = isPasswordIntent && !(reveal && this._revealed);
10702 let effectiveType;
10703 if (isPasswordIntent) {
10704 effectiveType = "text";
10705 } else if (reveal && this._revealed) {
10706 effectiveType = "text";
10707 } else {
10708 effectiveType = declaredType;
10709 }
10710 const rowClass = reveal ? "wpd-text-field__row wpd-text-field__row--has-reveal" : "wpd-text-field__row";
10711 const inputClass = isMasked ? "wpd-text-field__input wpd-text-field__input--masked" : "wpd-text-field__input";
10712 const hostId = this.id || "wpd-unnamed";
10713 const inputId = `${hostId}__input`;
10714 return html`
10715 ${label ? html`<label
10716 class="wpd-text-field__label"
10717 for=${inputId}
10718 >${label}</label>` : html``}
10719 <span class=${rowClass}>
10720 <input
10721 id=${inputId}
10722 class=${inputClass}
10723 type=${effectiveType}
10724 .value=${value}
10725 placeholder=${placeholder}
10726 ?disabled=${disabled}
10727 ?readonly=${readonly}
10728 autocomplete=${autocomplete}
10729 maxlength=${maxLength ?? ""}
10730 minlength=${minLength ?? ""}
10731 pattern=${pattern}
10732 name=${name}
10733 aria-invalid=${invalid ? "true" : "false"}
10734 aria-label=${label || ""}
10735 @input=${(e) => this._onInput(e)}
10736 @change=${(e) => this._onChange(e)}
10737 @keydown=${(e) => this._onKeyDown(e)}
10738 />
10739 ${suffix ? html`<span class="wpd-text-field__suffix">${suffix}</span>` : html``}
10740 ${reveal ? this._renderRevealButton(disabled) : html``}
10741 </span>
10742 `;
10743 }
10744 _renderRevealButton(disabled) {
10745 const label = this._revealed ? "Hide" : "Show";
10746 return html`
10747 <button
10748 type="button"
10749 class="wpd-text-field__reveal"
10750 aria-label=${label}
10751 aria-pressed=${this._revealed ? "true" : "false"}
10752 ?disabled=${disabled}
10753 tabindex="0"
10754 @click=${() => this._onToggleReveal()}
10755 >
10756 ${this._revealed ? _iconEyeOff() : _iconEye()}
10757 </button>
10758 `;
10759 }
10760 _onToggleReveal() {
10761 this._revealed = !this._revealed;
10762 this.requestUpdate();
10763 }
10764 _onInput(e) {
10765 const input = e.target;
10766 this.value = input.value;
10767 this.emit("wpd-input-change", { value: input.value });
10768 }
10769 _onChange(e) {
10770 const input = e.target;
10771 this.emit("wpd-input-commit", { value: input.value });
10772 }
10773 _onKeyDown(e) {
10774 if (e.key === "Enter" && !e.shiftKey && !e.altKey && !e.metaKey) {
10775 const input = e.target;
10776 this.emit("wpd-submit", { value: input.value });
10777 }
10778 }
10779 };
10780 _WpdTextField.props = [
10781 "label",
10782 "value",
10783 "placeholder",
10784 "disabled",
10785 "readonly",
10786 "autocomplete",
10787 "type",
10788 "maxlength",
10789 "minlength",
10790 "pattern",
10791 "name",
10792 "suffix",
10793 "invalid",
10794 "reveal"
10795 ];
10796 _WpdTextField.styles = [textFieldStyles];
10797 _WpdTextField.help = {
10798 title: "Text field",
10799 summary: "Labelled text input primitive. Two-way reflects `value`, emits wpd-input-change per keystroke, wpd-input-commit on blur/change, and wpd-submit on Enter. Optional password reveal toggle.",
10800 status: "stable",
10801 since: "0.11.0",
10802 props: [
10803 { name: "label", type: "string", description: "Visible label above the input." },
10804 { name: "value", type: "string", description: "Current input value; reflected two-way." },
10805 { name: "placeholder", type: "string", description: "Native placeholder string." },
10806 { name: "disabled", type: "boolean attribute", description: "Disables the native input." },
10807 { name: "readonly", type: "boolean attribute", description: "Marks the input readonly." },
10808 {
10809 name: "autocomplete",
10810 type: "string",
10811 default: "off",
10812 description: "Forwarded to the native input autocomplete attribute."
10813 },
10814 {
10815 name: "type",
10816 type: "string",
10817 default: "text",
10818 description: "Native input type (text, password, email, search, tel, url)."
10819 },
10820 { name: "maxlength", type: "integer (string)", description: "Native maxlength." },
10821 { name: "minlength", type: "integer (string)", description: "Native minlength." },
10822 { name: "pattern", type: "regex string", description: "Native validation pattern." },
10823 { name: "name", type: "string", description: "Forwarded to the native input for form submission." },
10824 { name: "suffix", type: "string", description: "Text rendered inside the right edge of the input row." },
10825 {
10826 name: "invalid",
10827 type: "boolean attribute",
10828 description: "Marks the field aria-invalid and applies the error style."
10829 },
10830 {
10831 name: "reveal",
10832 type: "boolean attribute",
10833 description: 'On type="password" fields, adds an eye-icon toggle that flips the input between hidden and visible text.'
10834 }
10835 ],
10836 events: [
10837 {
10838 name: "wpd-input-change",
10839 description: "Fires on every input keystroke.",
10840 detail: "{ value: string }"
10841 },
10842 {
10843 name: "wpd-input-commit",
10844 description: "Fires on the native change event (blur / Enter).",
10845 detail: "{ value: string }"
10846 },
10847 {
10848 name: "wpd-submit",
10849 description: "Fires when the user presses Enter (without Shift/Alt/Meta).",
10850 detail: "{ value: string }"
10851 }
10852 ],
10853 cssProps: [
10854 { name: "--desktop-mode-text", description: "Text colour." },
10855 { name: "--desktop-mode-muted", description: "Label + suffix colour." },
10856 { name: "--desktop-mode-border", description: "Input outline." },
10857 { name: "--desktop-mode-window-bg", description: "Input background." }
10858 ],
10859 example: html`
10860 <wpd-stack gap="8">
10861 <wpd-text-field label="Note title" value="Untitled" placeholder="Name this note"></wpd-text-field>
10862 <wpd-text-field type="password" reveal label="API key"></wpd-text-field>
10863 </wpd-stack>
10864 `
10865 };
10866 let WpdTextField = _WpdTextField;
10867 defineComponent("wpd-text-field", WpdTextField);
10868 function _iconEye() {
10869 return html`
10870 <svg
10871 viewBox="0 0 16 16"
10872 width="14"
10873 height="14"
10874 fill="none"
10875 stroke="currentColor"
10876 stroke-width="1.5"
10877 stroke-linecap="round"
10878 stroke-linejoin="round"
10879 aria-hidden="true"
10880 focusable="false"
10881 >
10882 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
10883 <circle cx="8" cy="8" r="2" />
10884 </svg>
10885 `;
10886 }
10887 function _iconEyeOff() {
10888 return html`
10889 <svg
10890 viewBox="0 0 16 16"
10891 width="14"
10892 height="14"
10893 fill="none"
10894 stroke="currentColor"
10895 stroke-width="1.5"
10896 stroke-linecap="round"
10897 stroke-linejoin="round"
10898 aria-hidden="true"
10899 focusable="false"
10900 >
10901 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
10902 <circle cx="8" cy="8" r="2" />
10903 <line x1="2" y1="2" x2="14" y2="14" />
10904 </svg>
10905 `;
10906 }
10907 function resolveUserEditClient() {
10908 const store = window.desktopModeWindowConfig;
10909 if (store?.["desktop-mode-user-edit"]) {
10910 return createUserEditClient("desktop-mode-user-edit");
10911 }
10912 if (store?.["desktop-mode-users"]) {
10913 return createUserEditClient("desktop-mode-users");
10914 }
10915 return createUserEditClient("desktop-mode-user-edit");
10916 }
10917 function notifyToast$1(body, kind = "info") {
10918 const api = window.wp?.desktop;
10919 if (api?.showToast) {
10920 let duration;
10921 if (kind === "error") {
10922 duration = 8e3;
10923 } else if (kind === "success") {
10924 duration = 5e3;
10925 }
10926 api.showToast({ message: body, duration });
10927 return;
10928 }
10929 console.info("[user-edit-window]", body);
10930 }
10931 async function mountProfileFormAt(host, userId) {
10932 return loadAndMountProfile(host, userId);
10933 }
10934 async function mountProfileAsideAt(host, userId, fresh) {
10935 return renderInsightsAside(host, userId, fresh);
10936 }
10937 async function mountProfileActivityAt(host, userId, fresh) {
10938 return renderInsightsActivity(host, userId, fresh);
10939 }
10940 async function loadAndMountProfile(host, userId) {
10941 host.replaceChildren();
10942 const skeleton = document.createElement("div");
10943 skeleton.className = "desktop-mode-user-edit__skeleton";
10944 skeleton.style.cssText = "display:flex;align-items:center;justify-content:center;padding:48px;color:var(--desktop-mode-muted, #50575e);font-size:13px;";
10945 skeleton.textContent = __("Loading profile…");
10946 host.appendChild(skeleton);
10947 let user;
10948 try {
10949 user = await resolveUserEditClient().fetchUser(userId);
10950 } catch (err) {
10951 host.replaceChildren();
10952 const msg = document.createElement("p");
10953 msg.style.cssText = "padding:32px;color:#b32d2e;font-size:13px;text-align:center;";
10954 msg.textContent = sprintf(
10955 // translators: %s is an error message.
10956 __("Could not load profile (%s)."),
10957 String(err.message ?? err)
10958 );
10959 host.appendChild(msg);
10960 throw err;
10961 }
10962 host.replaceChildren();
10963 mountProfileForm(host, user, userId);
10964 return user;
10965 }
10966 function resolveProfileConfig() {
10967 const store = window.desktopModeWindowConfig;
10968 const userEdit = store?.["desktop-mode-user-edit"];
10969 const users = store?.["desktop-mode-users"];
10970 return {
10971 ...users ?? {},
10972 ...userEdit ?? {}
10973 };
10974 }
10975 function mountProfileForm(host, user, userId) {
10976 const cfg = resolveProfileConfig();
10977 const wrap = document.createElement("div");
10978 wrap.className = "desktop-mode-user-edit__profile";
10979 const form = document.createElement("wpd-form");
10980 form.setAttribute("submit-label", __("Save changes"));
10981 form.setAttribute("reset-label", __("Revert"));
10982 form.setAttribute("columns", "auto");
10983 const header = document.createElement("div");
10984 header.setAttribute("slot", "header");
10985 let profileHeader = buildProfileHeader(user);
10986 header.appendChild(profileHeader);
10987 form.appendChild(header);
10988 form.appendChild(textField("username", __("Username"), user.username, {
10989 readonly: true
10990 }));
10991 form.appendChild(textField("first_name", __("First name"), user.first_name));
10992 form.appendChild(textField("last_name", __("Last name"), user.last_name));
10993 form.appendChild(
10994 textField("nickname", __("Nickname"), user.nickname ?? "", {
10995 required: true,
10996 fullWidth: false
10997 })
10998 );
10999 const displaySelect = document.createElement("wpd-select");
11000 displaySelect.setAttribute("name", "name");
11001 displaySelect.setAttribute("label", __("Display name publicly as"));
11002 displaySelect.items = displayNameCandidates(user);
11003 displaySelect.value = user.name;
11004 form.appendChild(displaySelect);
11005 form.appendChild(
11006 textField("email", __("Email (required)"), user.email, {
11007 required: true,
11008 type: "email"
11009 })
11010 );
11011 form.appendChild(textField("url", __("Website"), user.url, { type: "url" }));
11012 const contactMethods = cfg.contactMethods ?? {};
11013 for (const [slug, label] of Object.entries(contactMethods)) {
11014 const value = typeof user.meta === "object" && user.meta !== null ? String(
11015 user.meta[slug] ?? ""
11016 ) : "";
11017 form.appendChild(
11018 textField(`meta.${slug}`, label, value, {
11019 dataset: { meta: slug }
11020 })
11021 );
11022 }
11023 const bio = document.createElement("wpd-textarea");
11024 bio.setAttribute("name", "description");
11025 bio.setAttribute("label", __("Biographical info"));
11026 bio.setAttribute(
11027 "placeholder",
11028 __("Share a little about yourself — visible on author archives.")
11029 );
11030 bio.setAttribute("rows", "4");
11031 bio.setAttribute("full-width", "");
11032 bio.value = user.description;
11033 bio.setAttribute("value", user.description);
11034 form.appendChild(bio);
11035 const localeSelect = document.createElement("wpd-select");
11036 localeSelect.setAttribute("name", "locale");
11037 localeSelect.setAttribute("label", __("Language"));
11038 const locales = cfg.locales ?? { "": __("Site default") };
11039 localeSelect.items = Object.entries(locales).map(([value, label]) => ({
11040 value,
11041 label
11042 }));
11043 localeSelect.value = String(user.locale ?? "");
11044 form.appendChild(localeSelect);
11045 const isSelfEdit = userId === (cfg.currentUserId ?? 0);
11046 const roleMap = (() => {
11047 const assignable = cfg.assignableRoles;
11048 if (assignable && Object.keys(assignable).length > 0) {
11049 return assignable;
11050 }
11051 return cfg.allRoles ?? {};
11052 })();
11053 if (!isSelfEdit) {
11054 const roleSelect = document.createElement("wpd-select");
11055 roleSelect.setAttribute("name", "roles[0]");
11056 roleSelect.setAttribute("label", __("Role"));
11057 roleSelect.items = Object.entries(roleMap).map(([value, label]) => ({
11058 value,
11059 label
11060 }));
11061 const currentRole = Array.isArray(user.roles) ? user.roles[0] ?? "" : "";
11062 roleSelect.value = currentRole;
11063 form.appendChild(roleSelect);
11064 }
11065 {
11066 const optsHeading = document.createElement("h3");
11067 optsHeading.setAttribute("full-width", "");
11068 optsHeading.textContent = __("Personal options");
11069 optsHeading.style.cssText = "margin:18px 0 4px;font-size:13px;font-weight:600;text-transform:uppercase;letter-spacing:0.04em;color:var(--desktop-mode-muted, #50575e);";
11070 form.appendChild(optsHeading);
11071 const meta = user.meta ?? {};
11072 const richEditing = String(meta.rich_editing ?? "") !== "false";
11073 const syntaxHighlighting = String(meta.syntax_highlighting ?? "") !== "false";
11074 const commentShortcuts = String(meta.comment_shortcuts ?? "false") === "true";
11075 const adminBarFront = String(meta.show_admin_bar_front ?? "true") !== "false";
11076 form.appendChild(
11077 checkboxField(
11078 "meta.rich_editing",
11079 __("Disable the visual editor when writing"),
11080 !richEditing,
11081 { trueValue: "false", falseValue: "true", fullWidth: true }
11082 )
11083 );
11084 form.appendChild(
11085 checkboxField(
11086 "meta.syntax_highlighting",
11087 __("Disable syntax highlighting when editing code"),
11088 !syntaxHighlighting,
11089 { trueValue: "false", falseValue: "true", fullWidth: true }
11090 )
11091 );
11092 form.appendChild(
11093 checkboxField(
11094 "meta.comment_shortcuts",
11095 __("Enable keyboard shortcuts for comment moderation"),
11096 commentShortcuts,
11097 { trueValue: "true", falseValue: "false", fullWidth: true }
11098 )
11099 );
11100 form.appendChild(
11101 checkboxField(
11102 "meta.show_admin_bar_front",
11103 __("Show toolbar when viewing site"),
11104 adminBarFront,
11105 { trueValue: "true", falseValue: "false", fullWidth: true }
11106 )
11107 );
11108 const colorSchemes = cfg.colorSchemes ?? {};
11109 const currentScheme = String(meta.admin_color ?? "fresh");
11110 form.appendChild(
11111 buildAdminColorPicker(colorSchemes, currentScheme, {
11112 livePreview: isSelfEdit
11113 })
11114 );
11115 }
11116 const pwdHeading = document.createElement("h3");
11117 pwdHeading.setAttribute("full-width", "");
11118 pwdHeading.textContent = __("Account management");
11119 pwdHeading.style.cssText = "margin:18px 0 4px;font-size:13px;font-weight:600;text-transform:uppercase;letter-spacing:0.04em;color:var(--desktop-mode-muted, #50575e);";
11120 form.appendChild(pwdHeading);
11121 const pwdRow = document.createElement("div");
11122 pwdRow.setAttribute("full-width", "");
11123 pwdRow.style.cssText = "display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap;";
11124 const pwd = document.createElement("wpd-text-field");
11125 pwd.setAttribute("name", "password");
11126 pwd.setAttribute("type", "password");
11127 pwd.setAttribute("reveal", "");
11128 pwd.setAttribute("label", __("New password"));
11129 pwd.setAttribute(
11130 "placeholder",
11131 __("Leave blank to keep the current password.")
11132 );
11133 pwd.setAttribute("autocomplete", "new-password");
11134 pwd.style.flex = "1 1 280px";
11135 pwdRow.appendChild(pwd);
11136 const genBtn = document.createElement("wpd-button");
11137 genBtn.setAttribute("variant", "ghost");
11138 genBtn.setAttribute("type", "button");
11139 const genIcon = document.createElement("wpd-icon");
11140 genIcon.setAttribute("name", "randomize");
11141 genIcon.setAttribute("size", "14");
11142 genBtn.appendChild(genIcon);
11143 genBtn.appendChild(document.createTextNode(__("Generate strong")));
11144 genBtn.addEventListener("click", (e) => {
11145 e.preventDefault();
11146 const next = generateStrongPassword$1(18);
11147 pwd.value = next;
11148 pwd.setAttribute("value", next);
11149 const pwdConfirmEl = form.querySelector(
11150 'wpd-text-field[name="password_confirm"]'
11151 );
11152 if (pwdConfirmEl) {
11153 pwdConfirmEl.value = next;
11154 pwdConfirmEl.setAttribute("value", next);
11155 }
11156 void navigator.clipboard?.writeText(next).catch(() => {
11157 });
11158 notifyToast$1(__("Password generated and copied to clipboard."), "success");
11159 });
11160 pwdRow.appendChild(genBtn);
11161 form.appendChild(pwdRow);
11162 const pwdConfirm = document.createElement("wpd-text-field");
11163 pwdConfirm.setAttribute("name", "password_confirm");
11164 pwdConfirm.setAttribute("type", "password");
11165 pwdConfirm.setAttribute("reveal", "");
11166 pwdConfirm.setAttribute("label", __("Confirm new password"));
11167 pwdConfirm.setAttribute(
11168 "placeholder",
11169 __("Type the new password again.")
11170 );
11171 pwdConfirm.setAttribute("autocomplete", "new-password");
11172 pwdConfirm.setAttribute("full-width", "");
11173 form.appendChild(pwdConfirm);
11174 form.appendChild(
11175 buildSessionsRow(userId, isSelfEdit)
11176 );
11177 form.appendChild(buildAppPasswordsRow(userId));
11178 if (!isSelfEdit && cfg.isMultisite && user.meta?.is_super_admin !== void 0) {
11179 form.appendChild(
11180 checkboxField(
11181 "meta.is_super_admin",
11182 __("Grant super admin privileges for the network"),
11183 Boolean(
11184 user.meta?.is_super_admin
11185 ),
11186 { trueValue: "true", falseValue: "false", fullWidth: true }
11187 )
11188 );
11189 }
11190 let pending = false;
11191 form.addEventListener("wpd-form-submit", (e) => {
11192 const detail = e.detail;
11193 void onSubmit(detail.values);
11194 });
11195 const onSubmit = async (values) => {
11196 if (pending) {
11197 return;
11198 }
11199 pending = true;
11200 form.setBusy(true);
11201 form.clearErrors();
11202 const patch = {
11203 first_name: values.first_name,
11204 last_name: values.last_name,
11205 nickname: values.nickname,
11206 name: values.name,
11207 email: values.email,
11208 url: values.url,
11209 description: values.description,
11210 locale: values.locale ?? ""
11211 };
11212 if (typeof values.password === "string" && values.password !== "") {
11213 const confirm = String(values.password_confirm ?? "");
11214 if (confirm !== values.password) {
11215 form.setError(__("The two password fields do not match."));
11216 form.setFieldInvalid("password_confirm");
11217 pending = false;
11218 form.setBusy(false);
11219 return;
11220 }
11221 patch.password = values.password;
11222 }
11223 if (typeof values["roles[0]"] === "string" && values["roles[0]"]) {
11224 patch.roles = [values["roles[0]"]];
11225 }
11226 const meta = {};
11227 for (const [k, v] of Object.entries(values)) {
11228 if (!k.startsWith("meta.")) {
11229 continue;
11230 }
11231 let resolved = v;
11232 if (typeof v === "boolean") {
11233 const field = form.querySelector(`[name="${k}"]`);
11234 const valueAttr = field?.getAttribute("value");
11235 resolved = valueAttr ?? String(v);
11236 }
11237 meta[k.slice(5)] = resolved;
11238 }
11239 if (Object.keys(meta).length > 0) {
11240 patch.meta = meta;
11241 }
11242 const result = await resolveUserEditClient().saveUser(userId, patch);
11243 pending = false;
11244 form.setBusy(false);
11245 if (!result.ok) {
11246 const summary = result.message ?? mapErrorCode(result.error) ?? __("Save failed.");
11247 form.setError(summary);
11248 notifyToast$1(summary, "error");
11249 if (result.fieldErrors) {
11250 for (const field of Object.keys(result.fieldErrors)) {
11251 form.setFieldInvalid(field);
11252 }
11253 }
11254 console.warn("[user-edit] save failed", {
11255 code: result.error,
11256 message: result.message
11257 });
11258 return;
11259 }
11260 notifyToast$1(__("Profile saved."), "success");
11261 const broadcastApi = window.wp?.desktop;
11262 broadcastApi?.broadcast?.("desktop-mode.user.changed", {
11263 source: "user-edit-window",
11264 action: "updated",
11265 ids: [userId]
11266 });
11267 pwd.value = "";
11268 pwd.setAttribute("value", "");
11269 pwdConfirm.value = "";
11270 pwdConfirm.setAttribute("value", "");
11271 if (result.user) {
11272 Object.assign(user, result.user);
11273 const next = buildProfileHeader(user);
11274 profileHeader.replaceWith(next);
11275 profileHeader = next;
11276 const aside = host.ownerDocument?.querySelector(
11277 "[data-wpd-user-profile-aside]"
11278 );
11279 if (aside) {
11280 void mountProfileAsideAt(aside, userId, true);
11281 }
11282 }
11283 };
11284 wrap.appendChild(form);
11285 host.appendChild(wrap);
11286 }
11287 function buildProfileHeader(user) {
11288 const wrap = document.createElement("div");
11289 wrap.className = "desktop-mode-user-edit__header";
11290 wrap.style.cssText = "display:flex;align-items:center;gap:16px;margin:0 0 12px;";
11291 const avatar = document.createElement("wpd-avatar");
11292 avatar.setAttribute("size", "64");
11293 if (user.name || user.username) {
11294 avatar.setAttribute("name", user.name || user.username || "");
11295 }
11296 if (user.id > 0) {
11297 avatar.setAttribute("user-id", String(user.id));
11298 }
11299 const avatars = user.avatar_urls ?? {};
11300 const rawAvatar = avatars["96"] ?? avatars["48"] ?? "";
11301 if (rawAvatar) {
11302 applyAvatarSrc(avatar, rawAvatar);
11303 }
11304 wrap.appendChild(avatar);
11305 const text = document.createElement("div");
11306 text.style.cssText = "min-width:0;display:flex;flex-direction:column;gap:4px;";
11307 const name = document.createElement("div");
11308 name.style.cssText = "font-size:18px;font-weight:600;letter-spacing:-0.01em;";
11309 name.textContent = user.name || user.username || `#${user.id}`;
11310 text.appendChild(name);
11311 const sub = document.createElement("div");
11312 sub.style.cssText = "display:flex;align-items:center;gap:6px;font-size:12px;color:var(--desktop-mode-muted, #50575e);flex-wrap:wrap;";
11313 const handle = document.createElement("span");
11314 handle.textContent = `@${user.username}`;
11315 sub.appendChild(handle);
11316 const dot = document.createElement("span");
11317 dot.textContent = "·";
11318 dot.setAttribute("aria-hidden", "true");
11319 sub.appendChild(dot);
11320 const roleStr = Array.isArray(user.roles) ? user.roles.join(", ") : "";
11321 const roleSpan = document.createElement("span");
11322 roleSpan.textContent = roleStr || __("No role");
11323 sub.appendChild(roleSpan);
11324 text.appendChild(sub);
11325 wrap.appendChild(text);
11326 return wrap;
11327 }
11328 async function loadInsightsInto(host, userId, fresh) {
11329 host.replaceChildren();
11330 const skeleton = document.createElement("div");
11331 skeleton.style.cssText = "display:flex;align-items:center;justify-content:center;padding:32px;color:var(--desktop-mode-muted, #50575e);font-size:13px;";
11332 skeleton.textContent = __("Loading insights…");
11333 host.appendChild(skeleton);
11334 try {
11335 return await resolveUserEditClient().fetchInsights(userId, { fresh });
11336 } catch (err) {
11337 host.replaceChildren();
11338 const msg = document.createElement("p");
11339 msg.style.cssText = "padding:24px;color:#b32d2e;font-size:13px;text-align:center;";
11340 msg.textContent = sprintf(
11341 // translators: %s is an error message.
11342 __("Could not load insights (%s)."),
11343 String(err.message ?? err)
11344 );
11345 host.appendChild(msg);
11346 return null;
11347 }
11348 }
11349 async function renderInsightsAside(host, userId, fresh) {
11350 const data = await loadInsightsInto(host, userId, fresh);
11351 if (!data) {
11352 return;
11353 }
11354 host.replaceChildren();
11355 host.appendChild(buildAsideSummary(data));
11356 host.appendChild(buildAsideStatGrid(data));
11357 host.appendChild(buildContentSparkline(data));
11358 }
11359 async function renderInsightsActivity(host, userId, fresh) {
11360 const data = await loadInsightsInto(host, userId, fresh);
11361 if (!data) {
11362 return;
11363 }
11364 host.replaceChildren();
11365 const wrap = document.createElement("div");
11366 wrap.className = "desktop-mode-user-edit__activity";
11367 const heading = document.createElement("h3");
11368 heading.textContent = __("Recent activity");
11369 heading.style.cssText = "margin:24px 0 12px;font-size:13px;font-weight:600;text-transform:uppercase;letter-spacing:0.04em;color:var(--desktop-mode-muted, #50575e);";
11370 wrap.appendChild(heading);
11371 wrap.appendChild(buildRecentLists(data));
11372 wrap.appendChild(buildSecurityPanel(data));
11373 host.appendChild(wrap);
11374 }
11375 function buildAsideSummary(data) {
11376 const card = document.createElement("div");
11377 card.style.cssText = [
11378 "display:flex",
11379 "flex-direction:column",
11380 "align-items:center",
11381 "text-align:center",
11382 "gap:6px",
11383 "padding:16px",
11384 "border:1px solid var(--desktop-mode-border, #dcdcde)",
11385 "border-radius:12px",
11386 "background:var(--wp-admin-theme-bg-elevated, #f6f7f7)"
11387 ].join(";");
11388 const avatar = document.createElement("img");
11389 avatar.src = data.avatarUrl;
11390 avatar.alt = "";
11391 avatar.style.cssText = "width:72px;height:72px;border-radius:50%;flex-shrink:0;";
11392 card.appendChild(avatar);
11393 const name = document.createElement("div");
11394 name.style.cssText = "font-size:15px;font-weight:600;letter-spacing:-0.01em;";
11395 name.textContent = data.displayName || `#${data.userId}`;
11396 card.appendChild(name);
11397 const roles = document.createElement("div");
11398 roles.style.cssText = "display:flex;flex-wrap:wrap;gap:4px;justify-content:center;";
11399 for (const role of data.roles) {
11400 const chip = document.createElement("span");
11401 chip.textContent = role;
11402 chip.style.cssText = [
11403 "display:inline-flex",
11404 "padding:2px 8px",
11405 "border-radius:10px",
11406 "background:rgba(34,113,177,0.10)",
11407 "color:#0a4b78",
11408 "font-size:11px",
11409 "font-weight:600"
11410 ].join(";");
11411 roles.appendChild(chip);
11412 }
11413 if (data.roles.length === 0) {
11414 const noRole = document.createElement("span");
11415 noRole.textContent = __("No role");
11416 noRole.style.cssText = "font-size:11px;color:var(--desktop-mode-muted, #8c8f94);";
11417 roles.appendChild(noRole);
11418 }
11419 card.appendChild(roles);
11420 const completeness = data.profileCompleteness;
11421 if (completeness && completeness.total > 0) {
11422 const cwrap = document.createElement("div");
11423 cwrap.style.cssText = "display:flex;flex-direction:column;gap:4px;width:100%;margin-top:6px;";
11424 const top = document.createElement("div");
11425 top.style.cssText = "display:flex;justify-content:space-between;align-items:baseline;font-size:11px;color:var(--desktop-mode-muted, #50575e);";
11426 const lbl = document.createElement("span");
11427 lbl.textContent = __("Profile completeness");
11428 const pct = document.createElement("span");
11429 pct.style.cssText = "font-variant-numeric:tabular-nums;font-weight:600;";
11430 pct.textContent = `${completeness.percent}%`;
11431 top.appendChild(lbl);
11432 top.appendChild(pct);
11433 cwrap.appendChild(top);
11434 const track = document.createElement("div");
11435 track.style.cssText = [
11436 "height:4px",
11437 "border-radius:999px",
11438 "background:rgba(0,0,0,0.06)",
11439 "position:relative",
11440 "overflow:hidden"
11441 ].join(";");
11442 const bar = document.createElement("div");
11443 bar.style.cssText = [
11444 "position:absolute",
11445 "inset:0",
11446 `width:${completeness.percent}%`,
11447 "background:var(--wp-admin-theme-color, #2271b1)",
11448 "transition:width 360ms ease"
11449 ].join(";");
11450 track.appendChild(bar);
11451 cwrap.appendChild(track);
11452 card.appendChild(cwrap);
11453 }
11454 return card;
11455 }
11456 function buildAsideStatGrid(data) {
11457 const grid = document.createElement("div");
11458 grid.style.cssText = [
11459 "display:grid",
11460 "grid-template-columns:1fr 1fr",
11461 "gap:8px",
11462 "margin-top:12px"
11463 ].join(";");
11464 const tile = (label, value, sub) => {
11465 const card = document.createElement("div");
11466 card.style.cssText = [
11467 "border:1px solid var(--desktop-mode-border, #dcdcde)",
11468 "border-radius:8px",
11469 "padding:8px 10px",
11470 "display:flex",
11471 "flex-direction:column",
11472 "gap:1px",
11473 "min-width:0"
11474 ].join(";");
11475 const lbl = document.createElement("div");
11476 lbl.style.cssText = "font-size:10px;text-transform:uppercase;letter-spacing:0.04em;color:var(--desktop-mode-muted, #50575e);font-weight:600;";
11477 lbl.textContent = label;
11478 const val = document.createElement("div");
11479 val.style.cssText = "font-size:18px;font-weight:600;font-variant-numeric:tabular-nums;";
11480 val.textContent = value;
11481 card.appendChild(lbl);
11482 card.appendChild(val);
11483 if (sub) {
11484 const subEl = document.createElement("div");
11485 subEl.style.cssText = "font-size:10px;color:var(--desktop-mode-muted, #8c8f94);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
11486 subEl.title = sub;
11487 subEl.textContent = sub;
11488 card.appendChild(subEl);
11489 }
11490 return card;
11491 };
11492 const stats = data.stats;
11493 grid.appendChild(
11494 tile(
11495 __("Posts"),
11496 String(stats.posts),
11497 // translators: %d is a count of pages.
11498 stats.pages > 0 ? sprintf(__("+ %d pages"), stats.pages) : void 0
11499 )
11500 );
11501 let commentsSub;
11502 if (stats.commentsReceived > 0) {
11503 commentsSub = sprintf(
11504 // translators: %d is a count of received comments.
11505 __("%d received"),
11506 stats.commentsReceived
11507 );
11508 }
11509 grid.appendChild(
11510 tile(__("Comments"), String(stats.commentsAuthored), commentsSub)
11511 );
11512 grid.appendChild(
11513 tile(
11514 __("Last login"),
11515 stats.lastLoginAt ? relativeTime$1(stats.lastLoginAt) : __("Never"),
11516 stats.lastLoginAt ? new Date(stats.lastLoginAt * 1e3).toLocaleDateString() : void 0
11517 )
11518 );
11519 let memberValue = "—";
11520 if (stats.daysSinceRegistration !== null) {
11521 memberValue = sprintf(
11522 // translators: %d is a number of days.
11523 __("%d days"),
11524 stats.daysSinceRegistration
11525 );
11526 }
11527 grid.appendChild(
11528 tile(
11529 __("Member"),
11530 memberValue,
11531 stats.registeredAt ? new Date(stats.registeredAt * 1e3).toLocaleDateString() : void 0
11532 )
11533 );
11534 return grid;
11535 }
11536 function buildContentSparkline(data) {
11537 const wrap = document.createElement("div");
11538 wrap.style.cssText = [
11539 "border:1px solid var(--desktop-mode-border, #dcdcde)",
11540 "border-radius:10px",
11541 "padding:14px 16px",
11542 "margin:0 0 22px"
11543 ].join(";");
11544 const head = document.createElement("div");
11545 head.style.cssText = "display:flex;justify-content:space-between;align-items:baseline;margin:0 0 8px;";
11546 const title = document.createElement("div");
11547 title.style.cssText = "font-size:13px;font-weight:600;";
11548 title.textContent = __("Posts published — last 12 months");
11549 head.appendChild(title);
11550 const total = data.contentByMonth.reduce((s, m) => s + m.count, 0);
11551 const sub = document.createElement("div");
11552 sub.style.cssText = "font-size:11px;color:var(--desktop-mode-muted, #50575e);";
11553 sub.textContent = sprintf(
11554 // translators: %d is a count of posts.
11555 __("%d total"),
11556 total
11557 );
11558 head.appendChild(sub);
11559 wrap.appendChild(head);
11560 if (data.contentByMonth.length === 0) {
11561 const empty = document.createElement("p");
11562 empty.style.cssText = "margin:0;color:var(--desktop-mode-muted, #50575e);font-size:12px;";
11563 empty.textContent = __("No activity in the last 12 months.");
11564 wrap.appendChild(empty);
11565 return wrap;
11566 }
11567 const max = Math.max(1, ...data.contentByMonth.map((m) => m.count));
11568 const bars = document.createElement("div");
11569 bars.style.cssText = [
11570 "display:grid",
11571 `grid-template-columns:repeat(${data.contentByMonth.length}, 1fr)`,
11572 "gap:4px",
11573 "align-items:end",
11574 "height:60px"
11575 ].join(";");
11576 for (const month of data.contentByMonth) {
11577 const col = document.createElement("div");
11578 col.style.cssText = "display:flex;flex-direction:column;align-items:center;height:100%;justify-content:flex-end;";
11579 const bar = document.createElement("div");
11580 const heightPct = Math.round(month.count / max * 100);
11581 bar.style.cssText = [
11582 "width:100%",
11583 `height:${Math.max(3, heightPct)}%`,
11584 "background:var(--wp-admin-theme-color, #2271b1)",
11585 month.count === 0 ? "opacity:0.18" : "opacity:1",
11586 "border-radius:3px 3px 0 0",
11587 "transition:height 360ms ease"
11588 ].join(";");
11589 bar.title = sprintf(
11590 // translators: %1$s is a YYYY-MM month, %2$d is post count.
11591 __("%1$s — %2$d posts"),
11592 month.month,
11593 month.count
11594 );
11595 col.appendChild(bar);
11596 wrap.appendChild(col);
11597 bars.appendChild(col);
11598 }
11599 wrap.appendChild(bars);
11600 const labels = document.createElement("div");
11601 labels.style.cssText = [
11602 "display:grid",
11603 `grid-template-columns:repeat(${data.contentByMonth.length}, 1fr)`,
11604 "gap:4px",
11605 "margin-top:4px",
11606 "font-size:10px",
11607 "color:var(--desktop-mode-muted, #8c8f94)",
11608 "text-align:center"
11609 ].join(";");
11610 for (const month of data.contentByMonth) {
11611 const span = document.createElement("span");
11612 const parts = month.month.split("-");
11613 span.textContent = parts.length === 2 ? parts[1] : month.month;
11614 labels.appendChild(span);
11615 }
11616 wrap.appendChild(labels);
11617 return wrap;
11618 }
11619 function buildRecentLists(data) {
11620 const wrap = document.createElement("div");
11621 wrap.style.cssText = "display:grid;grid-template-columns:repeat(auto-fit, minmax(280px, 1fr));gap:14px;margin:0 0 22px;";
11622 wrap.appendChild(
11623 buildRecentList(
11624 __("Recent posts"),
11625 __("No recent posts."),
11626 data.recentPosts.map((p) => ({
11627 primary: p.title,
11628 secondary: relativeFromIso(p.dateGmt),
11629 tag: p.status !== "publish" ? p.status : null,
11630 badge: p.commentCount > 0 ? sprintf(
11631 // translators: %d is a count of comments.
11632 __("%d 💬"),
11633 p.commentCount
11634 ) : null
11635 }))
11636 )
11637 );
11638 wrap.appendChild(
11639 buildRecentList(
11640 __("Recent comments"),
11641 __("No recent comments."),
11642 data.recentComments.map((c) => {
11643 const when = relativeFromIso(c.dateGmt);
11644 return {
11645 primary: c.excerpt || __("(empty comment)"),
11646 secondary: c.postTitle ? `${__("on")} "${c.postTitle}" · ${when}` : when,
11647 tag: c.approved ? null : __("pending"),
11648 badge: null
11649 };
11650 })
11651 )
11652 );
11653 return wrap;
11654 }
11655 function buildRecentList(title, emptyText, items) {
11656 const card = document.createElement("div");
11657 card.style.cssText = [
11658 "border:1px solid var(--desktop-mode-border, #dcdcde)",
11659 "border-radius:10px",
11660 "padding:14px 16px",
11661 "min-width:0"
11662 ].join(";");
11663 const head = document.createElement("div");
11664 head.style.cssText = "font-size:13px;font-weight:600;margin:0 0 10px;";
11665 head.textContent = title;
11666 card.appendChild(head);
11667 if (items.length === 0) {
11668 const empty = document.createElement("p");
11669 empty.style.cssText = "margin:0;color:var(--desktop-mode-muted, #50575e);font-size:12px;";
11670 empty.textContent = emptyText;
11671 card.appendChild(empty);
11672 return card;
11673 }
11674 const list = document.createElement("ul");
11675 list.style.cssText = "list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:8px;";
11676 for (const item of items) {
11677 const li = document.createElement("li");
11678 li.style.cssText = "min-width:0;";
11679 const top = document.createElement("div");
11680 top.style.cssText = "display:flex;align-items:baseline;gap:6px;min-width:0;";
11681 const primary = document.createElement("span");
11682 primary.style.cssText = "font-size:13px;line-height:1.35;flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
11683 primary.textContent = item.primary;
11684 primary.title = item.primary;
11685 top.appendChild(primary);
11686 if (item.tag) {
11687 const tag = document.createElement("span");
11688 tag.style.cssText = "font-size:10px;text-transform:uppercase;letter-spacing:0.04em;background:rgba(0,0,0,0.06);padding:1px 6px;border-radius:8px;flex-shrink:0;";
11689 tag.textContent = item.tag;
11690 top.appendChild(tag);
11691 }
11692 if (item.badge) {
11693 const badge = document.createElement("span");
11694 badge.style.cssText = "font-size:11px;color:var(--desktop-mode-muted, #50575e);flex-shrink:0;";
11695 badge.textContent = item.badge;
11696 top.appendChild(badge);
11697 }
11698 li.appendChild(top);
11699 const sub = document.createElement("div");
11700 sub.style.cssText = "font-size:11px;color:var(--desktop-mode-muted, #8c8f94);";
11701 sub.textContent = item.secondary;
11702 li.appendChild(sub);
11703 list.appendChild(li);
11704 }
11705 card.appendChild(list);
11706 return card;
11707 }
11708 function buildSecurityPanel(data) {
11709 const card = document.createElement("div");
11710 card.style.cssText = [
11711 "border:1px solid var(--desktop-mode-border, #dcdcde)",
11712 "border-radius:10px",
11713 "padding:14px 16px"
11714 ].join(";");
11715 const head = document.createElement("div");
11716 head.style.cssText = "font-size:13px;font-weight:600;margin:0 0 10px;";
11717 head.textContent = __("Active sessions & app access");
11718 card.appendChild(head);
11719 const grid = document.createElement("div");
11720 grid.style.cssText = "display:grid;grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));gap:12px;";
11721 const sessionTile = document.createElement("div");
11722 sessionTile.style.cssText = "display:flex;flex-direction:column;gap:2px;font-size:12px;";
11723 const sessionLabel = document.createElement("div");
11724 sessionLabel.style.cssText = "color:var(--desktop-mode-muted, #50575e);font-size:11px;text-transform:uppercase;letter-spacing:0.04em;font-weight:600;";
11725 sessionLabel.textContent = __("Active sessions");
11726 const sessionValue = document.createElement("div");
11727 sessionValue.style.cssText = "font-size:18px;font-weight:600;";
11728 sessionValue.textContent = String(data.sessions.length);
11729 const sessionSub = document.createElement("div");
11730 sessionSub.style.cssText = "color:var(--desktop-mode-muted, #8c8f94);";
11731 const currentCount = data.sessions.filter((s) => s.current).length;
11732 sessionSub.textContent = currentCount > 0 ? __("Includes the current device.") : __("Logged in across multiple devices.");
11733 sessionTile.appendChild(sessionLabel);
11734 sessionTile.appendChild(sessionValue);
11735 sessionTile.appendChild(sessionSub);
11736 grid.appendChild(sessionTile);
11737 const appTile = document.createElement("div");
11738 appTile.style.cssText = "display:flex;flex-direction:column;gap:2px;font-size:12px;";
11739 const appLabel = document.createElement("div");
11740 appLabel.style.cssText = "color:var(--desktop-mode-muted, #50575e);font-size:11px;text-transform:uppercase;letter-spacing:0.04em;font-weight:600;";
11741 appLabel.textContent = __("Application passwords");
11742 const appValue = document.createElement("div");
11743 appValue.style.cssText = "font-size:18px;font-weight:600;";
11744 appValue.textContent = String(data.applicationPasswords.total);
11745 const appSub = document.createElement("div");
11746 appSub.style.cssText = "color:var(--desktop-mode-muted, #8c8f94);";
11747 if (data.applicationPasswords.lastUsedAt && data.applicationPasswords.lastUsedName) {
11748 appSub.textContent = sprintf(
11749 // translators: %1$s is the app password name, %2$s is a relative time.
11750 __('"%1$s" last used %2$s'),
11751 data.applicationPasswords.lastUsedName,
11752 relativeTime$1(data.applicationPasswords.lastUsedAt)
11753 );
11754 } else {
11755 appSub.textContent = data.applicationPasswords.total ? __("No recent use.") : __("No app passwords issued yet.");
11756 }
11757 appTile.appendChild(appLabel);
11758 appTile.appendChild(appValue);
11759 appTile.appendChild(appSub);
11760 grid.appendChild(appTile);
11761 card.appendChild(grid);
11762 return card;
11763 }
11764 function textField(formName, label, value, opts = {}) {
11765 const el = document.createElement("wpd-text-field");
11766 el.setAttribute("name", formName);
11767 el.setAttribute("label", label);
11768 el.setAttribute("value", value);
11769 el.value = value;
11770 if (opts.required) {
11771 el.setAttribute("required", "");
11772 }
11773 if (opts.readonly) {
11774 el.setAttribute("readonly", "");
11775 }
11776 if (opts.type) {
11777 el.setAttribute("type", opts.type);
11778 }
11779 if (opts.fullWidth !== false && opts.fullWidth) {
11780 el.setAttribute("full-width", "");
11781 }
11782 if (opts.dataset) {
11783 for (const [k, v] of Object.entries(opts.dataset)) {
11784 el.dataset[k] = v;
11785 }
11786 }
11787 return el;
11788 }
11789 function displayNameCandidates(user) {
11790 const candidates = /* @__PURE__ */ new Set();
11791 const add = (s) => {
11792 const t = s.trim();
11793 if (t !== "") {
11794 candidates.add(t);
11795 }
11796 };
11797 add(user.username);
11798 add(user.nickname ?? "");
11799 add(user.first_name);
11800 add(user.last_name);
11801 if (user.first_name || user.last_name) {
11802 add(`${user.first_name} ${user.last_name}`.trim());
11803 add(`${user.last_name} ${user.first_name}`.trim());
11804 }
11805 if (user.name) {
11806 add(user.name);
11807 }
11808 return Array.from(candidates).map((name) => ({
11809 value: name,
11810 label: name
11811 }));
11812 }
11813 function relativeFromIso(iso) {
11814 const ms = msFromIso(iso);
11815 if (!Number.isFinite(ms)) {
11816 return "—";
11817 }
11818 return relativeTime$1(Math.floor(ms / 1e3));
11819 }
11820 function relativeTime$1(ts) {
11821 if (!Number.isFinite(ts)) {
11822 return "—";
11823 }
11824 const now = Math.floor(Date.now() / 1e3);
11825 const delta = now - ts;
11826 if (delta < 60) {
11827 return __("just now");
11828 }
11829 if (delta < 3600) {
11830 return sprintf(__("%d min ago"), Math.floor(delta / 60));
11831 }
11832 if (delta < 86400) {
11833 return sprintf(__("%d h ago"), Math.floor(delta / 3600));
11834 }
11835 if (delta < 86400 * 30) {
11836 return sprintf(__("%d d ago"), Math.floor(delta / 86400));
11837 }
11838 if (delta < 86400 * 365) {
11839 return sprintf(__("%d mo ago"), Math.floor(delta / (86400 * 30)));
11840 }
11841 return sprintf(__("%d y ago"), Math.floor(delta / (86400 * 365)));
11842 }
11843 function msFromIso(iso) {
11844 if (!iso) {
11845 return NaN;
11846 }
11847 if (iso.startsWith("0000-00-00")) {
11848 return NaN;
11849 }
11850 let normalized = iso;
11851 if (normalized.includes(" ")) {
11852 normalized = normalized.replace(" ", "T");
11853 }
11854 if (!/Z$/.test(normalized) && !/[+-]\d{2}:?\d{2}$/.test(normalized)) {
11855 normalized += "Z";
11856 }
11857 const parsed = Date.parse(normalized);
11858 return Number.isFinite(parsed) ? parsed : NaN;
11859 }
11860 function generateStrongPassword$1(length) {
11861 const upper = "ABCDEFGHJKLMNPQRSTUVWXYZ";
11862 const lower = "abcdefghjkmnpqrstuvwxyz";
11863 const digits = "23456789";
11864 const symbols = "!@#$%^&*-_=+";
11865 const all = upper + lower + digits + symbols;
11866 const buf = new Uint32Array(length);
11867 crypto.getRandomValues(buf);
11868 let out = "";
11869 for (let i = 0; i < length; i += 1) {
11870 out += all[buf[i] % all.length];
11871 }
11872 return out;
11873 }
11874 function mapErrorCode(code) {
11875 switch (code) {
11876 case "rest_user_invalid_email":
11877 case "invalid_email":
11878 return __("Email address is not valid.");
11879 case "rest_user_email_exists":
11880 case "existing_user_email":
11881 return __("That email is already in use.");
11882 case "rest_user_invalid_role":
11883 return __("You are not allowed to assign that role.");
11884 default:
11885 return null;
11886 }
11887 }
11888 function applyColorSchemePreview(slug, info) {
11889 if (!info.url) {
11890 flipBodyClass(slug);
11891 flipShellScheme(slug);
11892 return;
11893 }
11894 let link = document.getElementById(
11895 "colors-css"
11896 );
11897 if (!link) {
11898 link = document.createElement("link");
11899 link.rel = "stylesheet";
11900 link.id = "colors-css";
11901 document.head.appendChild(link);
11902 }
11903 link.href = info.url;
11904 flipBodyClass(slug);
11905 flipShellScheme(slug);
11906 }
11907 function flipShellScheme(slug) {
11908 const shell = document.querySelector(".desktop-mode-shell");
11909 if (shell) {
11910 shell.setAttribute("data-desktop-mode-scheme", slug);
11911 }
11912 }
11913 function flipBodyClass(slug) {
11914 const body = document.body;
11915 const next = `admin-color-${slug}`;
11916 for (const cls of Array.from(body.classList)) {
11917 if (cls.startsWith("admin-color-") && cls !== next) {
11918 body.classList.remove(cls);
11919 }
11920 }
11921 body.classList.add(next);
11922 }
11923 function buildAdminColorPicker(schemes, current, opts = {}) {
11924 const wrap = document.createElement("div");
11925 wrap.setAttribute("full-width", "");
11926 wrap.style.cssText = "display:flex;flex-direction:column;gap:6px;";
11927 const label = document.createElement("span");
11928 label.style.cssText = "font-size:11px;text-transform:uppercase;letter-spacing:0.04em;color:var(--desktop-mode-muted, #50575e);font-weight:600;";
11929 label.textContent = __("Admin colour scheme");
11930 wrap.appendChild(label);
11931 const hidden = document.createElement("wpd-text-field");
11932 hidden.setAttribute("name", "meta.admin_color");
11933 hidden.setAttribute("value", current);
11934 hidden.value = current;
11935 hidden.style.display = "none";
11936 wrap.appendChild(hidden);
11937 const grid = document.createElement("div");
11938 grid.style.cssText = [
11939 "display:grid",
11940 "grid-template-columns:repeat(auto-fill, minmax(140px, 1fr))",
11941 "gap:8px"
11942 ].join(";");
11943 wrap.appendChild(grid);
11944 let selected = current;
11945 const updateSelected = (slug) => {
11946 selected = slug;
11947 hidden.value = slug;
11948 hidden.setAttribute("value", slug);
11949 for (const t of Array.from(grid.children)) {
11950 const tile = t;
11951 const v = tile.dataset.scheme;
11952 tile.style.borderColor = v === slug ? "var(--wp-admin-theme-color, #2271b1)" : "var(--desktop-mode-border, #dcdcde)";
11953 tile.style.boxShadow = v === slug ? "0 0 0 1px var(--wp-admin-theme-color, #2271b1) inset" : "none";
11954 tile.setAttribute("aria-checked", v === slug ? "true" : "false");
11955 }
11956 };
11957 for (const [slug, info] of Object.entries(schemes)) {
11958 const tile = document.createElement("button");
11959 tile.type = "button";
11960 tile.setAttribute("role", "radio");
11961 tile.setAttribute("aria-checked", slug === selected ? "true" : "false");
11962 tile.dataset.scheme = slug;
11963 tile.style.cssText = [
11964 "appearance:none",
11965 "border:1px solid var(--desktop-mode-border, #dcdcde)",
11966 "background:var(--wp-admin-theme-bg, #fff)",
11967 "color:inherit",
11968 "border-radius:8px",
11969 "padding:10px 10px 8px",
11970 "cursor:pointer",
11971 "display:flex",
11972 "flex-direction:column",
11973 "gap:6px",
11974 "text-align:left",
11975 "min-width:0",
11976 "transition:border-color 120ms ease, box-shadow 120ms ease"
11977 ].join(";");
11978 const swatchRow = document.createElement("span");
11979 swatchRow.style.cssText = "display:flex;height:18px;border-radius:4px;overflow:hidden;border:1px solid rgba(0,0,0,0.06);";
11980 const colors = (info.colors ?? []).slice(0, 4);
11981 if (colors.length === 0) {
11982 colors.push("#dcdcde", "#dcdcde", "#dcdcde");
11983 }
11984 for (const color of colors) {
11985 const swatch = document.createElement("span");
11986 swatch.style.cssText = `flex:1 1 auto;background:${color};`;
11987 swatchRow.appendChild(swatch);
11988 }
11989 tile.appendChild(swatchRow);
11990 const name = document.createElement("span");
11991 name.style.cssText = "font-size:12px;font-weight:500;";
11992 name.textContent = info.name;
11993 tile.appendChild(name);
11994 tile.addEventListener("click", () => {
11995 updateSelected(slug);
11996 if (opts.livePreview) {
11997 applyColorSchemePreview(slug, info);
11998 }
11999 });
12000 grid.appendChild(tile);
12001 }
12002 updateSelected(selected);
12003 return wrap;
12004 }
12005 function checkboxField(name, label, checked, opts = {}) {
12006 const trueValue = opts.trueValue ?? "true";
12007 const falseValue = opts.falseValue ?? "false";
12008 const wrap = document.createElement("span");
12009 if (opts.fullWidth) {
12010 wrap.setAttribute("full-width", "");
12011 }
12012 const cb = document.createElement("wpd-checkbox-label");
12013 cb.setAttribute("label", label);
12014 cb.setAttribute("name", name);
12015 cb.setAttribute("value", checked ? trueValue : falseValue);
12016 cb.value = checked ? trueValue : falseValue;
12017 if (checked) {
12018 cb.setAttribute("checked", "");
12019 }
12020 cb.addEventListener("wpd-checkbox-change", (e) => {
12021 const detail = e.detail;
12022 const v = detail?.checked ? trueValue : falseValue;
12023 cb.value = v;
12024 cb.setAttribute("value", v);
12025 });
12026 wrap.appendChild(cb);
12027 return wrap;
12028 }
12029 function buildSessionsRow(userId, isSelfEdit) {
12030 const wrap = document.createElement("div");
12031 wrap.setAttribute("full-width", "");
12032 wrap.style.cssText = "display:flex;align-items:center;gap:12px;flex-wrap:wrap;";
12033 const label = document.createElement("span");
12034 label.style.cssText = "font-size:13px;color:var(--desktop-mode-fg, inherit);";
12035 label.textContent = __("Active sessions");
12036 wrap.appendChild(label);
12037 const btn = document.createElement("wpd-button");
12038 btn.setAttribute("variant", "ghost");
12039 btn.setAttribute("type", "button");
12040 btn.textContent = isSelfEdit ? __("Log out everywhere else") : __("Log this user out everywhere");
12041 btn.addEventListener("click", async (e) => {
12042 e.preventDefault();
12043 try {
12044 const cfg = resolveUserEditClient().getConfig();
12045 const base = cfg.insightsUrlBase ?? joinRestUrl(cfg.restRoot, "desktop-mode/v1/users/");
12046 const res = await trackedFetch(
12047 joinRestUrl(base, `${userId}/destroy-sessions`),
12048 {
12049 method: "POST",
12050 credentials: "same-origin",
12051 headers: {
12052 "Content-Type": "application/json",
12053 "X-WP-Nonce": cfg.restNonce
12054 },
12055 body: JSON.stringify({
12056 scope: isSelfEdit ? "others" : "all"
12057 })
12058 },
12059 { source: "user-edit-window/destroy-sessions" }
12060 );
12061 if (!res.ok) {
12062 throw new Error(`http_${res.status}`);
12063 }
12064 notifyToast$1(__("Sessions destroyed."), "success");
12065 } catch (err) {
12066 notifyToast$1(
12067 sprintf(
12068 // translators: %s is an error message.
12069 __("Could not destroy sessions (%s)."),
12070 String(err.message ?? err)
12071 ),
12072 "error"
12073 );
12074 }
12075 });
12076 wrap.appendChild(btn);
12077 return wrap;
12078 }
12079 function buildAppPasswordsRow(userId) {
12080 const wrap = document.createElement("div");
12081 wrap.setAttribute("full-width", "");
12082 wrap.style.cssText = "display:flex;flex-direction:column;gap:8px;border:1px solid var(--desktop-mode-border, #dcdcde);border-radius:8px;padding:12px 14px;";
12083 const heading = document.createElement("div");
12084 heading.style.cssText = "display:flex;align-items:center;justify-content:space-between;gap:8px;";
12085 const headLabel = document.createElement("span");
12086 headLabel.textContent = __("Application passwords");
12087 headLabel.style.cssText = "font-size:13px;font-weight:600;";
12088 heading.appendChild(headLabel);
12089 wrap.appendChild(heading);
12090 const cfg = resolveUserEditClient().getConfig();
12091 const base = cfg.insightsUrlBase ?? joinRestUrl(cfg.restRoot, "desktop-mode/v1/users/");
12092 const list = document.createElement("ul");
12093 list.style.cssText = "list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px;";
12094 wrap.appendChild(list);
12095 const createRow = document.createElement("div");
12096 createRow.style.cssText = "display:flex;gap:8px;align-items:flex-end;flex-wrap:wrap;margin-top:6px;";
12097 const nameInput = document.createElement("wpd-text-field");
12098 nameInput.setAttribute("label", __("New application password name"));
12099 nameInput.setAttribute(
12100 "placeholder",
12101 __("e.g. iPhone, WP-CLI, Backup tool")
12102 );
12103 nameInput.style.flex = "1 1 220px";
12104 createRow.appendChild(nameInput);
12105 const createBtn = document.createElement("wpd-button");
12106 createBtn.setAttribute("variant", "primary");
12107 createBtn.setAttribute("type", "button");
12108 createBtn.textContent = __("Create");
12109 createRow.appendChild(createBtn);
12110 wrap.appendChild(createRow);
12111 const renderItems = (items) => {
12112 list.replaceChildren();
12113 if (items.length === 0) {
12114 const empty = document.createElement("li");
12115 empty.style.cssText = "font-size:12px;color:var(--desktop-mode-muted, #50575e);";
12116 empty.textContent = __("No application passwords issued yet.");
12117 list.appendChild(empty);
12118 return;
12119 }
12120 for (const item of items) {
12121 const row = document.createElement("li");
12122 row.style.cssText = "display:flex;align-items:center;gap:8px;font-size:12px;";
12123 const nameSpan = document.createElement("span");
12124 nameSpan.style.cssText = "flex:1 1 auto;font-weight:500;";
12125 nameSpan.textContent = item.name;
12126 row.appendChild(nameSpan);
12127 const meta = document.createElement("span");
12128 meta.style.cssText = "color:var(--desktop-mode-muted, #8c8f94);";
12129 meta.textContent = item.last_used ? sprintf(
12130 // translators: %s is a relative time.
12131 __("last used %s"),
12132 relativeTime$1(item.last_used)
12133 ) : __("never used");
12134 row.appendChild(meta);
12135 const revoke = document.createElement("wpd-button");
12136 revoke.setAttribute("variant", "ghost");
12137 revoke.setAttribute("type", "button");
12138 revoke.textContent = __("Revoke");
12139 revoke.addEventListener("click", async (e) => {
12140 e.preventDefault();
12141 try {
12142 const res = await trackedFetch(
12143 joinRestUrl(base, `${userId}/application-passwords/${item.uuid}`),
12144 {
12145 method: "DELETE",
12146 credentials: "same-origin",
12147 headers: { "X-WP-Nonce": cfg.restNonce }
12148 },
12149 { source: "user-edit-window/app-pw-revoke" }
12150 );
12151 if (!res.ok) {
12152 throw new Error(`http_${res.status}`);
12153 }
12154 row.remove();
12155 notifyToast$1(__("Application password revoked."), "success");
12156 } catch (err) {
12157 notifyToast$1(
12158 String(err.message ?? err),
12159 "error"
12160 );
12161 }
12162 });
12163 row.appendChild(revoke);
12164 list.appendChild(row);
12165 }
12166 };
12167 const refresh = async () => {
12168 try {
12169 const res = await trackedFetch(
12170 joinRestUrl(base, `${userId}/application-passwords`),
12171 {
12172 credentials: "same-origin",
12173 headers: { "X-WP-Nonce": cfg.restNonce }
12174 },
12175 { source: "user-edit-window/app-pw-list", silent: true }
12176 );
12177 if (!res.ok) {
12178 return;
12179 }
12180 const data = await res.json();
12181 renderItems(data.items ?? []);
12182 } catch {
12183 }
12184 };
12185 void refresh();
12186 createBtn.addEventListener("click", async (e) => {
12187 e.preventDefault();
12188 const name = String(nameInput.value ?? "").trim();
12189 if (!name) {
12190 notifyToast$1(__("Application password name is required."), "error");
12191 return;
12192 }
12193 try {
12194 const res = await trackedFetch(
12195 joinRestUrl(base, `${userId}/application-passwords`),
12196 {
12197 method: "POST",
12198 credentials: "same-origin",
12199 headers: {
12200 "Content-Type": "application/json",
12201 "X-WP-Nonce": cfg.restNonce
12202 },
12203 body: JSON.stringify({ name })
12204 },
12205 { source: "user-edit-window/app-pw-create" }
12206 );
12207 if (!res.ok) {
12208 throw new Error(`http_${res.status}`);
12209 }
12210 const data = await res.json();
12211 notifyToast$1(
12212 sprintf(
12213 // translators: %s is an application password.
12214 __("Created. Copy the password now: %s"),
12215 data.password
12216 ),
12217 "success"
12218 );
12219 void navigator.clipboard?.writeText(data.password).catch(() => {
12220 });
12221 nameInput.value = "";
12222 nameInput.setAttribute("value", "");
12223 void refresh();
12224 } catch (err) {
12225 notifyToast$1(
12226 String(err.message ?? err),
12227 "error"
12228 );
12229 }
12230 });
12231 return wrap;
12232 }
12233 const userEditRender = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
12234 __proto__: null,
12235 mountProfileActivityAt,
12236 mountProfileAsideAt,
12237 mountProfileFormAt
12238 }, Symbol.toStringTag, { value: "Module" }));
12239 async function showPagesIntroDialog() {
12240 return new Promise((resolve) => {
12241 const backdrop = document.createElement("div");
12242 backdrop.className = "desktop-mode-pages-intro__backdrop";
12243 backdrop.setAttribute("role", "presentation");
12244 Object.assign(backdrop.style, {
12245 position: "fixed",
12246 inset: "0",
12247 background: "color-mix(in srgb, var(--wp-admin-theme-color, #1d2327) 60%, transparent)",
12248 backdropFilter: "blur(2px)",
12249 zIndex: "100000",
12250 display: "flex",
12251 alignItems: "center",
12252 justifyContent: "center",
12253 padding: "24px"
12254 });
12255 const dialog = document.createElement("div");
12256 dialog.setAttribute("role", "dialog");
12257 dialog.setAttribute("aria-modal", "true");
12258 dialog.setAttribute("aria-labelledby", "desktop-mode-pages-intro-title");
12259 dialog.className = "desktop-mode-pages-intro";
12260 Object.assign(dialog.style, {
12261 background: "var(--wp-admin-theme-bg, #fff)",
12262 color: "var(--wp-admin-theme-fg, #1d2327)",
12263 borderRadius: "14px",
12264 boxShadow: "0 24px 60px rgba(0,0,0,.28)",
12265 maxWidth: "520px",
12266 width: "100%",
12267 maxHeight: "90vh",
12268 overflow: "auto",
12269 padding: "28px 32px 24px",
12270 fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
12271 });
12272 dialog.innerHTML = renderDialogMarkup$1();
12273 backdrop.appendChild(dialog);
12274 document.body.appendChild(backdrop);
12275 const primaryBtn = dialog.querySelector(
12276 '[data-action="confirm"]'
12277 );
12278 const settingsBtn = dialog.querySelector(
12279 '[data-action="settings"]'
12280 );
12281 primaryBtn?.focus();
12282 let resolved = false;
12283 const cleanup = (result) => {
12284 if (resolved) {
12285 return;
12286 }
12287 resolved = true;
12288 document.removeEventListener("keydown", onKey, true);
12289 backdrop.remove();
12290 resolve(result);
12291 };
12292 const onKey = (e) => {
12293 if (e.key === "Escape") {
12294 e.preventDefault();
12295 cleanup("cancel");
12296 }
12297 };
12298 document.addEventListener("keydown", onKey, true);
12299 backdrop.addEventListener("click", (e) => {
12300 if (e.target === backdrop) {
12301 cleanup("cancel");
12302 }
12303 });
12304 primaryBtn?.addEventListener("click", () => cleanup("confirm"));
12305 settingsBtn?.addEventListener("click", () => cleanup("settings"));
12306 });
12307 }
12308 function renderDialogMarkup$1() {
12309 const title = __("Welcome to the new Pages window");
12310 const lede = __(
12311 "You're looking at the redesigned Pages list — same data you already manage, with a UX tuned for how Desktop Mode wants you to work."
12312 );
12313 const highlights = [
12314 __("Sticky header and sticky title column so long lists stay readable as you scroll."),
12315 __('Front page and Posts page badges right on the title — no more "wait, which one is the homepage?".'),
12316 __("Page Template column so you can spot which template each page uses at a glance."),
12317 __("Slug column with one-click copy — perfect when configuring redirects or sharing canonical URLs."),
12318 __("Comments column, Parent column, View link, lock indicator, multi-select bulk actions, inline search, status segments. All in one screen, no reloads.")
12319 ];
12320 const li = (arr) => arr.map(
12321 (s) => `<li><span class="dot" aria-hidden="true"></span>${escapeHtml$1(s)}</li>`
12322 ).join("");
12323 return `
12324 <style>
12325 .desktop-mode-pages-intro h2 {
12326 margin: 0 0 8px;
12327 font-size: 22px;
12328 font-weight: 600;
12329 letter-spacing: -0.01em;
12330 }
12331 .desktop-mode-pages-intro p.lede {
12332 margin: 0 0 20px;
12333 color: var(--wp-admin-theme-fg-muted, #50575e);
12334 font-size: 14px;
12335 line-height: 1.5;
12336 }
12337 .desktop-mode-pages-intro__list {
12338 list-style: none;
12339 margin: 0 0 22px;
12340 padding: 0;
12341 font-size: 14px;
12342 line-height: 1.5;
12343 }
12344 .desktop-mode-pages-intro__list li {
12345 display: flex;
12346 align-items: flex-start;
12347 gap: 10px;
12348 padding: 6px 0;
12349 }
12350 .desktop-mode-pages-intro__list .dot {
12351 flex: 0 0 auto;
12352 width: 6px;
12353 height: 6px;
12354 margin-top: 9px;
12355 border-radius: 50%;
12356 background: var(--wp-admin-theme-color, #2271b1);
12357 }
12358 .desktop-mode-pages-intro__footer {
12359 display: flex;
12360 justify-content: flex-end;
12361 gap: 8px;
12362 margin-top: 8px;
12363 }
12364 .desktop-mode-pages-intro__footer button {
12365 appearance: none;
12366 border: 1px solid var(--wp-admin-theme-border, #dcdcde);
12367 background: var(--wp-admin-theme-bg, #fff);
12368 color: inherit;
12369 padding: 8px 14px;
12370 border-radius: 6px;
12371 font-size: 13px;
12372 cursor: pointer;
12373 }
12374 .desktop-mode-pages-intro__footer button.primary {
12375 border-color: var(--wp-admin-theme-color, #2271b1);
12376 background: var(--wp-admin-theme-color, #2271b1);
12377 color: #fff;
12378 font-weight: 500;
12379 }
12380 .desktop-mode-pages-intro__footer button:hover { filter: brightness(1.05); }
12381 .desktop-mode-pages-intro__footer button:focus-visible {
12382 outline: 2px solid var(--wp-admin-theme-color, #2271b1);
12383 outline-offset: 2px;
12384 }
12385 </style>
12386 <h2 id="desktop-mode-pages-intro-title">${escapeHtml$1(title)}</h2>
12387 <p class="lede">${escapeHtml$1(lede)}</p>
12388 <ul class="desktop-mode-pages-intro__list">${li(highlights)}</ul>
12389 <div class="desktop-mode-pages-intro__footer">
12390 <button type="button" data-action="settings">${escapeHtml$1(
12391 __("Take me to settings")
12392 )}</button>
12393 <button type="button" class="primary" data-action="confirm">${escapeHtml$1(
12394 __("Got it")
12395 )}</button>
12396 </div>
12397 `;
12398 }
12399 function escapeHtml$1(s) {
12400 const t = document.createElement("div");
12401 t.textContent = s;
12402 return t.innerHTML;
12403 }
12404 const pagesIntroDialog = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
12405 __proto__: null,
12406 showPagesIntroDialog
12407 }, Symbol.toStringTag, { value: "Module" }));
12408 const REPULSION_K = 5500;
12409 const SPRING_K = 0.05;
12410 const SPRING_LEN = 130;
12411 const MIN_RADIUS = 22;
12412 const MAX_RADIUS = 48;
12413 const POST_PER_PAGE$1 = 10;
12414 const POST_RING_RADIUS$1 = 170;
12415 async function mountCategoriesMindmap(host, client) {
12416 const api = window.wp?.desktop;
12417 if (!api || typeof api.loadModules !== "function") {
12418 host.textContent = __("Mindmap unavailable: shell modules API missing.");
12419 return () => {
12420 };
12421 }
12422 try {
12423 await api.loadModules(["pixijs"]);
12424 } catch {
12425 host.textContent = __("Mindmap unavailable.");
12426 return () => {
12427 };
12428 }
12429 const pixiMaybe = window.PIXI;
12430 if (!pixiMaybe) {
12431 host.textContent = __("Mindmap unavailable.");
12432 return () => {
12433 };
12434 }
12435 const pixi = pixiMaybe;
12436 host.replaceChildren();
12437 host.classList.add("wpd-mindmap");
12438 const toolbar = document.createElement("div");
12439 toolbar.className = "wpd-mindmap__toolbar";
12440 const addRootBtn = document.createElement("button");
12441 addRootBtn.type = "button";
12442 addRootBtn.className = "wpd-mindmap__btn wpd-mindmap__btn--primary";
12443 addRootBtn.innerHTML = '<span class="dashicons dashicons-plus" aria-hidden="true"></span>' + __("Add root category");
12444 const recenterBtn = document.createElement("button");
12445 recenterBtn.type = "button";
12446 recenterBtn.className = "wpd-mindmap__btn";
12447 recenterBtn.innerHTML = '<span class="dashicons dashicons-image-rotate" aria-hidden="true"></span>' + __("Recenter");
12448 const searchWrap = document.createElement("div");
12449 searchWrap.className = "wpd-mindmap__search";
12450 const searchInput = document.createElement("input");
12451 searchInput.type = "search";
12452 searchInput.className = "wpd-mindmap__search-input";
12453 searchInput.placeholder = __("Search categories…");
12454 searchInput.setAttribute(
12455 "aria-label",
12456 __("Search categories in the mindmap")
12457 );
12458 searchWrap.appendChild(searchInput);
12459 const searchResults = document.createElement("ul");
12460 searchResults.className = "wpd-mindmap__search-results";
12461 searchResults.hidden = true;
12462 searchWrap.appendChild(searchResults);
12463 const hint = document.createElement("span");
12464 hint.className = "wpd-mindmap__hint";
12465 hint.textContent = __(
12466 "Click a node to focus + edit · drag onto another to reparent · wheel to zoom"
12467 );
12468 toolbar.appendChild(addRootBtn);
12469 toolbar.appendChild(recenterBtn);
12470 toolbar.appendChild(searchWrap);
12471 toolbar.appendChild(hint);
12472 host.appendChild(toolbar);
12473 const layout = document.createElement("div");
12474 layout.className = "wpd-mindmap__layout";
12475 host.appendChild(layout);
12476 const stage = document.createElement("div");
12477 stage.className = "wpd-mindmap__stage";
12478 stage.classList.add("is-loading");
12479 layout.appendChild(stage);
12480 const sidebar = document.createElement("aside");
12481 sidebar.className = "wpd-mindmap__sidebar";
12482 layout.appendChild(sidebar);
12483 const app = new pixi.Application();
12484 await app.init({
12485 resizeTo: stage,
12486 backgroundAlpha: 0,
12487 antialias: true,
12488 autoDensity: true,
12489 resolution: Math.min(window.devicePixelRatio || 1, 2)
12490 });
12491 stage.appendChild(app.canvas);
12492 app.canvas.classList.add("wpd-mindmap__canvas");
12493 const world = new pixi.Container();
12494 world.x = stage.clientWidth / 2;
12495 world.y = stage.clientHeight / 2;
12496 app.stage.addChild(world);
12497 const edgeLayer = new pixi.Container();
12498 const nodeLayer = new pixi.Container();
12499 const postEdgeLayer = new pixi.Container();
12500 const postLayer = new pixi.Container();
12501 const chipLayer = new pixi.Container();
12502 const postChipLayer = new pixi.Container();
12503 world.addChild(edgeLayer);
12504 world.addChild(postEdgeLayer);
12505 world.addChild(postLayer);
12506 world.addChild(nodeLayer);
12507 world.addChild(chipLayer);
12508 world.addChild(postChipLayer);
12509 const edgeGfx = new pixi.Graphics();
12510 edgeLayer.addChild(edgeGfx);
12511 const postEdgeGfx = new pixi.Graphics();
12512 postEdgeLayer.addChild(postEdgeGfx);
12513 const CHIP_TEXT_RES2 = 4;
12514 const pager = new pixi.Container();
12515 pager.eventMode = "passive";
12516 pager.visible = false;
12517 postLayer.addChild(pager);
12518 const pagerPrev = new pixi.Graphics();
12519 const pagerNext = new pixi.Graphics();
12520 const pagerLabel = new pixi.Text({
12521 text: "1 / 1",
12522 style: {
12523 fill: 5265246,
12524 fontSize: 14,
12525 fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
12526 fontWeight: "600"
12527 },
12528 resolution: CHIP_TEXT_RES2
12529 });
12530 pagerLabel.anchor.set(0.5);
12531 pagerPrev.eventMode = "static";
12532 pagerPrev.cursor = "pointer";
12533 pagerNext.eventMode = "static";
12534 pagerNext.cursor = "pointer";
12535 pagerPrev.hitArea = new pixi.Circle(0, 0, 16);
12536 pagerNext.hitArea = new pixi.Circle(0, 0, 16);
12537 pager.addChild(pagerPrev);
12538 pager.addChild(pagerLabel);
12539 pager.addChild(pagerNext);
12540 const stopBubble = (e) => {
12541 e.stopPropagation?.();
12542 pixiInteractionAt = performance.now();
12543 };
12544 pagerPrev.on("pointerdown", stopBubble);
12545 pagerNext.on("pointerdown", stopBubble);
12546 pagerPrev.on("pointertap", (e) => {
12547 stopBubble(e);
12548 lastFocusChange = performance.now();
12549 if (focusPage <= 1) {
12550 return;
12551 }
12552 focusPage--;
12553 void loadPostsForFocus();
12554 });
12555 pagerNext.on("pointertap", (e) => {
12556 stopBubble(e);
12557 lastFocusChange = performance.now();
12558 if (focusPage >= focusTotalPages) {
12559 return;
12560 }
12561 focusPage++;
12562 void loadPostsForFocus();
12563 });
12564 const nodes = /* @__PURE__ */ new Map();
12565 const chips = /* @__PURE__ */ new Map();
12566 const postChips = /* @__PURE__ */ new Map();
12567 const postNodes = /* @__PURE__ */ new Map();
12568 let focusId = null;
12569 let focusPage = 1;
12570 let focusTotalPages = 1;
12571 let loadSeq = 0;
12572 let pixiInteractionAt = 0;
12573 let dragNode = null;
12574 let dragHover = null;
12575 let panActive = false;
12576 let panStart = null;
12577 let panMovedDist = 0;
12578 let raf = null;
12579 let lastTick = performance.now();
12580 let targetScale = world.scale.x;
12581 let targetWorldX = world.x;
12582 let targetWorldY = world.y;
12583 let nudgeAwayFrom = null;
12584 const pinnedTargetBackup = /* @__PURE__ */ new Map();
12585 let prevView = null;
12586 let draft = null;
12587 const themeHue = readAdminThemeHue$1();
12588 const clusterColor = (idx) => hslToInt$1((themeHue + idx * 47) % 360, 55, 52);
12589 let terms = [];
12590 try {
12591 const all = [];
12592 let page = 1;
12593 while (page <= 5) {
12594 const res = await client.fetchTerms("categories", { page, perPage: 100 });
12595 all.push(...res.items);
12596 if (page >= res.totalPages) {
12597 break;
12598 }
12599 page++;
12600 }
12601 terms = all;
12602 } catch (err) {
12603 showToast$1(__("Couldn’t load categories:"), err);
12604 }
12605 const showError = (title, err) => showToast$1(title, err);
12606 function isUncategorized(term) {
12607 if (term.isDefault) {
12608 return true;
12609 }
12610 return term.id === 1 || term.slug === "uncategorized" || term.name.toLowerCase() === "uncategorized";
12611 }
12612 function syncEmptyHint() {
12613 const existing = stage.querySelector(".wpd-mindmap__empty");
12614 if (terms.length <= 1) {
12615 if (!existing) {
12616 const empty = document.createElement("div");
12617 empty.className = "wpd-mindmap__empty";
12618 empty.textContent = __(
12619 'No custom categories yet. Click "Add root category" to start branching.'
12620 );
12621 stage.appendChild(empty);
12622 }
12623 } else if (existing) {
12624 existing.remove();
12625 }
12626 }
12627 function buildTree() {
12628 const childMap = /* @__PURE__ */ new Map();
12629 for (const t of terms) {
12630 const list = childMap.get(t.parent) ?? [];
12631 list.push(t);
12632 childMap.set(t.parent, list);
12633 }
12634 const allRoots = childMap.get(0) ?? [];
12635 const roots = allRoots.filter((r) => !isUncategorized(r));
12636 const uncategorized = allRoots.find(isUncategorized);
12637 const place = (term, depth, rootIdx, angle, angleSpan) => {
12638 const rootRingByCount = roots.length > 1 ? 110 + roots.length * 28 : 0;
12639 const rootRing = uncategorized ? Math.max(rootRingByCount, 140) : rootRingByCount;
12640 const baseRadius = depth === 0 ? rootRing : rootRing + 160 + (depth - 1) * 150;
12641 const tx = baseRadius * Math.cos(angle);
12642 const ty = baseRadius * Math.sin(angle);
12643 const radius = nodeRadius(term.count, terms);
12644 const color = depth === 0 ? clusterColor(rootIdx) : nodes.get(term.parent)?.color ?? clusterColor(rootIdx);
12645 let node = nodes.get(term.id);
12646 if (!node) {
12647 const gfx = new pixi.Graphics();
12648 gfx.eventMode = "static";
12649 gfx.cursor = "pointer";
12650 node = {
12651 id: term.id,
12652 parent: term.parent,
12653 name: term.name,
12654 description: term.description,
12655 count: term.count,
12656 x: tx,
12657 y: ty,
12658 tx,
12659 ty,
12660 radius,
12661 depth,
12662 color,
12663 gfx,
12664 pinned: depth === 0
12665 };
12666 nodeLayer.addChild(gfx);
12667 gfx.on("pointerdown", (e) => onNodePointerDown(e, node));
12668 nodes.set(term.id, node);
12669 } else {
12670 node.parent = term.parent;
12671 node.name = term.name;
12672 node.description = term.description;
12673 node.count = term.count;
12674 node.depth = depth;
12675 node.color = color;
12676 node.radius = radius;
12677 node.tx = tx;
12678 node.ty = ty;
12679 node.pinned = depth === 0;
12680 }
12681 drawNodeDisc(node, false);
12682 const kids = childMap.get(term.id) ?? [];
12683 if (kids.length > 0) {
12684 const sub = angleSpan / kids.length;
12685 kids.forEach((child, i) => {
12686 place(
12687 child,
12688 depth + 1,
12689 rootIdx,
12690 angle - angleSpan / 2 + sub * (i + 0.5),
12691 sub * 0.85
12692 );
12693 });
12694 }
12695 };
12696 const liveIds = new Set(terms.map((t) => t.id));
12697 for (const [id, node] of nodes) {
12698 if (!liveIds.has(id)) {
12699 nodeLayer.removeChild(node.gfx);
12700 node.gfx.destroy();
12701 nodes.delete(id);
12702 destroyChip(id);
12703 }
12704 }
12705 const rootCount = Math.max(1, roots.length);
12706 roots.forEach((root, idx) => {
12707 const angle = 2 * Math.PI / rootCount * idx;
12708 place(root, 0, idx, angle, 2 * Math.PI / rootCount);
12709 });
12710 if (uncategorized) {
12711 placeIsolated(uncategorized);
12712 }
12713 syncEmptyHint();
12714 }
12715 function placeIsolated(term) {
12716 const tx = 0;
12717 const ty = 0;
12718 const radius = nodeRadius(term.count, terms);
12719 const color = 9211796;
12720 let node = nodes.get(term.id);
12721 if (!node) {
12722 const gfx = new pixi.Graphics();
12723 gfx.eventMode = "static";
12724 gfx.cursor = "pointer";
12725 node = {
12726 id: term.id,
12727 parent: 0,
12728 name: term.name,
12729 description: term.description,
12730 count: term.count,
12731 x: tx,
12732 y: ty,
12733 tx,
12734 ty,
12735 radius,
12736 depth: 0,
12737 color,
12738 gfx,
12739 pinned: true
12740 };
12741 nodeLayer.addChild(gfx);
12742 gfx.on("pointerdown", (e) => onNodePointerDown(e, node));
12743 nodes.set(term.id, node);
12744 } else {
12745 node.parent = 0;
12746 node.name = term.name;
12747 node.description = term.description;
12748 node.count = term.count;
12749 node.depth = 0;
12750 node.color = color;
12751 node.radius = radius;
12752 node.tx = tx;
12753 node.ty = ty;
12754 node.pinned = true;
12755 }
12756 drawNodeDisc(node, false);
12757 }
12758 function drawCurvedEdge(g, x1, y1, x2, y2, color, opts = {}) {
12759 const dx = x2 - x1;
12760 const cp1x = x1 + dx * 0.5;
12761 const cp1y = y1;
12762 const cp2x = x2 - dx * 0.5;
12763 const cp2y = y2;
12764 const alpha = opts.alpha ?? 0.5;
12765 const width = opts.width ?? 1.5;
12766 if (!opts.dashed) {
12767 g.moveTo(x1, y1);
12768 g.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x2, y2);
12769 g.stroke({ color, width, alpha });
12770 return;
12771 }
12772 const sampleAt = (t) => {
12773 const omt = 1 - t;
12774 const px = omt * omt * omt * x1 + 3 * omt * omt * t * cp1x + 3 * omt * t * t * cp2x + t * t * t * x2;
12775 const py = omt * omt * omt * y1 + 3 * omt * omt * t * cp1y + 3 * omt * t * t * cp2y + t * t * t * y2;
12776 return { x: px, y: py };
12777 };
12778 const STEPS = 32;
12779 const phase = opts.dashPhase ?? 0;
12780 const stride = Math.max(1, opts.dashStride ?? 1);
12781 let lastX = x1;
12782 let lastY = y1;
12783 for (let i = 1; i <= STEPS; i++) {
12784 const p = sampleAt(i / STEPS);
12785 const groupIdx = Math.floor((i - 1 + phase) / stride);
12786 const visible = groupIdx % 2 === 0;
12787 if (visible) {
12788 g.moveTo(lastX, lastY);
12789 g.lineTo(p.x, p.y);
12790 g.stroke({ color, width, alpha });
12791 }
12792 lastX = p.x;
12793 lastY = p.y;
12794 }
12795 }
12796 function drawNodeDisc(node, highlighted) {
12797 const g = node.gfx;
12798 g.clear();
12799 const r = node.radius;
12800 if (!highlighted) {
12801 g.circle(0, 5, r);
12802 g.fill({ color: 0, alpha: 0.18 });
12803 }
12804 if (highlighted) {
12805 g.circle(0, 0, r + 10);
12806 g.fill({ color: node.color, alpha: 0.22 });
12807 }
12808 g.circle(0, 0, r);
12809 g.fill(shadeColor(node.color, -0.18));
12810 g.circle(0, -r * 0.06, r * 0.94);
12811 g.fill(node.color);
12812 g.circle(-r * 0.32, -r * 0.42, r * 0.3);
12813 g.fill({ color: 16777215, alpha: 0.32 });
12814 g.circle(0, 0, r);
12815 g.stroke({
12816 color: 16777215,
12817 width: highlighted ? 3 : 2,
12818 alignment: 0
12819 });
12820 g.x = node.x;
12821 g.y = node.y;
12822 g.zIndex = 10;
12823 g.hitArea = new pixi.Circle(0, 0, r + 4);
12824 }
12825 function drawDropTarget(hover, sourceColor) {
12826 drawNodeDisc(hover, false);
12827 const g = hover.gfx;
12828 const t = performance.now();
12829 const pulse = Math.sin(t / 280) * 0.5 + 0.5;
12830 const ringR = hover.radius + 6 + pulse * 5;
12831 g.circle(0, 0, ringR);
12832 g.stroke({
12833 color: sourceColor,
12834 width: 3,
12835 alpha: 0.6 + pulse * 0.35
12836 });
12837 g.circle(0, 0, hover.radius * 0.42);
12838 g.fill({ color: sourceColor, alpha: 0.85 });
12839 g.hitArea = new pixi.Circle(0, 0, hover.radius + 12);
12840 }
12841 function drawEdges() {
12842 edgeGfx.clear();
12843 for (const node of nodes.values()) {
12844 if (!node.parent) {
12845 continue;
12846 }
12847 const parent = nodes.get(node.parent);
12848 if (!parent) {
12849 continue;
12850 }
12851 const isOldLink = dragNode !== null && node === dragNode;
12852 const isFocusEdge = focusId !== null && (node.id === focusId || node.parent === focusId);
12853 const dimMul = focusId !== null && !isFocusEdge ? 0.35 : 1;
12854 drawCurvedEdge(
12855 edgeGfx,
12856 parent.x,
12857 parent.y,
12858 node.x,
12859 node.y,
12860 parent.color,
12861 isOldLink ? { dashed: true, alpha: 0.28 * dimMul } : { alpha: 0.5 * dimMul }
12862 );
12863 }
12864 if (dragNode && dragHover) {
12865 const x1 = dragNode.x;
12866 const y1 = dragNode.y;
12867 const x2 = dragHover.x;
12868 const y2 = dragHover.y;
12869 const targetColor = dragHover.color;
12870 drawCurvedEdge(edgeGfx, x1, y1, x2, y2, targetColor, {
12871 alpha: 0.22,
12872 width: 9
12873 });
12874 const dashPhase = Math.floor(performance.now() / 70);
12875 drawCurvedEdge(edgeGfx, x1, y1, x2, y2, targetColor, {
12876 alpha: 0.95,
12877 width: 2.5,
12878 dashed: true,
12879 dashStride: 2,
12880 dashPhase
12881 });
12882 const pt = performance.now() % 1300 / 1300;
12883 const omt = 1 - pt;
12884 const dx = x2 - x1;
12885 const cp1x = x1 + dx * 0.5;
12886 const cp1y = y1;
12887 const cp2x = x2 - dx * 0.5;
12888 const cp2y = y2;
12889 const px = omt * omt * omt * x1 + 3 * omt * omt * pt * cp1x + 3 * omt * pt * pt * cp2x + pt * pt * pt * x2;
12890 const py = omt * omt * omt * y1 + 3 * omt * omt * pt * cp1y + 3 * omt * pt * pt * cp2y + pt * pt * pt * y2;
12891 edgeGfx.circle(px, py, 5);
12892 edgeGfx.fill({ color: 16777215, alpha: 0.95 });
12893 edgeGfx.stroke({ color: targetColor, width: 2, alpha: 1 });
12894 }
12895 postEdgeGfx.clear();
12896 if (focusId !== null) {
12897 const center = nodes.get(focusId);
12898 if (center) {
12899 for (const post of postNodes.values()) {
12900 postEdgeGfx.moveTo(center.x, center.y);
12901 postEdgeGfx.lineTo(post.x, post.y);
12902 postEdgeGfx.stroke({
12903 color: center.color,
12904 width: 1,
12905 alpha: 0.35
12906 });
12907 }
12908 }
12909 }
12910 }
12911 const FONT_FAMILY2 = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
12912 const CHIP_NAME_MAX_CHARS2 = 18;
12913 const POST_TITLE_MAX_CHARS2 = 22;
12914 function truncateChipName2(name) {
12915 return name.length > CHIP_NAME_MAX_CHARS2 ? name.slice(0, CHIP_NAME_MAX_CHARS2 - 1) + "…" : name;
12916 }
12917 function ensureChip(node) {
12918 const existing = chips.get(node.id);
12919 if (existing) {
12920 return existing;
12921 }
12922 const container = new pixi.Container();
12923 container.eventMode = "static";
12924 container.cursor = "pointer";
12925 const bg = new pixi.Graphics();
12926 container.addChild(bg);
12927 const nameText = new pixi.Text({
12928 text: truncateChipName2(node.name),
12929 style: {
12930 fill: 1909543,
12931 fontSize: 14,
12932 fontFamily: FONT_FAMILY2,
12933 fontWeight: "600"
12934 },
12935 resolution: CHIP_TEXT_RES2
12936 });
12937 container.addChild(nameText);
12938 const countBg = new pixi.Graphics();
12939 container.addChild(countBg);
12940 const countText = new pixi.Text({
12941 text: String(node.count),
12942 style: {
12943 fill: 16777215,
12944 fontSize: 12,
12945 fontFamily: FONT_FAMILY2,
12946 fontWeight: "700"
12947 },
12948 resolution: CHIP_TEXT_RES2
12949 });
12950 container.addChild(countText);
12951 const chip = {
12952 container,
12953 bg,
12954 nameText,
12955 countBg,
12956 countText,
12957 width: 0,
12958 height: 0,
12959 cachedName: "",
12960 cachedCount: -1,
12961 cachedFocused: false,
12962 cachedHover: false,
12963 cachedColor: -1
12964 };
12965 chips.set(node.id, chip);
12966 chipLayer.addChild(container);
12967 container.on("pointerdown", (e) => {
12968 e.stopPropagation?.();
12969 pixiInteractionAt = performance.now();
12970 });
12971 container.on("pointertap", () => {
12972 void focusNode(node.id);
12973 });
12974 container.on("pointerover", () => {
12975 chip.cachedHover = true;
12976 layoutChip(chip, node);
12977 });
12978 container.on("pointerout", () => {
12979 chip.cachedHover = false;
12980 layoutChip(chip, node);
12981 });
12982 return chip;
12983 }
12984 function layoutChip(chip, node) {
12985 const focused = focusId === node.id;
12986 const displayName = truncateChipName2(node.name);
12987 const countStr = String(node.count);
12988 if (chip.nameText.text !== displayName) {
12989 chip.nameText.text = displayName;
12990 }
12991 if (chip.countText.text !== countStr) {
12992 chip.countText.text = countStr;
12993 }
12994 chip.cachedName = displayName;
12995 chip.cachedCount = node.count;
12996 chip.cachedFocused = focused;
12997 chip.cachedColor = node.color;
12998 const padX = 9;
12999 const padY = 3;
13000 const gap = 5;
13001 const countPadX = 5;
13002 const countPadY = 2;
13003 const minBadgeW = 18;
13004 const nameW = chip.nameText.width;
13005 const nameH = chip.nameText.height;
13006 const countW = chip.countText.width;
13007 const countH = chip.countText.height;
13008 const badgeW = Math.max(minBadgeW, countW + countPadX * 2);
13009 const badgeH = countH + countPadY * 2;
13010 const totalW = padX + nameW + gap + badgeW + padX;
13011 const totalH = Math.max(nameH, badgeH) + padY * 2;
13012 chip.width = totalW;
13013 chip.height = totalH;
13014 const left = -totalW / 2;
13015 chip.bg.clear();
13016 chip.bg.roundRect(left, 0, totalW, totalH, totalH / 2);
13017 if (focused) {
13018 chip.bg.fill(node.color);
13019 } else if (chip.cachedHover) {
13020 chip.bg.fill({ color: 16777215, alpha: 0.96 });
13021 chip.bg.stroke({
13022 color: node.color,
13023 width: 1.5,
13024 alpha: 1
13025 });
13026 } else {
13027 chip.bg.fill({ color: 16777215, alpha: 0.88 });
13028 chip.bg.stroke({
13029 color: 0,
13030 width: 1,
13031 alpha: 0.06
13032 });
13033 }
13034 chip.nameText.x = left + padX;
13035 chip.nameText.y = (totalH - nameH) / 2;
13036 chip.nameText.style.fill = focused ? 16777215 : 1909543;
13037 const badgeX = left + padX + nameW + gap;
13038 const badgeY = (totalH - badgeH) / 2;
13039 chip.countBg.clear();
13040 chip.countBg.roundRect(
13041 badgeX,
13042 badgeY,
13043 badgeW,
13044 badgeH,
13045 badgeH / 2
13046 );
13047 chip.countBg.fill(
13048 focused ? { color: 16777215, alpha: 0.25 } : node.color
13049 );
13050 chip.countText.x = badgeX + (badgeW - countW) / 2;
13051 chip.countText.y = badgeY + (badgeH - countH) / 2;
13052 }
13053 function destroyChip(id) {
13054 const chip = chips.get(id);
13055 if (!chip) {
13056 return;
13057 }
13058 chipLayer.removeChild(chip.container);
13059 chip.container.destroy({ children: true });
13060 chips.delete(id);
13061 }
13062 function syncChipPositions() {
13063 const activeIds = new Set(nodes.keys());
13064 for (const id of [...chips.keys()]) {
13065 if (!activeIds.has(id)) {
13066 destroyChip(id);
13067 }
13068 }
13069 const chipCounterScale = 1 / Math.max(0.01, world.scale.x);
13070 const anyFocus = focusId !== null;
13071 for (const node of nodes.values()) {
13072 const chip = ensureChip(node);
13073 chip.container.x = node.x;
13074 chip.container.y = node.y + node.radius + 6;
13075 chip.container.scale.set(chipCounterScale);
13076 const focused = focusId === node.id;
13077 const targetAlpha = !anyFocus || focused ? 1 : 0.4;
13078 if (Math.abs(chip.container.alpha - targetAlpha) > 5e-3) {
13079 chip.container.alpha += (targetAlpha - chip.container.alpha) * 0.18;
13080 } else {
13081 chip.container.alpha = targetAlpha;
13082 }
13083 if (Math.abs(node.gfx.alpha - targetAlpha) > 5e-3) {
13084 node.gfx.alpha += (targetAlpha - node.gfx.alpha) * 0.18;
13085 } else {
13086 node.gfx.alpha = targetAlpha;
13087 }
13088 const displayName = truncateChipName2(node.name);
13089 if (chip.cachedName !== displayName || chip.cachedCount !== node.count || chip.cachedFocused !== focused || chip.cachedColor !== node.color) {
13090 layoutChip(chip, node);
13091 }
13092 }
13093 for (const post of postNodes.values()) {
13094 const chip = postChips.get(post.id);
13095 if (!chip) {
13096 continue;
13097 }
13098 chip.container.x = post.x;
13099 chip.container.y = post.y;
13100 chip.container.scale.set(chipCounterScale);
13101 if (chip.container.alpha < 1) {
13102 chip.container.alpha = Math.min(
13103 1,
13104 chip.container.alpha + 0.18
13105 );
13106 }
13107 }
13108 }
13109 function physicsStep(dt) {
13110 const list = Array.from(nodes.values());
13111 for (const a of list) {
13112 if (a.pinned) {
13113 a.x += (a.tx - a.x) * 0.12;
13114 a.y += (a.ty - a.y) * 0.12;
13115 a.gfx.x = a.x;
13116 a.gfx.y = a.y;
13117 continue;
13118 }
13119 let fx = 0;
13120 let fy = 0;
13121 for (const b of list) {
13122 if (a === b) {
13123 continue;
13124 }
13125 const dx = a.x - b.x;
13126 const dy = a.y - b.y;
13127 const d2 = dx * dx + dy * dy + 1;
13128 const f = REPULSION_K / d2;
13129 const d = Math.sqrt(d2);
13130 fx += dx / d * f;
13131 fy += dy / d * f;
13132 }
13133 const parent = nodes.get(a.parent);
13134 if (parent) {
13135 const dx = parent.x - a.x;
13136 const dy = parent.y - a.y;
13137 const d = Math.sqrt(dx * dx + dy * dy) || 1;
13138 const stretch = d - SPRING_LEN;
13139 fx += dx / d * stretch * SPRING_K;
13140 fy += dy / d * stretch * SPRING_K;
13141 } else {
13142 fx += -a.x * 8e-4;
13143 fy += -a.y * 8e-4;
13144 }
13145 if (nudgeAwayFrom && a.id !== focusId) {
13146 const ndx = a.x - nudgeAwayFrom.x;
13147 const ndy = a.y - nudgeAwayFrom.y;
13148 const nd = Math.sqrt(ndx * ndx + ndy * ndy) || 1;
13149 const limit = nudgeAwayFrom.radius + a.radius;
13150 if (nd < limit) {
13151 const pushK = 18;
13152 fx += ndx / nd * pushK * (limit - nd);
13153 fy += ndy / nd * pushK * (limit - nd);
13154 }
13155 }
13156 if (a !== dragNode) {
13157 a.x += fx * dt * 1e-3 + (a.tx - a.x) * 0.02;
13158 a.y += fy * dt * 1e-3 + (a.ty - a.y) * 0.02;
13159 }
13160 a.gfx.x = a.x;
13161 a.gfx.y = a.y;
13162 }
13163 }
13164 function preSettlePhysics(iterations) {
13165 for (let i = 0; i < iterations; i++) {
13166 physicsStep(16);
13167 }
13168 for (const n of nodes.values()) {
13169 n.tx = n.x;
13170 n.ty = n.y;
13171 }
13172 }
13173 function tick() {
13174 const now = performance.now();
13175 const dt = Math.min(50, now - lastTick);
13176 lastTick = now;
13177 const ZOOM_EASE = 0.22;
13178 const ds = targetScale - world.scale.x;
13179 const dwx = targetWorldX - world.x;
13180 const dwy = targetWorldY - world.y;
13181 if (Math.abs(ds) > 5e-4 || Math.abs(dwx) > 0.5 || Math.abs(dwy) > 0.5) {
13182 world.scale.set(world.scale.x + ds * ZOOM_EASE);
13183 world.x += dwx * ZOOM_EASE;
13184 world.y += dwy * ZOOM_EASE;
13185 }
13186 physicsStep(dt);
13187 for (const p of postNodes.values()) {
13188 p.x += (p.tx - p.x) * 0.18;
13189 p.y += (p.ty - p.y) * 0.18;
13190 p.gfx.x = p.x;
13191 p.gfx.y = p.y;
13192 }
13193 drawEdges();
13194 if (dragNode && dragHover) {
13195 drawDropTarget(dragHover, dragNode.color);
13196 }
13197 syncChipPositions();
13198 raf = requestAnimationFrame(tick);
13199 }
13200 let dragStartPos = null;
13201 let dragOffset = { x: 0, y: 0 };
13202 function onNodePointerDown(e, node) {
13203 const ev = e;
13204 ev.stopPropagation?.();
13205 pixiInteractionAt = performance.now();
13206 dragNode = node;
13207 node.pinned = true;
13208 node.tx = node.x;
13209 node.ty = node.y;
13210 dragStartPos = { x: ev.global.x, y: ev.global.y };
13211 const local = stageToWorld({ x: ev.global.x, y: ev.global.y });
13212 dragOffset = { x: node.x - local.x, y: node.y - local.y };
13213 }
13214 function stageToWorld(global) {
13215 return {
13216 x: (global.x - world.x) / world.scale.x,
13217 y: (global.y - world.y) / world.scale.y
13218 };
13219 }
13220 function onStagePointerDown(e) {
13221 const ev = e;
13222 panActive = true;
13223 panStart = { x: ev.global.x, y: ev.global.y };
13224 panMovedDist = 0;
13225 }
13226 function onStagePointerMove(e) {
13227 const ev = e;
13228 if (dragNode) {
13229 const cursorWorld = stageToWorld(ev.global);
13230 const nx = cursorWorld.x + dragOffset.x;
13231 const ny = cursorWorld.y + dragOffset.y;
13232 dragNode.x = nx;
13233 dragNode.y = ny;
13234 dragNode.tx = nx;
13235 dragNode.ty = ny;
13236 dragNode.gfx.x = nx;
13237 dragNode.gfx.y = ny;
13238 let hover = null;
13239 for (const c of nodes.values()) {
13240 if (c === dragNode) {
13241 continue;
13242 }
13243 const dx = c.x - cursorWorld.x;
13244 const dy = c.y - cursorWorld.y;
13245 if (dx * dx + dy * dy < c.radius * c.radius) {
13246 hover = c;
13247 break;
13248 }
13249 }
13250 if (hover !== dragHover) {
13251 if (dragHover) {
13252 drawNodeDisc(dragHover, focusId === dragHover.id);
13253 }
13254 dragHover = hover;
13255 if (hover && dragNode) {
13256 drawDropTarget(hover, dragNode.color);
13257 }
13258 }
13259 return;
13260 }
13261 if (panActive && panStart) {
13262 const dx = ev.global.x - panStart.x;
13263 const dy = ev.global.y - panStart.y;
13264 world.x += dx;
13265 world.y += dy;
13266 targetWorldX += dx;
13267 targetWorldY += dy;
13268 panMovedDist += Math.sqrt(dx * dx + dy * dy);
13269 panStart = { x: ev.global.x, y: ev.global.y };
13270 }
13271 }
13272 async function onStagePointerUp(e) {
13273 if (dragNode) {
13274 const node = dragNode;
13275 const target = dragHover;
13276 const startPos = dragStartPos;
13277 dragNode = null;
13278 dragHover = null;
13279 dragStartPos = null;
13280 node.pinned = node.depth === 0;
13281 let movement = Infinity;
13282 const ev = e;
13283 if (startPos && ev && ev.global) {
13284 const dx = ev.global.x - startPos.x;
13285 const dy = ev.global.y - startPos.y;
13286 movement = Math.sqrt(dx * dx + dy * dy);
13287 }
13288 if (!target && movement < 2) {
13289 focusNode(node.id);
13290 panActive = false;
13291 panStart = null;
13292 return;
13293 }
13294 if (target && target.id !== node.parent && !isAncestor(node.id, target.id)) {
13295 try {
13296 await client.updateTerm("categories", node.id, {
13297 parent: target.id
13298 });
13299 node.parent = target.id;
13300 terms = terms.map(
13301 (t) => t.id === node.id ? { ...t, parent: target.id } : t
13302 );
13303 buildTree();
13304 } catch (err) {
13305 showError(__("Reparent failed:"), err);
13306 }
13307 } else {
13308 drawNodeDisc(node, focusId === node.id);
13309 if (target) {
13310 drawNodeDisc(target, focusId === target.id);
13311 }
13312 }
13313 }
13314 panActive = false;
13315 panStart = null;
13316 }
13317 app.stage.eventMode = "static";
13318 app.stage.hitArea = new pixi.Rectangle(
13319 0,
13320 0,
13321 stage.clientWidth,
13322 stage.clientHeight
13323 );
13324 app.stage.on("pointerdown", onStagePointerDown);
13325 app.stage.on("pointermove", onStagePointerMove);
13326 app.stage.on("pointerup", (e) => void onStagePointerUp(e));
13327 app.stage.on("pointerupoutside", (e) => void onStagePointerUp(e));
13328 function onWheel(e) {
13329 e.preventDefault();
13330 const SENSITIVITY = 8e-4;
13331 const factor = Math.exp(-e.deltaY * SENSITIVITY);
13332 const prev = targetScale;
13333 const next = Math.max(0.3, Math.min(2.5, prev * factor));
13334 if (Math.abs(next - prev) < 5e-4) {
13335 return;
13336 }
13337 const r = stage.getBoundingClientRect();
13338 const sx = e.clientX - r.left;
13339 const sy = e.clientY - r.top;
13340 const wx = (sx - targetWorldX) / prev;
13341 const wy = (sy - targetWorldY) / prev;
13342 targetScale = next;
13343 targetWorldX = sx - wx * next;
13344 targetWorldY = sy - wy * next;
13345 }
13346 stage.addEventListener("wheel", onWheel, { passive: false });
13347 let firstFitDone = false;
13348 let settledW = 0;
13349 let settledH = 0;
13350 const SETTLE_THRESHOLD_PX = 24;
13351 const SETTLE_DEBOUNCE_MS = 80;
13352 let settleTimer = null;
13353 function onResize() {
13354 const r = stage.getBoundingClientRect();
13355 app.renderer.resize(r.width, r.height);
13356 app.stage.hitArea = new pixi.Rectangle(0, 0, r.width, r.height);
13357 if (!firstFitDone && r.width > 0 && r.height > 0) {
13358 firstFitDone = true;
13359 settledW = r.width;
13360 settledH = r.height;
13361 fitToView();
13362 stage.classList.remove("is-loading");
13363 }
13364 if (settleTimer !== null) {
13365 window.clearTimeout(settleTimer);
13366 }
13367 settleTimer = window.setTimeout(() => {
13368 settleTimer = null;
13369 const cur = stage.getBoundingClientRect();
13370 const dw = Math.abs(cur.width - settledW);
13371 const dh = Math.abs(cur.height - settledH);
13372 if (dw >= SETTLE_THRESHOLD_PX || dh >= SETTLE_THRESHOLD_PX) {
13373 settledW = cur.width;
13374 settledH = cur.height;
13375 recenterCamera();
13376 }
13377 }, SETTLE_DEBOUNCE_MS);
13378 app.render();
13379 }
13380 const ro = new ResizeObserver(onResize);
13381 ro.observe(stage);
13382 function isAncestor(ancestor, descendant) {
13383 let cur = nodes.get(descendant);
13384 let safety = 32;
13385 while (cur && safety-- > 0) {
13386 if (cur.id === ancestor) {
13387 return true;
13388 }
13389 if (!cur.parent) {
13390 return false;
13391 }
13392 cur = nodes.get(cur.parent);
13393 }
13394 return false;
13395 }
13396 let lastFocusChange = 0;
13397 const SPOTLIGHT_RADIUS2 = POST_RING_RADIUS$1 + 130;
13398 async function focusNode(id) {
13399 if (focusId === id) {
13400 closeFocus();
13401 return;
13402 }
13403 const wasFocused = focusId !== null;
13404 focusId = id;
13405 focusPage = 1;
13406 lastFocusChange = performance.now();
13407 const focused = nodes.get(id);
13408 if (focused) {
13409 if (!wasFocused) {
13410 prevView = {
13411 scale: targetScale,
13412 x: targetWorldX,
13413 y: targetWorldY
13414 };
13415 }
13416 const r = stage.getBoundingClientRect();
13417 if (r.width > 0 && r.height > 0) {
13418 const half = POST_RING_RADIUS$1 + 70;
13419 const sx = r.width * 0.85 / (2 * half);
13420 const sy = r.height * 0.85 / (2 * half);
13421 const newScale = Math.max(
13422 0.5,
13423 Math.min(1.6, Math.min(sx, sy))
13424 );
13425 targetScale = newScale;
13426 targetWorldX = r.width / 2 - focused.x * newScale;
13427 targetWorldY = r.height / 2 - focused.y * newScale;
13428 }
13429 nudgeAwayFrom = {
13430 x: focused.x,
13431 y: focused.y,
13432 radius: SPOTLIGHT_RADIUS2
13433 };
13434 pinnedTargetBackup.clear();
13435 for (const n of nodes.values()) {
13436 if (n.id === id || !n.pinned) {
13437 continue;
13438 }
13439 const dx = n.x - focused.x;
13440 const dy = n.y - focused.y;
13441 const d = Math.sqrt(dx * dx + dy * dy) || 1;
13442 if (d >= SPOTLIGHT_RADIUS2 + n.radius) {
13443 continue;
13444 }
13445 pinnedTargetBackup.set(n.id, { tx: n.tx, ty: n.ty });
13446 const push = SPOTLIGHT_RADIUS2 + n.radius + 20;
13447 n.tx = focused.x + dx / d * push;
13448 n.ty = focused.y + dy / d * push;
13449 }
13450 }
13451 for (const n of nodes.values()) {
13452 drawNodeDisc(n, focusId === n.id);
13453 }
13454 paintSidebar();
13455 await loadPostsForFocus();
13456 }
13457 function closeFocus() {
13458 focusId = null;
13459 lastFocusChange = performance.now();
13460 loadSeq++;
13461 nudgeAwayFrom = null;
13462 for (const [id, t] of pinnedTargetBackup) {
13463 const n = nodes.get(id);
13464 if (n) {
13465 n.tx = t.tx;
13466 n.ty = t.ty;
13467 }
13468 }
13469 pinnedTargetBackup.clear();
13470 if (prevView) {
13471 targetScale = prevView.scale;
13472 targetWorldX = prevView.x;
13473 targetWorldY = prevView.y;
13474 prevView = null;
13475 }
13476 paintSidebar();
13477 clearPosts();
13478 for (const n of nodes.values()) {
13479 drawNodeDisc(n, false);
13480 }
13481 }
13482 function clearPosts() {
13483 for (const post of postNodes.values()) {
13484 postLayer.removeChild(post.gfx);
13485 post.gfx.destroy();
13486 }
13487 postNodes.clear();
13488 for (const chip of postChips.values()) {
13489 postChipLayer.removeChild(chip.container);
13490 chip.container.destroy({ children: true });
13491 }
13492 postChips.clear();
13493 postEdgeGfx.clear();
13494 pager.visible = false;
13495 }
13496 function ensurePostChip(post) {
13497 const existing = postChips.get(post.id);
13498 if (existing) {
13499 return existing;
13500 }
13501 const container = new pixi.Container();
13502 container.eventMode = "static";
13503 container.cursor = "pointer";
13504 container.alpha = 0;
13505 const bg = new pixi.Graphics();
13506 container.addChild(bg);
13507 const dot = new pixi.Graphics();
13508 container.addChild(dot);
13509 const titleText = new pixi.Text({
13510 text: post.title,
13511 style: {
13512 fill: 1909543,
13513 // Matches category chip fontSize so the two read at
13514 // the same weight when both are deployed. Base size
13515 // is the on-screen size since the post chip's
13516 // container counter-scales with `1/world.scale.x`
13517 // in `syncChipPositions`.
13518 fontSize: 14,
13519 fontFamily: FONT_FAMILY2,
13520 fontWeight: "500"
13521 },
13522 resolution: CHIP_TEXT_RES2
13523 });
13524 container.addChild(titleText);
13525 const chip = {
13526 container,
13527 bg,
13528 dot,
13529 titleText,
13530 width: 0,
13531 height: 0,
13532 cachedTitle: "",
13533 cachedHover: false
13534 };
13535 postChips.set(post.id, chip);
13536 postChipLayer.addChild(container);
13537 container.on("pointerdown", (e) => {
13538 e.stopPropagation?.();
13539 pixiInteractionAt = performance.now();
13540 });
13541 container.on("pointertap", () => {
13542 openInPostsTab(post.id, post.editUrl, post.title);
13543 closeFocus();
13544 });
13545 container.on("pointerover", () => {
13546 chip.cachedHover = true;
13547 layoutPostChip(chip, post);
13548 });
13549 container.on("pointerout", () => {
13550 chip.cachedHover = false;
13551 layoutPostChip(chip, post);
13552 });
13553 layoutPostChip(chip, post);
13554 return chip;
13555 }
13556 function layoutPostChip(chip, post) {
13557 const displayTitle = post.title.length > POST_TITLE_MAX_CHARS2 ? post.title.slice(0, POST_TITLE_MAX_CHARS2 - 1) + "…" : post.title;
13558 if (chip.titleText.text !== displayTitle) {
13559 chip.titleText.text = displayTitle;
13560 }
13561 chip.cachedTitle = displayTitle;
13562 const padX = 9;
13563 const padY = 3;
13564 const dotR = 4;
13565 const gap = 6;
13566 const titleW = chip.titleText.width;
13567 const titleH = chip.titleText.height;
13568 const totalW = padX + dotR * 2 + gap + titleW + padX;
13569 const totalH = Math.max(titleH, dotR * 2) + padY * 2;
13570 chip.width = totalW;
13571 chip.height = totalH;
13572 const left = -totalW / 2;
13573 const top = -totalH / 2;
13574 chip.bg.clear();
13575 chip.bg.roundRect(left, top, totalW, totalH, totalH / 2);
13576 if (chip.cachedHover) {
13577 chip.bg.fill({ color: 16777215, alpha: 1 });
13578 chip.bg.stroke({
13579 color: post.tone,
13580 width: 1.5,
13581 alpha: 1
13582 });
13583 } else {
13584 chip.bg.fill({ color: 16777215, alpha: 0.95 });
13585 chip.bg.stroke({
13586 color: 0,
13587 width: 1,
13588 alpha: 0.12
13589 });
13590 }
13591 chip.dot.clear();
13592 chip.dot.circle(left + padX + dotR, 0, dotR);
13593 chip.dot.fill({ color: post.tone, alpha: 0.85 });
13594 chip.dot.stroke({ color: 16777215, width: 1 });
13595 chip.titleText.x = left + padX + dotR * 2 + gap;
13596 chip.titleText.y = -titleH / 2;
13597 }
13598 const POSTS_CACHE_TTL_MS = 6e4;
13599 const postsCache = /* @__PURE__ */ new Map();
13600 function applyPostsResult(entry, focusedNodeId) {
13601 focusTotalPages = entry.totalPages;
13602 if (Number.isFinite(entry.realTotal)) {
13603 const node = nodes.get(focusedNodeId);
13604 if (node && node.count !== entry.realTotal) {
13605 node.count = entry.realTotal;
13606 terms = terms.map(
13607 (t) => t.id === node.id ? { ...t, count: entry.realTotal } : t
13608 );
13609 layoutChip(ensureChip(node), node);
13610 }
13611 }
13612 renderPosts(entry.items);
13613 }
13614 async function loadPostsForFocus() {
13615 if (focusId === null) {
13616 return;
13617 }
13618 const mySeq = ++loadSeq;
13619 const myFocusId = focusId;
13620 const cacheKey2 = `${focusId}:${focusPage}`;
13621 const cached = postsCache.get(cacheKey2);
13622 if (cached && performance.now() - cached.fetchedAt < POSTS_CACHE_TTL_MS) {
13623 applyPostsResult(cached, myFocusId);
13624 return;
13625 }
13626 const cfg = client.getConfig();
13627 const url = new URL(cfg.postsUrl);
13628 url.searchParams.set("categories", String(focusId));
13629 url.searchParams.set("per_page", String(POST_PER_PAGE$1));
13630 url.searchParams.set("page", String(focusPage));
13631 url.searchParams.set("status", "any");
13632 url.searchParams.set("_fields", "id,title,status");
13633 try {
13634 const response = await fetchShellJson$1(client, url.toString());
13635 if (mySeq !== loadSeq || focusId !== myFocusId) {
13636 return;
13637 }
13638 const raw = response.json ?? [];
13639 const totalPages = Math.max(
13640 1,
13641 parseInt(response.headers.get("X-WP-TotalPages") ?? "1", 10) || 1
13642 );
13643 const realTotalParsed = parseInt(response.headers.get("X-WP-Total") ?? "", 10);
13644 const realTotal = Number.isFinite(realTotalParsed) ? realTotalParsed : -1;
13645 const items = raw.map((p) => ({
13646 id: p.id,
13647 title: stripTags$1(p.title?.rendered || `#${p.id}`),
13648 editUrl: `${cfg.editPostUrlBase}?post=${p.id}&action=edit`
13649 }));
13650 const entry = {
13651 items,
13652 totalPages,
13653 realTotal,
13654 fetchedAt: performance.now()
13655 };
13656 postsCache.set(cacheKey2, entry);
13657 applyPostsResult(entry, myFocusId);
13658 } catch (err) {
13659 showError(__("Couldn’t load posts:"), err);
13660 }
13661 }
13662 function renderPosts(items) {
13663 clearPosts();
13664 if (focusId === null) {
13665 return;
13666 }
13667 const center = nodes.get(focusId);
13668 if (!center) {
13669 return;
13670 }
13671 const count = items.length;
13672 const ringR = POST_RING_RADIUS$1 + Math.max(0, count - 8) * 6;
13673 items.forEach((item, idx) => {
13674 const angle = 2 * Math.PI / Math.max(1, count) * idx - Math.PI / 2;
13675 const tx = center.x + Math.cos(angle) * ringR;
13676 const ty = center.y + Math.sin(angle) * ringR;
13677 const tone = center.color;
13678 const gfx = new pixi.Graphics();
13679 postLayer.addChild(gfx);
13680 const post = {
13681 id: item.id,
13682 title: item.title,
13683 editUrl: item.editUrl,
13684 angle,
13685 r: ringR,
13686 x: center.x,
13687 y: center.y,
13688 tx,
13689 ty,
13690 gfx,
13691 tone
13692 };
13693 postNodes.set(item.id, post);
13694 ensurePostChip(post);
13695 });
13696 repaintPager();
13697 }
13698 function repaintPager() {
13699 if (focusId === null || focusTotalPages <= 1) {
13700 pager.visible = false;
13701 return;
13702 }
13703 pager.visible = true;
13704 const center = nodes.get(focusId);
13705 if (!center) {
13706 pager.visible = false;
13707 return;
13708 }
13709 const prevDisabled = focusPage <= 1;
13710 const nextDisabled = focusPage >= focusTotalPages;
13711 drawPagerButton(pagerPrev, "◀", prevDisabled);
13712 drawPagerButton(pagerNext, "▶", nextDisabled);
13713 pagerPrev.cursor = prevDisabled ? "default" : "pointer";
13714 pagerNext.cursor = nextDisabled ? "default" : "pointer";
13715 pagerLabel.text = `${focusPage} / ${focusTotalPages}`;
13716 pagerPrev.x = -38;
13717 pagerPrev.y = 0;
13718 pagerNext.x = 38;
13719 pagerNext.y = 0;
13720 pagerLabel.x = 0;
13721 pagerLabel.y = 0;
13722 pager.x = center.x;
13723 pager.y = center.y + POST_RING_RADIUS$1 + 60;
13724 }
13725 function drawPagerButton(gfx, glyph, disabled) {
13726 gfx.clear();
13727 gfx.circle(0, 0, 14);
13728 gfx.fill({
13729 color: disabled ? 15921906 : 16777215,
13730 alpha: disabled ? 0.7 : 1
13731 });
13732 gfx.stroke({
13733 color: 0,
13734 width: 1,
13735 alpha: 0.12
13736 });
13737 const children = gfx.children;
13738 const label = children?.[0] ?? null;
13739 if (!label) {
13740 const t = new pixi.Text({
13741 text: glyph,
13742 style: {
13743 fill: disabled ? 11580344 : 5265246,
13744 fontSize: 16,
13745 fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
13746 fontWeight: "600"
13747 },
13748 resolution: CHIP_TEXT_RES2
13749 });
13750 t.anchor.set(0.5);
13751 gfx.addChild(t);
13752 } else {
13753 label.text = glyph;
13754 label.style.fill = disabled ? 11580344 : 5265246;
13755 }
13756 }
13757 function openInPostsTab(_id, editUrl, title) {
13758 const wm = api?.windowManager;
13759 const derive = api?.deriveWindowId;
13760 const postsWin = wm && typeof wm.getById === "function" ? wm.getById("desktop-mode-posts") : void 0;
13761 if (postsWin && typeof postsWin.isFullscreen === "function" && typeof postsWin.toggleFullscreen === "function" && postsWin.isFullscreen()) {
13762 postsWin.toggleFullscreen();
13763 }
13764 if (wm && typeof derive === "function") {
13765 const id = derive(editUrl);
13766 wm.open({
13767 id,
13768 baseId: id,
13769 url: editUrl,
13770 title: title ?? editUrl,
13771 icon: "dashicons-admin-post"
13772 });
13773 return;
13774 }
13775 try {
13776 window.open(editUrl, "_blank");
13777 } catch {
13778 window.location.assign(editUrl);
13779 }
13780 }
13781 function paintDraftSidebar(d) {
13782 const parentNode = d.parent !== 0 ? nodes.get(d.parent) : null;
13783 const header = document.createElement("div");
13784 header.className = "wpd-mindmap__sidebar-header";
13785 const dot = document.createElement("span");
13786 dot.className = "wpd-mindmap__sidebar-dot";
13787 const color = parentNode ? parentNode.color : clusterColor(terms.length);
13788 dot.style.background = `#${color.toString(16).padStart(6, "0")}`;
13789 const label = document.createElement("code");
13790 label.className = "wpd-mindmap__sidebar-slug";
13791 label.textContent = parentNode ? sprintf(
13792 /* translators: %s: parent category name. */
13793 __("New child of %s"),
13794 parentNode.name
13795 ) : __("New root category");
13796 header.appendChild(dot);
13797 header.appendChild(label);
13798 sidebar.appendChild(header);
13799 const nameLabel = document.createElement("label");
13800 nameLabel.className = "wpd-mindmap__sidebar-label";
13801 nameLabel.textContent = __("Name");
13802 sidebar.appendChild(nameLabel);
13803 const nameInput = document.createElement("input");
13804 nameInput.type = "text";
13805 nameInput.className = "wpd-mindmap__editor-name";
13806 nameInput.placeholder = __("e.g. Recipes");
13807 sidebar.appendChild(nameInput);
13808 requestAnimationFrame(() => nameInput.focus());
13809 const slugLabel = document.createElement("label");
13810 slugLabel.className = "wpd-mindmap__sidebar-label";
13811 slugLabel.textContent = __("Slug");
13812 sidebar.appendChild(slugLabel);
13813 const slugInput = document.createElement("input");
13814 slugInput.type = "text";
13815 slugInput.className = "wpd-mindmap__editor-name";
13816 slugInput.placeholder = __("auto-from-name");
13817 slugInput.spellcheck = false;
13818 slugInput.autocapitalize = "off";
13819 slugInput.addEventListener("input", () => {
13820 const v = slugInput.value;
13821 const norm = v.toLowerCase().replace(/[^a-z0-9-]+/g, "-");
13822 if (v !== norm) {
13823 const sel = slugInput.selectionStart ?? norm.length;
13824 slugInput.value = norm;
13825 slugInput.setSelectionRange(sel, sel);
13826 }
13827 });
13828 sidebar.appendChild(slugInput);
13829 const descLabel = document.createElement("label");
13830 descLabel.className = "wpd-mindmap__sidebar-label";
13831 descLabel.textContent = __("Description");
13832 sidebar.appendChild(descLabel);
13833 const descInput = document.createElement("textarea");
13834 descInput.className = "wpd-mindmap__editor-desc";
13835 descInput.placeholder = __("Description (optional)");
13836 descInput.rows = 4;
13837 sidebar.appendChild(descInput);
13838 const actions = document.createElement("div");
13839 actions.className = "wpd-mindmap__editor-actions";
13840 const createBtn = document.createElement("button");
13841 createBtn.type = "button";
13842 createBtn.className = "wpd-mindmap__btn wpd-mindmap__btn--primary";
13843 createBtn.textContent = __("Create");
13844 const cancelBtn = document.createElement("button");
13845 cancelBtn.type = "button";
13846 cancelBtn.className = "wpd-mindmap__btn wpd-mindmap__btn--danger";
13847 cancelBtn.textContent = __("Cancel");
13848 const handleCreate = async () => {
13849 const name = nameInput.value.trim();
13850 if (!name) {
13851 nameInput.focus();
13852 return;
13853 }
13854 createBtn.disabled = true;
13855 try {
13856 const created = await client.createCategory(name, d.parent, {
13857 slug: slugInput.value.trim() || void 0,
13858 description: descInput.value || void 0
13859 });
13860 const next = {
13861 id: created.id,
13862 name: created.name,
13863 slug: created.slug || "",
13864 parent: created.parent,
13865 count: 0,
13866 description: created.description || "",
13867 isDefault: false
13868 };
13869 if (!terms.some((t) => t.id === next.id)) {
13870 terms = terms.concat(next);
13871 }
13872 draft = null;
13873 buildTree();
13874 focusId = created.id;
13875 paintSidebar();
13876 await loadPostsForFocus();
13877 } catch (err) {
13878 createBtn.disabled = false;
13879 showError(__("Couldn’t create:"), err);
13880 }
13881 };
13882 createBtn.addEventListener("click", () => {
13883 void handleCreate();
13884 });
13885 cancelBtn.addEventListener("click", () => {
13886 draft = null;
13887 paintSidebar();
13888 });
13889 nameInput.addEventListener("keydown", (e) => {
13890 if (e.key === "Enter") {
13891 e.preventDefault();
13892 void handleCreate();
13893 } else if (e.key === "Escape") {
13894 draft = null;
13895 paintSidebar();
13896 }
13897 });
13898 actions.appendChild(createBtn);
13899 actions.appendChild(cancelBtn);
13900 sidebar.appendChild(actions);
13901 }
13902 function paintSidebar() {
13903 sidebar.replaceChildren();
13904 if (draft !== null) {
13905 paintDraftSidebar(draft);
13906 return;
13907 }
13908 if (focusId === null) {
13909 const empty = document.createElement("div");
13910 empty.className = "wpd-mindmap__sidebar-empty";
13911 const icon = document.createElement("span");
13912 icon.className = "dashicons dashicons-admin-tools";
13913 icon.setAttribute("aria-hidden", "true");
13914 empty.appendChild(icon);
13915 const title = document.createElement("h3");
13916 title.textContent = __("No category selected");
13917 empty.appendChild(title);
13918 const help = document.createElement("p");
13919 help.textContent = __(
13920 "Click a node on the mindmap to edit its name, description, and posts."
13921 );
13922 empty.appendChild(help);
13923 sidebar.appendChild(empty);
13924 return;
13925 }
13926 const node = nodes.get(focusId);
13927 if (!node) {
13928 focusId = null;
13929 paintSidebar();
13930 return;
13931 }
13932 const id = node.id;
13933 const header = document.createElement("div");
13934 header.className = "wpd-mindmap__sidebar-header";
13935 const dot = document.createElement("span");
13936 dot.className = "wpd-mindmap__sidebar-dot";
13937 dot.style.background = `#${node.color.toString(16).padStart(6, "0")}`;
13938 const term = terms.find((t) => t.id === id);
13939 const idLabel = document.createElement("code");
13940 idLabel.className = "wpd-mindmap__sidebar-slug";
13941 idLabel.textContent = `#${id}`;
13942 header.appendChild(dot);
13943 header.appendChild(idLabel);
13944 sidebar.appendChild(header);
13945 const nameLabel = document.createElement("label");
13946 nameLabel.className = "wpd-mindmap__sidebar-label";
13947 nameLabel.textContent = __("Name");
13948 sidebar.appendChild(nameLabel);
13949 const nameInput = document.createElement("input");
13950 nameInput.type = "text";
13951 nameInput.className = "wpd-mindmap__editor-name";
13952 nameInput.value = node.name;
13953 nameInput.placeholder = __("Name");
13954 sidebar.appendChild(nameInput);
13955 const slugLabel = document.createElement("label");
13956 slugLabel.className = "wpd-mindmap__sidebar-label";
13957 slugLabel.textContent = __("Slug");
13958 sidebar.appendChild(slugLabel);
13959 const slugInput = document.createElement("input");
13960 slugInput.type = "text";
13961 slugInput.className = "wpd-mindmap__editor-name";
13962 slugInput.value = term?.slug || "";
13963 slugInput.placeholder = __("auto-from-name");
13964 slugInput.spellcheck = false;
13965 slugInput.autocapitalize = "off";
13966 slugInput.addEventListener("input", () => {
13967 const v = slugInput.value;
13968 const norm = v.toLowerCase().replace(/[^a-z0-9-]+/g, "-");
13969 if (v !== norm) {
13970 const sel = slugInput.selectionStart ?? norm.length;
13971 slugInput.value = norm;
13972 slugInput.setSelectionRange(sel, sel);
13973 }
13974 });
13975 sidebar.appendChild(slugInput);
13976 const descLabel = document.createElement("label");
13977 descLabel.className = "wpd-mindmap__sidebar-label";
13978 descLabel.textContent = __("Description");
13979 sidebar.appendChild(descLabel);
13980 const descInput = document.createElement("textarea");
13981 descInput.className = "wpd-mindmap__editor-desc";
13982 descInput.value = node.description || "";
13983 descInput.placeholder = __("Description (optional)");
13984 descInput.rows = 4;
13985 sidebar.appendChild(descInput);
13986 const meta = document.createElement("p");
13987 meta.className = "wpd-mindmap__sidebar-meta";
13988 meta.textContent = sprintf(
13989 /* translators: %d: post count. */
13990 __("%d posts in this category."),
13991 node.count
13992 );
13993 sidebar.appendChild(meta);
13994 const actions = document.createElement("div");
13995 actions.className = "wpd-mindmap__editor-actions";
13996 const addChildBtn = document.createElement("button");
13997 addChildBtn.type = "button";
13998 addChildBtn.className = "wpd-mindmap__btn wpd-mindmap__btn--secondary";
13999 addChildBtn.textContent = __("+ Child");
14000 addChildBtn.addEventListener("click", () => {
14001 startDraft(id);
14002 });
14003 const makeRootBtn = node.parent && node.parent !== 0 ? document.createElement("button") : null;
14004 if (makeRootBtn) {
14005 makeRootBtn.type = "button";
14006 makeRootBtn.className = "wpd-mindmap__btn wpd-mindmap__btn--secondary";
14007 makeRootBtn.textContent = __("Make root");
14008 makeRootBtn.title = __(
14009 "Promote this category to a top-level root (no parent)."
14010 );
14011 makeRootBtn.addEventListener("click", async () => {
14012 try {
14013 await client.updateTerm("categories", node.id, { parent: 0 });
14014 node.parent = 0;
14015 terms = terms.map(
14016 (t) => t.id === node.id ? { ...t, parent: 0 } : t
14017 );
14018 buildTree();
14019 paintSidebar();
14020 } catch (err) {
14021 showError(__("Couldn’t reparent:"), err);
14022 }
14023 });
14024 }
14025 const saveBtn = document.createElement("button");
14026 saveBtn.type = "button";
14027 saveBtn.className = "wpd-mindmap__btn wpd-mindmap__btn--primary";
14028 saveBtn.textContent = __("Save");
14029 saveBtn.addEventListener("click", async () => {
14030 const name = nameInput.value.trim();
14031 if (!name) {
14032 return;
14033 }
14034 const description = descInput.value;
14035 const slugRaw = slugInput.value.trim();
14036 const currentSlug = term?.slug ?? "";
14037 if (name === node.name && description === (node.description || "") && slugRaw === currentSlug) {
14038 return;
14039 }
14040 const patch = { name, description };
14041 if (slugRaw !== currentSlug) {
14042 patch.slug = slugRaw;
14043 }
14044 try {
14045 const updated = await client.updateTerm(
14046 "categories",
14047 node.id,
14048 patch
14049 );
14050 node.name = updated.name;
14051 node.description = updated.description;
14052 terms = terms.map(
14053 (t) => t.id === node.id ? {
14054 ...t,
14055 name: updated.name,
14056 description: updated.description,
14057 slug: updated.slug ?? t.slug
14058 } : t
14059 );
14060 layoutChip(ensureChip(node), node);
14061 paintSidebar();
14062 } catch (err) {
14063 showError(__("Couldn’t save:"), err);
14064 }
14065 });
14066 const delBtn = document.createElement("button");
14067 delBtn.type = "button";
14068 delBtn.className = "wpd-mindmap__btn wpd-mindmap__btn--danger";
14069 delBtn.textContent = __("Delete");
14070 let armResetTimer = null;
14071 const armDelete = () => {
14072 delBtn.textContent = __("Click again to delete");
14073 delBtn.classList.add("is-armed");
14074 if (armResetTimer !== null) {
14075 window.clearTimeout(armResetTimer);
14076 }
14077 armResetTimer = window.setTimeout(() => {
14078 delBtn.textContent = __("Delete");
14079 delBtn.classList.remove("is-armed");
14080 armResetTimer = null;
14081 }, 2500);
14082 };
14083 delBtn.addEventListener("click", async () => {
14084 if (!delBtn.classList.contains("is-armed")) {
14085 armDelete();
14086 return;
14087 }
14088 if (armResetTimer !== null) {
14089 window.clearTimeout(armResetTimer);
14090 armResetTimer = null;
14091 }
14092 try {
14093 await client.deleteTerm("categories", node.id);
14094 terms = terms.filter((t) => t.id !== node.id);
14095 focusId = null;
14096 clearPosts();
14097 buildTree();
14098 paintSidebar();
14099 } catch (err) {
14100 showError(__("Couldn’t delete:"), err);
14101 }
14102 });
14103 actions.appendChild(addChildBtn);
14104 if (makeRootBtn) {
14105 actions.appendChild(makeRootBtn);
14106 }
14107 actions.appendChild(saveBtn);
14108 actions.appendChild(delBtn);
14109 sidebar.appendChild(actions);
14110 }
14111 function startDraft(parent) {
14112 if (parent !== 0 && !nodes.get(parent)) {
14113 return;
14114 }
14115 draft = { parent };
14116 paintSidebar();
14117 }
14118 addRootBtn.addEventListener("click", () => {
14119 startDraft(0);
14120 });
14121 function fitToView(opts = {}) {
14122 const padding = opts.padding ?? 90;
14123 const animate = opts.animate ?? false;
14124 const r = stage.getBoundingClientRect();
14125 if (nodes.size === 0 || r.width === 0 || r.height === 0) {
14126 const cx2 = r.width / 2;
14127 const cy2 = r.height / 2;
14128 targetScale = 1;
14129 targetWorldX = cx2;
14130 targetWorldY = cy2;
14131 if (!animate) {
14132 world.x = cx2;
14133 world.y = cy2;
14134 world.scale.set(1);
14135 }
14136 return;
14137 }
14138 let minX = Infinity;
14139 let minY = Infinity;
14140 let maxX = -Infinity;
14141 let maxY = -Infinity;
14142 const LABEL_OVERHANG = 30;
14143 for (const n of nodes.values()) {
14144 const rad = n.radius;
14145 minX = Math.min(minX, n.tx - rad);
14146 minY = Math.min(minY, n.ty - rad);
14147 maxX = Math.max(maxX, n.tx + rad);
14148 maxY = Math.max(maxY, n.ty + rad + LABEL_OVERHANG);
14149 }
14150 const w = Math.max(1, maxX - minX);
14151 const h = Math.max(1, maxY - minY);
14152 const sx = (r.width - padding * 2) / w;
14153 const sy = (r.height - padding * 2) / h;
14154 const scale = Math.max(0.2, Math.min(1.5, Math.min(sx, sy)));
14155 const cx = (minX + maxX) / 2;
14156 const cy = (minY + maxY) / 2;
14157 const newWorldX = r.width / 2 - cx * scale;
14158 const newWorldY = r.height / 2 - cy * scale;
14159 targetScale = scale;
14160 targetWorldX = newWorldX;
14161 targetWorldY = newWorldY;
14162 if (!animate) {
14163 world.scale.set(scale);
14164 world.x = newWorldX;
14165 world.y = newWorldY;
14166 }
14167 }
14168 function recenterCamera() {
14169 if (focusId !== null) {
14170 const focused = nodes.get(focusId);
14171 const r = stage.getBoundingClientRect();
14172 if (focused && r.width > 0 && r.height > 0) {
14173 const half = POST_RING_RADIUS$1 + 70;
14174 const sx = r.width * 0.85 / (2 * half);
14175 const sy = r.height * 0.85 / (2 * half);
14176 const newScale = Math.max(
14177 0.5,
14178 Math.min(1.6, Math.min(sx, sy))
14179 );
14180 targetScale = newScale;
14181 targetWorldX = r.width / 2 - focused.x * newScale;
14182 targetWorldY = r.height / 2 - focused.y * newScale;
14183 return;
14184 }
14185 }
14186 fitToView({ animate: true });
14187 }
14188 recenterBtn.addEventListener("click", () => recenterCamera());
14189 app.canvas.addEventListener("click", (e) => {
14190 const now = performance.now();
14191 if (now - lastFocusChange < 250 || now - pixiInteractionAt < 250) {
14192 return;
14193 }
14194 if (panMovedDist > 4) {
14195 return;
14196 }
14197 const target = e.target;
14198 if (target === app.canvas && !dragNode && focusId !== null) {
14199 closeFocus();
14200 }
14201 });
14202 async function refreshCountsViaBulk() {
14203 if (terms.length === 0) {
14204 return;
14205 }
14206 const cfg = client.getConfig();
14207 const url = new URL(
14208 joinRestUrl(cfg.restRoot, "desktop-mode/v1/term-counts")
14209 );
14210 url.searchParams.set("taxonomy", "category");
14211 url.searchParams.set(
14212 "ids",
14213 terms.map((t) => t.id).join(",")
14214 );
14215 try {
14216 const response = await fetchShellJson$1(client, url.toString());
14217 const map = response.json;
14218 let dirty = false;
14219 terms = terms.map((t) => {
14220 const fresh = map[String(t.id)];
14221 if (typeof fresh === "number" && fresh !== t.count) {
14222 dirty = true;
14223 const node = nodes.get(t.id);
14224 if (node) {
14225 node.count = fresh;
14226 layoutChip(ensureChip(node), node);
14227 }
14228 return { ...t, count: fresh };
14229 }
14230 return t;
14231 });
14232 if (dirty) {
14233 buildTree();
14234 fitToView({ animate: true });
14235 }
14236 } catch {
14237 }
14238 }
14239 buildTree();
14240 paintSidebar();
14241 preSettlePhysics(80);
14242 raf = requestAnimationFrame(tick);
14243 void refreshCountsViaBulk();
14244 let currentMatches = [];
14245 let selectedIndex = 0;
14246 const repaintHighlight = () => {
14247 const items = searchResults.querySelectorAll(
14248 ".wpd-mindmap__search-result"
14249 );
14250 items.forEach((el, i) => {
14251 const active = i === selectedIndex;
14252 el.classList.toggle("is-active", active);
14253 if (active) {
14254 el.scrollIntoView({ block: "nearest" });
14255 }
14256 });
14257 };
14258 const selectMatch = (n) => {
14259 searchInput.value = "";
14260 searchResults.hidden = true;
14261 searchResults.replaceChildren();
14262 currentMatches = [];
14263 selectedIndex = 0;
14264 void focusNode(n.id);
14265 };
14266 const renderSearchResults = () => {
14267 const q = searchInput.value.trim().toLowerCase();
14268 if (q.length === 0) {
14269 searchResults.hidden = true;
14270 searchResults.replaceChildren();
14271 currentMatches = [];
14272 selectedIndex = 0;
14273 return;
14274 }
14275 currentMatches = Array.from(nodes.values()).filter((n) => n.name.toLowerCase().includes(q)).sort((a, b) => b.count - a.count).slice(0, 10);
14276 selectedIndex = 0;
14277 searchResults.replaceChildren();
14278 currentMatches.forEach((n, i) => {
14279 const li = document.createElement("li");
14280 const btn = document.createElement("button");
14281 btn.type = "button";
14282 btn.className = "wpd-mindmap__search-result";
14283 if (i === 0) {
14284 btn.classList.add("is-active");
14285 }
14286 const nameEl = document.createElement("span");
14287 nameEl.className = "wpd-mindmap__search-title";
14288 nameEl.textContent = n.name || `#${n.id}`;
14289 const countEl = document.createElement("span");
14290 countEl.className = "wpd-mindmap__search-meta";
14291 countEl.textContent = sprintf(
14292 /* translators: %d: number of posts assigned to a category. */
14293 __("%d posts"),
14294 n.count
14295 );
14296 btn.appendChild(nameEl);
14297 btn.appendChild(countEl);
14298 btn.addEventListener("mousedown", (ev) => {
14299 ev.preventDefault();
14300 selectMatch(n);
14301 });
14302 btn.addEventListener("mouseenter", () => {
14303 selectedIndex = i;
14304 repaintHighlight();
14305 });
14306 li.appendChild(btn);
14307 searchResults.appendChild(li);
14308 });
14309 searchResults.hidden = currentMatches.length === 0;
14310 };
14311 searchInput.addEventListener("input", renderSearchResults);
14312 searchInput.addEventListener("focus", renderSearchResults);
14313 searchInput.addEventListener("keydown", (ev) => {
14314 if (ev.key === "ArrowDown") {
14315 if (currentMatches.length === 0) {
14316 return;
14317 }
14318 ev.preventDefault();
14319 selectedIndex = Math.min(
14320 selectedIndex + 1,
14321 currentMatches.length - 1
14322 );
14323 repaintHighlight();
14324 } else if (ev.key === "ArrowUp") {
14325 if (currentMatches.length === 0) {
14326 return;
14327 }
14328 ev.preventDefault();
14329 selectedIndex = Math.max(selectedIndex - 1, 0);
14330 repaintHighlight();
14331 } else if (ev.key === "Enter") {
14332 if (currentMatches.length === 0) {
14333 return;
14334 }
14335 ev.preventDefault();
14336 selectMatch(currentMatches[selectedIndex]);
14337 } else if (ev.key === "Escape") {
14338 searchInput.value = "";
14339 searchResults.hidden = true;
14340 searchResults.replaceChildren();
14341 currentMatches = [];
14342 selectedIndex = 0;
14343 }
14344 });
14345 searchInput.addEventListener("blur", () => {
14346 setTimeout(() => {
14347 searchResults.hidden = true;
14348 }, 120);
14349 });
14350 const onDocClickSearch = (ev) => {
14351 if (!searchWrap.contains(ev.target)) {
14352 searchResults.hidden = true;
14353 }
14354 };
14355 document.addEventListener("click", onDocClickSearch);
14356 return () => {
14357 if (raf !== null) {
14358 cancelAnimationFrame(raf);
14359 raf = null;
14360 }
14361 if (settleTimer !== null) {
14362 window.clearTimeout(settleTimer);
14363 settleTimer = null;
14364 }
14365 ro.disconnect();
14366 stage.removeEventListener("wheel", onWheel);
14367 document.removeEventListener("click", onDocClickSearch);
14368 try {
14369 app.ticker?.stop();
14370 } catch {
14371 }
14372 try {
14373 app.canvas?.remove();
14374 } catch {
14375 }
14376 host.replaceChildren();
14377 host.classList.remove("wpd-mindmap");
14378 };
14379 }
14380 function nodeRadius(count, all) {
14381 const max = Math.max(1, ...all.map((t) => t.count));
14382 const ratio = Math.sqrt(count / max);
14383 return MIN_RADIUS + (MAX_RADIUS - MIN_RADIUS) * ratio;
14384 }
14385 function readAdminThemeHue$1() {
14386 try {
14387 const value = getComputedStyle(document.documentElement).getPropertyValue("--wp-admin-theme-color").trim();
14388 if (!value) {
14389 return 210;
14390 }
14391 const c = document.createElement("span");
14392 c.style.color = value;
14393 document.body.appendChild(c);
14394 const rgb = getComputedStyle(c).color;
14395 c.remove();
14396 const m = rgb.match(/\d+/g);
14397 if (!m || m.length < 3) {
14398 return 210;
14399 }
14400 return rgbToHue$1(
14401 parseInt(m[0], 10),
14402 parseInt(m[1], 10),
14403 parseInt(m[2], 10)
14404 );
14405 } catch {
14406 return 210;
14407 }
14408 }
14409 function rgbToHue$1(r, g, b) {
14410 const rn = r / 255;
14411 const gn = g / 255;
14412 const bn = b / 255;
14413 const max = Math.max(rn, gn, bn);
14414 const min = Math.min(rn, gn, bn);
14415 const d = max - min;
14416 if (d === 0) {
14417 return 210;
14418 }
14419 let h;
14420 switch (max) {
14421 case rn:
14422 h = (gn - bn) / d + (gn < bn ? 6 : 0);
14423 break;
14424 case gn:
14425 h = (bn - rn) / d + 2;
14426 break;
14427 default:
14428 h = (rn - gn) / d + 4;
14429 break;
14430 }
14431 return Math.round(h * 60);
14432 }
14433 function hslToInt$1(h, s, l) {
14434 const sn = s / 100;
14435 const ln = l / 100;
14436 const c = (1 - Math.abs(2 * ln - 1)) * sn;
14437 const hp = h / 60;
14438 const x = c * (1 - Math.abs(hp % 2 - 1));
14439 let r = 0;
14440 let g = 0;
14441 let b = 0;
14442 if (hp < 1) {
14443 r = c;
14444 g = x;
14445 } else if (hp < 2) {
14446 r = x;
14447 g = c;
14448 } else if (hp < 3) {
14449 g = c;
14450 b = x;
14451 } else if (hp < 4) {
14452 g = x;
14453 b = c;
14454 } else if (hp < 5) {
14455 r = x;
14456 b = c;
14457 } else {
14458 r = c;
14459 b = x;
14460 }
14461 const m = ln - c / 2;
14462 const ri = Math.round((r + m) * 255);
14463 const gi = Math.round((g + m) * 255);
14464 const bi = Math.round((b + m) * 255);
14465 return ri * 65536 + gi * 256 + bi;
14466 }
14467 function shadeColor(color, delta) {
14468 const r = Math.floor(color / 65536) % 256;
14469 const g = Math.floor(color / 256) % 256;
14470 const b = color % 256;
14471 const adj = (ch) => {
14472 return Math.round(ch * (1 + delta));
14473 };
14474 return adj(r) * 65536 + adj(g) * 256 + adj(b);
14475 }
14476 function stripTags$1(html2) {
14477 const tmp = document.createElement("div");
14478 tmp.innerHTML = html2;
14479 return tmp.textContent || tmp.innerText || "";
14480 }
14481 function showToast$1(title, err) {
14482 const reason = err instanceof Error ? err.message : String(err);
14483 const api = window.wp?.desktop;
14484 if (api && typeof api.showToast === "function") {
14485 api.showToast({
14486 message: `${title} ${reason}`.trim(),
14487 duration: 6e3
14488 });
14489 return;
14490 }
14491 console.error(title, err);
14492 }
14493 async function fetchShellJson$1(client, url) {
14494 const cfg = client.getConfig();
14495 const init = {
14496 method: "GET",
14497 credentials: "same-origin",
14498 headers: {
14499 "X-WP-Nonce": cfg.restNonce,
14500 Accept: "application/json"
14501 }
14502 };
14503 const response = await trackedFetch(url, init, {
14504 windowId: "desktop-mode-posts"
14505 });
14506 if (!response.ok) {
14507 throw new Error(`${response.status} ${response.statusText}`);
14508 }
14509 const json = await response.json();
14510 return { json, headers: response.headers };
14511 }
14512 const categoriesMindmap = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
14513 __proto__: null,
14514 mountCategoriesMindmap
14515 }, Symbol.toStringTag, { value: "Module" }));
14516 const POST_PER_PAGE = 10;
14517 const POST_RING_RADIUS = 170;
14518 const MIN_FONT_SIZE = 11;
14519 const MAX_FONT_SIZE = 28;
14520 const FONT_FAMILY = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
14521 const CHIP_TEXT_RES = 3;
14522 const CHIP_NAME_MAX_CHARS = 22;
14523 const POST_TITLE_MAX_CHARS = 22;
14524 const CHIP_PAD_X = 11;
14525 const CHIP_PAD_Y = 6;
14526 const CHIP_GAP_HASH = 4;
14527 const CHIP_GAP_COUNT = 8;
14528 const SPIRAL_PADDING = 14;
14529 const SPOTLIGHT_RADIUS = POST_RING_RADIUS + 130;
14530 async function mountTagsCloud(host, client) {
14531 const api = window.wp?.desktop;
14532 if (!api || typeof api.loadModules !== "function") {
14533 host.textContent = __("Tag cloud unavailable: shell modules API missing.");
14534 return () => {
14535 };
14536 }
14537 try {
14538 await api.loadModules(["pixijs"]);
14539 } catch {
14540 host.textContent = __("Tag cloud unavailable.");
14541 return () => {
14542 };
14543 }
14544 const pixiMaybe = window.PIXI;
14545 if (!pixiMaybe) {
14546 host.textContent = __("Tag cloud unavailable.");
14547 return () => {
14548 };
14549 }
14550 const pixi = pixiMaybe;
14551 host.replaceChildren();
14552 host.classList.add("wpd-tagcloud");
14553 const toolbar = document.createElement("div");
14554 toolbar.className = "wpd-tagcloud__toolbar";
14555 const addTagBtn = document.createElement("button");
14556 addTagBtn.type = "button";
14557 addTagBtn.className = "wpd-tagcloud__btn wpd-tagcloud__btn--primary";
14558 addTagBtn.innerHTML = '<span class="dashicons dashicons-plus" aria-hidden="true"></span>' + __("Add tag");
14559 const recenterBtn = document.createElement("button");
14560 recenterBtn.type = "button";
14561 recenterBtn.className = "wpd-tagcloud__btn";
14562 recenterBtn.innerHTML = '<span class="dashicons dashicons-image-rotate" aria-hidden="true"></span>' + __("Recenter");
14563 const reflowBtn = document.createElement("button");
14564 reflowBtn.type = "button";
14565 reflowBtn.className = "wpd-tagcloud__btn";
14566 reflowBtn.innerHTML = '<span class="dashicons dashicons-grid-view" aria-hidden="true"></span>' + __("Reflow");
14567 reflowBtn.title = __(
14568 "Recompute the chip layout from scratch — discards manual repositioning."
14569 );
14570 const searchWrap = document.createElement("div");
14571 searchWrap.className = "wpd-tagcloud__search";
14572 const searchInput = document.createElement("input");
14573 searchInput.type = "search";
14574 searchInput.className = "wpd-tagcloud__search-input";
14575 searchInput.placeholder = __("Search tags…");
14576 searchInput.setAttribute(
14577 "aria-label",
14578 __("Search tags in the cloud")
14579 );
14580 searchWrap.appendChild(searchInput);
14581 const searchResults = document.createElement("ul");
14582 searchResults.className = "wpd-tagcloud__search-results";
14583 searchResults.hidden = true;
14584 searchWrap.appendChild(searchResults);
14585 const hint = document.createElement("span");
14586 hint.className = "wpd-tagcloud__hint";
14587 hint.textContent = __(
14588 "Click a tag to focus + edit · drag to reposition · wheel to zoom"
14589 );
14590 toolbar.appendChild(addTagBtn);
14591 toolbar.appendChild(recenterBtn);
14592 toolbar.appendChild(reflowBtn);
14593 toolbar.appendChild(searchWrap);
14594 toolbar.appendChild(hint);
14595 host.appendChild(toolbar);
14596 const layout = document.createElement("div");
14597 layout.className = "wpd-tagcloud__layout";
14598 host.appendChild(layout);
14599 const stage = document.createElement("div");
14600 stage.className = "wpd-tagcloud__stage";
14601 stage.classList.add("is-loading");
14602 layout.appendChild(stage);
14603 const sidebar = document.createElement("aside");
14604 sidebar.className = "wpd-tagcloud__sidebar";
14605 layout.appendChild(sidebar);
14606 const app = new pixi.Application();
14607 await app.init({
14608 resizeTo: stage,
14609 backgroundAlpha: 0,
14610 antialias: true,
14611 autoDensity: true,
14612 resolution: Math.min(window.devicePixelRatio || 1, 2)
14613 });
14614 stage.appendChild(app.canvas);
14615 app.canvas.classList.add("wpd-tagcloud__canvas");
14616 const world = new pixi.Container();
14617 world.x = stage.clientWidth / 2;
14618 world.y = stage.clientHeight / 2;
14619 app.stage.addChild(world);
14620 const chipLayer = new pixi.Container();
14621 const postEdgeLayer = new pixi.Container();
14622 const postLayer = new pixi.Container();
14623 const postChipLayer = new pixi.Container();
14624 world.addChild(postEdgeLayer);
14625 world.addChild(chipLayer);
14626 world.addChild(postLayer);
14627 world.addChild(postChipLayer);
14628 const postEdgeGfx = new pixi.Graphics();
14629 postEdgeLayer.addChild(postEdgeGfx);
14630 const pager = new pixi.Container();
14631 pager.eventMode = "passive";
14632 pager.visible = false;
14633 postLayer.addChild(pager);
14634 const pagerPrev = new pixi.Graphics();
14635 const pagerNext = new pixi.Graphics();
14636 const pagerLabel = new pixi.Text({
14637 text: "1 / 1",
14638 style: {
14639 fill: 5265246,
14640 fontSize: 12,
14641 fontFamily: FONT_FAMILY,
14642 fontWeight: "600"
14643 }
14644 });
14645 pagerLabel.anchor.set(0.5);
14646 pagerPrev.eventMode = "static";
14647 pagerPrev.cursor = "pointer";
14648 pagerNext.eventMode = "static";
14649 pagerNext.cursor = "pointer";
14650 pagerPrev.hitArea = new pixi.Circle(0, 0, 16);
14651 pagerNext.hitArea = new pixi.Circle(0, 0, 16);
14652 pager.addChild(pagerPrev);
14653 pager.addChild(pagerLabel);
14654 pager.addChild(pagerNext);
14655 const stopBubble = (e) => {
14656 e.stopPropagation?.();
14657 pixiInteractionAt = performance.now();
14658 };
14659 pagerPrev.on("pointerdown", stopBubble);
14660 pagerNext.on("pointerdown", stopBubble);
14661 pagerPrev.on("pointertap", (e) => {
14662 stopBubble(e);
14663 lastFocusChange = performance.now();
14664 if (focusPage <= 1) {
14665 return;
14666 }
14667 focusPage--;
14668 void loadPostsForFocus();
14669 });
14670 pagerNext.on("pointertap", (e) => {
14671 stopBubble(e);
14672 lastFocusChange = performance.now();
14673 if (focusPage >= focusTotalPages) {
14674 return;
14675 }
14676 focusPage++;
14677 void loadPostsForFocus();
14678 });
14679 const tags = /* @__PURE__ */ new Map();
14680 const postChips = /* @__PURE__ */ new Map();
14681 const postNodes = /* @__PURE__ */ new Map();
14682 let focusId = null;
14683 let focusPage = 1;
14684 let focusTotalPages = 1;
14685 let loadSeq = 0;
14686 let pixiInteractionAt = 0;
14687 let dragChip = null;
14688 let dragOffset = { x: 0, y: 0 };
14689 let dragStart = null;
14690 let panActive = false;
14691 let panStart = null;
14692 let panMovedDist = 0;
14693 let raf = null;
14694 let lastTick = performance.now();
14695 let targetScale = world.scale.x;
14696 let targetWorldX = world.x;
14697 let targetWorldY = world.y;
14698 let nudgeAwayFrom = null;
14699 let prevView = null;
14700 let lastFocusChange = 0;
14701 let draft = null;
14702 let terms = [];
14703 const positionsKey = computePositionsKey();
14704 const persistedPositions = readPersistedPositions(positionsKey);
14705 let cooccurrenceMap = /* @__PURE__ */ new Map();
14706 const themeHue = readAdminThemeHue();
14707 try {
14708 const all = [];
14709 let page = 1;
14710 while (page <= 5) {
14711 const res = await client.fetchTerms("tags", { page, perPage: 100 });
14712 all.push(...res.items);
14713 if (page >= res.totalPages) {
14714 break;
14715 }
14716 page++;
14717 }
14718 terms = all;
14719 } catch (err) {
14720 showToast(__("Couldn’t load tags:"), err);
14721 }
14722 const showError = (title, err) => showToast(title, err);
14723 function buildCloud() {
14724 const liveIds = new Set(terms.map((t) => t.id));
14725 for (const [id, box] of tags) {
14726 if (!liveIds.has(id)) {
14727 chipLayer.removeChild(box.chip.container);
14728 box.chip.container.destroy({ children: true });
14729 tags.delete(id);
14730 }
14731 }
14732 const maxCount = Math.max(1, ...terms.map((t) => t.count));
14733 const fresh = [];
14734 for (const term of terms) {
14735 const fontSize = fontSizeFor(term.count, maxCount);
14736 const hue = tagHue(term.slug || term.name, themeHue);
14737 const rotation = tagRotation(term.slug || term.name);
14738 const existing = tags.get(term.id);
14739 if (existing) {
14740 existing.name = term.name;
14741 existing.slug = term.slug;
14742 existing.description = term.description;
14743 existing.count = term.count;
14744 existing.fontSize = fontSize;
14745 existing.hue = hue;
14746 existing.rotation = rotation;
14747 layoutChip(existing);
14748 } else {
14749 const chip = createTagChip(pixi, chipLayer, term, fontSize, hue);
14750 const persisted = persistedPositions.get(term.id);
14751 const box = {
14752 id: term.id,
14753 name: term.name,
14754 slug: term.slug,
14755 description: term.description,
14756 count: term.count,
14757 fontSize,
14758 hue,
14759 rotation,
14760 x: persisted ? persisted.x : 0,
14761 y: persisted ? persisted.y : 0,
14762 tx: persisted ? persisted.x : 0,
14763 ty: persisted ? persisted.y : 0,
14764 width: 0,
14765 height: 0,
14766 chip
14767 };
14768 tags.set(term.id, box);
14769 layoutChip(box);
14770 wireChipPointer(box);
14771 if (!persisted) {
14772 fresh.push(box);
14773 }
14774 }
14775 }
14776 const placed = [];
14777 const placedById = /* @__PURE__ */ new Map();
14778 for (const box of tags.values()) {
14779 if (!fresh.includes(box)) {
14780 placed.push({
14781 x: box.tx - box.width / 2,
14782 y: box.ty - box.height / 2,
14783 w: box.width,
14784 h: box.height
14785 });
14786 placedById.set(box.id, { x: box.tx, y: box.ty });
14787 }
14788 }
14789 fresh.sort((a, b) => b.count - a.count);
14790 packBoxesWithClusters(fresh, placed, placedById, cooccurrenceMap);
14791 for (const box of fresh) {
14792 box.x = box.tx;
14793 box.y = box.ty;
14794 }
14795 }
14796 function wireChipPointer(box) {
14797 const c = box.chip.container;
14798 c.on("pointerdown", (e) => {
14799 const ev = e;
14800 ev.stopPropagation?.();
14801 pixiInteractionAt = performance.now();
14802 dragChip = box;
14803 dragStart = { x: ev.global.x, y: ev.global.y };
14804 const local = stageToWorld({ x: ev.global.x, y: ev.global.y });
14805 dragOffset = { x: box.x - local.x, y: box.y - local.y };
14806 });
14807 c.on("pointerover", () => {
14808 box.chip.cachedHover = true;
14809 paintChip(box);
14810 });
14811 c.on("pointerout", () => {
14812 box.chip.cachedHover = false;
14813 paintChip(box);
14814 });
14815 }
14816 function layoutChip(box) {
14817 const chip = box.chip;
14818 const displayName = truncateChipName(box.name);
14819 const countStr = String(box.count);
14820 if (chip.nameText.text !== displayName) {
14821 chip.nameText.text = displayName;
14822 }
14823 if (chip.countText.text !== countStr) {
14824 chip.countText.text = countStr;
14825 }
14826 chip.nameText.style.fontSize = box.fontSize;
14827 chip.hashText.style.fontSize = box.fontSize;
14828 chip.countText.style.fontSize = Math.max(
14829 10,
14830 Math.round(box.fontSize * 0.55)
14831 );
14832 chip.cachedName = displayName;
14833 chip.cachedCount = box.count;
14834 chip.cachedHue = box.hue;
14835 const hashW = chip.hashText.width;
14836 const nameW = chip.nameText.width;
14837 const nameH = chip.nameText.height;
14838 const countW = chip.countText.width;
14839 const countH = chip.countText.height;
14840 const countBadgeW = Math.max(18, countW + 10);
14841 const countBadgeH = Math.max(14, countH + 4);
14842 const totalW = CHIP_PAD_X + hashW + CHIP_GAP_HASH + nameW + CHIP_GAP_COUNT + countBadgeW + CHIP_PAD_X;
14843 const totalH = Math.max(nameH, countBadgeH) + CHIP_PAD_Y * 2;
14844 box.width = totalW;
14845 box.height = totalH;
14846 paintChip(box);
14847 }
14848 function paintChip(box) {
14849 const chip = box.chip;
14850 const focused = focusId === box.id;
14851 chip.cachedFocused = focused;
14852 const totalW = box.width;
14853 const totalH = box.height;
14854 const left = -totalW / 2;
14855 const top = -totalH / 2;
14856 const radius = totalH / 2;
14857 let fillBg;
14858 if (focused) {
14859 fillBg = hslToInt(box.hue, 70, 48);
14860 } else if (chip.cachedHover) {
14861 fillBg = hslToInt(box.hue, 70, 92);
14862 } else {
14863 fillBg = hslToInt(box.hue, 60, 95);
14864 }
14865 const borderColor = focused ? hslToInt(box.hue, 70, 38) : hslToInt(box.hue, 50, 70);
14866 const textColor = focused ? 16777215 : 1909543;
14867 const hashColor = focused ? 16777215 : hslToInt(box.hue, 65, 42);
14868 const countBg = focused ? hslToInt(box.hue, 80, 30) : hslToInt(box.hue, 70, 50);
14869 chip.shadow.clear();
14870 chip.shadow.roundRect(
14871 left - 1,
14872 top + 3,
14873 totalW + 2,
14874 totalH + 2,
14875 radius + 1
14876 );
14877 let shadowAlpha = 0.1;
14878 if (focused) {
14879 shadowAlpha = 0.18;
14880 } else if (chip.cachedHover) {
14881 shadowAlpha = 0.16;
14882 }
14883 chip.shadow.fill({
14884 color: 0,
14885 alpha: shadowAlpha
14886 });
14887 chip.bg.clear();
14888 chip.bg.roundRect(left, top, totalW, totalH, radius);
14889 chip.bg.fill(fillBg);
14890 chip.bg.stroke({
14891 color: borderColor,
14892 width: focused ? 2 : 1.25,
14893 alpha: focused ? 1 : 0.85
14894 });
14895 const hashW = chip.hashText.width;
14896 const nameW = chip.nameText.width;
14897 const nameH = chip.nameText.height;
14898 const countW = chip.countText.width;
14899 const countH = chip.countText.height;
14900 const countBadgeW = Math.max(18, countW + 10);
14901 const countBadgeH = Math.max(14, countH + 4);
14902 chip.hashText.x = left + CHIP_PAD_X;
14903 chip.hashText.y = (totalH - nameH) / 2 + top;
14904 chip.hashText.style.fill = hashColor;
14905 chip.nameText.x = left + CHIP_PAD_X + hashW + CHIP_GAP_HASH;
14906 chip.nameText.y = (totalH - nameH) / 2 + top;
14907 chip.nameText.style.fill = textColor;
14908 const badgeX = left + CHIP_PAD_X + hashW + CHIP_GAP_HASH + nameW + CHIP_GAP_COUNT;
14909 const badgeY = (totalH - countBadgeH) / 2 + top;
14910 chip.bg.roundRect(
14911 badgeX,
14912 badgeY,
14913 countBadgeW,
14914 countBadgeH,
14915 countBadgeH / 2
14916 );
14917 chip.bg.fill(countBg);
14918 chip.countText.x = badgeX + (countBadgeW - countW) / 2;
14919 chip.countText.y = badgeY + (countBadgeH - countH) / 2;
14920 chip.countText.style.fill = 16777215;
14921 }
14922 function findSpiralSlot(w, h, placed, anchorX = 0, anchorY = 0) {
14923 if (placed.length === 0) {
14924 return { x: anchorX, y: anchorY };
14925 }
14926 const padding = SPIRAL_PADDING;
14927 {
14928 const aabb = {
14929 x: anchorX - w / 2 - padding,
14930 y: anchorY - h / 2 - padding,
14931 w: w + padding * 2,
14932 h: h + padding * 2
14933 };
14934 let overlap = false;
14935 for (const p of placed) {
14936 if (aabbIntersect(aabb, p)) {
14937 overlap = true;
14938 break;
14939 }
14940 }
14941 if (!overlap) {
14942 return { x: anchorX, y: anchorY };
14943 }
14944 }
14945 let theta = 0;
14946 const maxIter = 1e4;
14947 for (let i = 0; i < maxIter; i++) {
14948 theta += 0.18;
14949 const r = theta * 5;
14950 const cx = anchorX + r * Math.cos(theta);
14951 const cy = anchorY + r * Math.sin(theta) * 0.7;
14952 const aabb = {
14953 x: cx - w / 2 - padding,
14954 y: cy - h / 2 - padding,
14955 w: w + padding * 2,
14956 h: h + padding * 2
14957 };
14958 let overlap = false;
14959 for (const p of placed) {
14960 if (aabbIntersect(aabb, p)) {
14961 overlap = true;
14962 break;
14963 }
14964 }
14965 if (!overlap) {
14966 return { x: cx, y: cy };
14967 }
14968 }
14969 return {
14970 x: anchorX,
14971 y: anchorY + (placed.length + 1) * (h + padding)
14972 };
14973 }
14974 function packBoxesWithClusters(boxesInOrder, placed, placedById, cooccurrence) {
14975 let clusterCounter = 0;
14976 const allocateClusterAnchor = () => {
14977 const idx = clusterCounter++;
14978 if (idx === 0) {
14979 return { x: 0, y: 0 };
14980 }
14981 const theta = idx * 2.4;
14982 const radius = 120 + idx * 70;
14983 return {
14984 x: radius * Math.cos(theta),
14985 y: radius * Math.sin(theta) * 0.8
14986 };
14987 };
14988 for (const box of boxesInOrder) {
14989 let anchorX = 0;
14990 let anchorY = 0;
14991 let usedCentroid = false;
14992 const neighbors = cooccurrence.get(box.id);
14993 if (neighbors && neighbors.length > 0) {
14994 let sumX = 0;
14995 let sumY = 0;
14996 let sumW = 0;
14997 for (const n of neighbors) {
14998 const pos = placedById.get(n.id);
14999 if (!pos) {
15000 continue;
15001 }
15002 sumX += pos.x * n.shared;
15003 sumY += pos.y * n.shared;
15004 sumW += n.shared;
15005 }
15006 if (sumW > 0) {
15007 anchorX = sumX / sumW;
15008 anchorY = sumY / sumW;
15009 usedCentroid = true;
15010 }
15011 }
15012 if (!usedCentroid) {
15013 const anchor = allocateClusterAnchor();
15014 anchorX = anchor.x;
15015 anchorY = anchor.y;
15016 }
15017 const slot = findSpiralSlot(
15018 box.width,
15019 box.height,
15020 placed,
15021 anchorX,
15022 anchorY
15023 );
15024 box.tx = slot.x;
15025 box.ty = slot.y;
15026 placedById.set(box.id, { x: slot.x, y: slot.y });
15027 placed.push({
15028 x: slot.x - box.width / 2,
15029 y: slot.y - box.height / 2,
15030 w: box.width,
15031 h: box.height
15032 });
15033 }
15034 }
15035 function syncChipPositions() {
15036 const chipCounterScale = 1 / Math.max(0.01, world.scale.x);
15037 const anyFocus = focusId !== null;
15038 for (const box of tags.values()) {
15039 const c = box.chip.container;
15040 c.x = box.x;
15041 c.y = box.y;
15042 const counter = Math.max(1, chipCounterScale);
15043 c.scale.set(counter);
15044 c.rotation = box.rotation;
15045 const focused = focusId === box.id;
15046 const targetAlpha = !anyFocus || focused ? 1 : 0.32;
15047 if (Math.abs(c.alpha - targetAlpha) > 5e-3) {
15048 c.alpha += (targetAlpha - c.alpha) * 0.18;
15049 } else {
15050 c.alpha = targetAlpha;
15051 }
15052 }
15053 for (const post of postNodes.values()) {
15054 const chip = postChips.get(post.id);
15055 if (!chip) {
15056 continue;
15057 }
15058 chip.container.x = post.x;
15059 chip.container.y = post.y;
15060 chip.container.scale.set(chipCounterScale);
15061 if (chip.container.alpha < 1) {
15062 chip.container.alpha = Math.min(
15063 1,
15064 chip.container.alpha + 0.18
15065 );
15066 }
15067 }
15068 }
15069 function tick() {
15070 const now = performance.now();
15071 const dt = Math.min(50, now - lastTick);
15072 lastTick = now;
15073 const ZOOM_EASE = 0.22;
15074 const ds = targetScale - world.scale.x;
15075 const dwx = targetWorldX - world.x;
15076 const dwy = targetWorldY - world.y;
15077 if (Math.abs(ds) > 5e-4 || Math.abs(dwx) > 0.5 || Math.abs(dwy) > 0.5) {
15078 world.scale.set(world.scale.x + ds * ZOOM_EASE);
15079 world.x += dwx * ZOOM_EASE;
15080 world.y += dwy * ZOOM_EASE;
15081 }
15082 for (const box of tags.values()) {
15083 if (box === dragChip) {
15084 continue;
15085 }
15086 let tx = box.tx;
15087 let ty = box.ty;
15088 if (nudgeAwayFrom && box.id !== focusId) {
15089 const dx = box.tx - nudgeAwayFrom.x;
15090 const dy = box.ty - nudgeAwayFrom.y;
15091 const d = Math.sqrt(dx * dx + dy * dy) || 1;
15092 const limit = nudgeAwayFrom.radius + Math.max(box.width, box.height) / 2;
15093 if (d < limit) {
15094 const push = limit + 12;
15095 tx = nudgeAwayFrom.x + dx / d * push;
15096 ty = nudgeAwayFrom.y + dy / d * push;
15097 }
15098 }
15099 const ease = 1 - Math.exp(-dt * 0.012);
15100 box.x += (tx - box.x) * ease;
15101 box.y += (ty - box.y) * ease;
15102 }
15103 for (const p of postNodes.values()) {
15104 p.x += (p.tx - p.x) * 0.18;
15105 p.y += (p.ty - p.y) * 0.18;
15106 p.gfx.x = p.x;
15107 p.gfx.y = p.y;
15108 }
15109 drawPostEdges();
15110 syncChipPositions();
15111 raf = requestAnimationFrame(tick);
15112 }
15113 function drawPostEdges() {
15114 postEdgeGfx.clear();
15115 if (focusId === null) {
15116 return;
15117 }
15118 const center = tags.get(focusId);
15119 if (!center) {
15120 return;
15121 }
15122 for (const post of postNodes.values()) {
15123 postEdgeGfx.moveTo(center.x, center.y);
15124 postEdgeGfx.lineTo(post.x, post.y);
15125 postEdgeGfx.stroke({
15126 color: hslToInt(center.hue, 60, 50),
15127 width: 1,
15128 alpha: 0.35
15129 });
15130 }
15131 }
15132 function stageToWorld(global) {
15133 return {
15134 x: (global.x - world.x) / world.scale.x,
15135 y: (global.y - world.y) / world.scale.y
15136 };
15137 }
15138 function onStagePointerDown(e) {
15139 const ev = e;
15140 panActive = true;
15141 panStart = { x: ev.global.x, y: ev.global.y };
15142 panMovedDist = 0;
15143 }
15144 function onStagePointerMove(e) {
15145 const ev = e;
15146 if (dragChip) {
15147 const cursorWorld = stageToWorld(ev.global);
15148 const nx = cursorWorld.x + dragOffset.x;
15149 const ny = cursorWorld.y + dragOffset.y;
15150 dragChip.x = nx;
15151 dragChip.y = ny;
15152 dragChip.tx = nx;
15153 dragChip.ty = ny;
15154 return;
15155 }
15156 if (panActive && panStart) {
15157 const dx = ev.global.x - panStart.x;
15158 const dy = ev.global.y - panStart.y;
15159 world.x += dx;
15160 world.y += dy;
15161 targetWorldX += dx;
15162 targetWorldY += dy;
15163 panMovedDist += Math.sqrt(dx * dx + dy * dy);
15164 panStart = { x: ev.global.x, y: ev.global.y };
15165 }
15166 }
15167 function onStagePointerUp(e) {
15168 if (dragChip) {
15169 const box = dragChip;
15170 const startPos = dragStart;
15171 dragChip = null;
15172 dragStart = null;
15173 let movement = Infinity;
15174 const ev = e;
15175 if (startPos && ev && ev.global) {
15176 const dx = ev.global.x - startPos.x;
15177 const dy = ev.global.y - startPos.y;
15178 movement = Math.sqrt(dx * dx + dy * dy);
15179 }
15180 if (movement < 3) {
15181 void focusTag(box.id);
15182 } else {
15183 persistedPositions.set(box.id, { x: box.tx, y: box.ty });
15184 writePersistedPositions(positionsKey, persistedPositions);
15185 }
15186 }
15187 panActive = false;
15188 panStart = null;
15189 }
15190 app.stage.eventMode = "static";
15191 app.stage.hitArea = new pixi.Rectangle(
15192 0,
15193 0,
15194 stage.clientWidth,
15195 stage.clientHeight
15196 );
15197 app.stage.on("pointerdown", onStagePointerDown);
15198 app.stage.on("pointermove", onStagePointerMove);
15199 app.stage.on("pointerup", (e) => onStagePointerUp(e));
15200 app.stage.on("pointerupoutside", (e) => onStagePointerUp(e));
15201 function onWheel(e) {
15202 e.preventDefault();
15203 const SENSITIVITY = 8e-4;
15204 const factor = Math.exp(-e.deltaY * SENSITIVITY);
15205 const prev = targetScale;
15206 const next = Math.max(0.3, Math.min(2.5, prev * factor));
15207 if (Math.abs(next - prev) < 5e-4) {
15208 return;
15209 }
15210 const r = stage.getBoundingClientRect();
15211 const sx = e.clientX - r.left;
15212 const sy = e.clientY - r.top;
15213 const wx = (sx - targetWorldX) / prev;
15214 const wy = (sy - targetWorldY) / prev;
15215 targetScale = next;
15216 targetWorldX = sx - wx * next;
15217 targetWorldY = sy - wy * next;
15218 }
15219 stage.addEventListener("wheel", onWheel, { passive: false });
15220 let firstFitDone = false;
15221 let settledW = 0;
15222 let settledH = 0;
15223 const SETTLE_THRESHOLD_PX = 24;
15224 const SETTLE_DEBOUNCE_MS = 80;
15225 let settleTimer = null;
15226 function onResize() {
15227 const r = stage.getBoundingClientRect();
15228 app.renderer.resize(r.width, r.height);
15229 app.stage.hitArea = new pixi.Rectangle(0, 0, r.width, r.height);
15230 if (!firstFitDone && r.width > 0 && r.height > 0) {
15231 firstFitDone = true;
15232 settledW = r.width;
15233 settledH = r.height;
15234 fitToView();
15235 stage.classList.remove("is-loading");
15236 }
15237 if (settleTimer !== null) {
15238 window.clearTimeout(settleTimer);
15239 }
15240 settleTimer = window.setTimeout(() => {
15241 settleTimer = null;
15242 const cur = stage.getBoundingClientRect();
15243 const dw = Math.abs(cur.width - settledW);
15244 const dh = Math.abs(cur.height - settledH);
15245 if (dw >= SETTLE_THRESHOLD_PX || dh >= SETTLE_THRESHOLD_PX) {
15246 settledW = cur.width;
15247 settledH = cur.height;
15248 recenterCamera();
15249 }
15250 }, SETTLE_DEBOUNCE_MS);
15251 app.render();
15252 }
15253 const ro = new ResizeObserver(onResize);
15254 ro.observe(stage);
15255 async function focusTag(id) {
15256 if (focusId === id) {
15257 closeFocus();
15258 return;
15259 }
15260 const wasFocused = focusId !== null;
15261 focusId = id;
15262 focusPage = 1;
15263 lastFocusChange = performance.now();
15264 const focused = tags.get(id);
15265 if (focused) {
15266 if (!wasFocused) {
15267 prevView = {
15268 scale: targetScale,
15269 x: targetWorldX,
15270 y: targetWorldY
15271 };
15272 }
15273 const r = stage.getBoundingClientRect();
15274 if (r.width > 0 && r.height > 0) {
15275 const half = POST_RING_RADIUS + 70;
15276 const sx = r.width * 0.85 / (2 * half);
15277 const sy = r.height * 0.85 / (2 * half);
15278 const newScale = Math.max(
15279 0.5,
15280 Math.min(1.6, Math.min(sx, sy))
15281 );
15282 targetScale = newScale;
15283 targetWorldX = r.width / 2 - focused.x * newScale;
15284 targetWorldY = r.height / 2 - focused.y * newScale;
15285 }
15286 nudgeAwayFrom = {
15287 x: focused.x,
15288 y: focused.y,
15289 radius: SPOTLIGHT_RADIUS
15290 };
15291 }
15292 for (const box of tags.values()) {
15293 paintChip(box);
15294 }
15295 paintSidebar();
15296 await loadPostsForFocus();
15297 }
15298 function closeFocus() {
15299 focusId = null;
15300 lastFocusChange = performance.now();
15301 loadSeq++;
15302 nudgeAwayFrom = null;
15303 if (prevView) {
15304 targetScale = prevView.scale;
15305 targetWorldX = prevView.x;
15306 targetWorldY = prevView.y;
15307 prevView = null;
15308 }
15309 paintSidebar();
15310 clearPosts();
15311 for (const box of tags.values()) {
15312 paintChip(box);
15313 }
15314 }
15315 function clearPosts() {
15316 for (const post of postNodes.values()) {
15317 postLayer.removeChild(post.gfx);
15318 post.gfx.destroy();
15319 }
15320 postNodes.clear();
15321 for (const chip of postChips.values()) {
15322 postChipLayer.removeChild(chip.container);
15323 chip.container.destroy({ children: true });
15324 }
15325 postChips.clear();
15326 postEdgeGfx.clear();
15327 pager.visible = false;
15328 }
15329 function ensurePostChip(post) {
15330 const existing = postChips.get(post.id);
15331 if (existing) {
15332 return existing;
15333 }
15334 const container = new pixi.Container();
15335 container.eventMode = "static";
15336 container.cursor = "pointer";
15337 container.alpha = 0;
15338 const bg = new pixi.Graphics();
15339 container.addChild(bg);
15340 const dot = new pixi.Graphics();
15341 container.addChild(dot);
15342 const titleText = new pixi.Text({
15343 text: post.title,
15344 style: {
15345 fill: 1909543,
15346 fontSize: 12,
15347 fontFamily: FONT_FAMILY,
15348 fontWeight: "500"
15349 },
15350 resolution: CHIP_TEXT_RES
15351 });
15352 container.addChild(titleText);
15353 const chip = {
15354 container,
15355 bg,
15356 dot,
15357 titleText,
15358 width: 0,
15359 height: 0,
15360 cachedTitle: "",
15361 cachedHover: false
15362 };
15363 postChips.set(post.id, chip);
15364 postChipLayer.addChild(container);
15365 container.on("pointerdown", (e) => {
15366 e.stopPropagation?.();
15367 pixiInteractionAt = performance.now();
15368 });
15369 container.on("pointertap", () => {
15370 openInPostsTab(post.id, post.editUrl, post.title);
15371 closeFocus();
15372 });
15373 container.on("pointerover", () => {
15374 chip.cachedHover = true;
15375 layoutPostChip(chip, post);
15376 });
15377 container.on("pointerout", () => {
15378 chip.cachedHover = false;
15379 layoutPostChip(chip, post);
15380 });
15381 layoutPostChip(chip, post);
15382 return chip;
15383 }
15384 function layoutPostChip(chip, post) {
15385 const displayTitle = post.title.length > POST_TITLE_MAX_CHARS ? post.title.slice(0, POST_TITLE_MAX_CHARS - 1) + "…" : post.title;
15386 if (chip.titleText.text !== displayTitle) {
15387 chip.titleText.text = displayTitle;
15388 }
15389 chip.cachedTitle = displayTitle;
15390 const padX = 9;
15391 const padY = 3;
15392 const dotR = 4;
15393 const gap = 6;
15394 const titleW = chip.titleText.width;
15395 const titleH = chip.titleText.height;
15396 const totalW = padX + dotR * 2 + gap + titleW + padX;
15397 const totalH = Math.max(titleH, dotR * 2) + padY * 2;
15398 chip.width = totalW;
15399 chip.height = totalH;
15400 const left = -totalW / 2;
15401 const top = -totalH / 2;
15402 chip.bg.clear();
15403 chip.bg.roundRect(left, top, totalW, totalH, totalH / 2);
15404 if (chip.cachedHover) {
15405 chip.bg.fill({ color: 16777215, alpha: 1 });
15406 chip.bg.stroke({
15407 color: post.tone,
15408 width: 1.5,
15409 alpha: 1
15410 });
15411 } else {
15412 chip.bg.fill({ color: 16777215, alpha: 0.95 });
15413 chip.bg.stroke({
15414 color: 0,
15415 width: 1,
15416 alpha: 0.12
15417 });
15418 }
15419 chip.dot.clear();
15420 chip.dot.circle(left + padX + dotR, 0, dotR);
15421 chip.dot.fill({ color: post.tone, alpha: 0.85 });
15422 chip.dot.stroke({ color: 16777215, width: 1 });
15423 chip.titleText.x = left + padX + dotR * 2 + gap;
15424 chip.titleText.y = -titleH / 2;
15425 }
15426 const POSTS_CACHE_TTL_MS = 6e4;
15427 const postsCache = /* @__PURE__ */ new Map();
15428 function applyPostsResult(entry, focusedTagId) {
15429 focusTotalPages = entry.totalPages;
15430 if (Number.isFinite(entry.realTotal)) {
15431 const box = tags.get(focusedTagId);
15432 if (box && box.count !== entry.realTotal) {
15433 box.count = entry.realTotal;
15434 terms = terms.map(
15435 (t) => t.id === box.id ? { ...t, count: entry.realTotal } : t
15436 );
15437 layoutChip(box);
15438 }
15439 }
15440 renderPosts(entry.items);
15441 }
15442 async function loadPostsForFocus() {
15443 if (focusId === null) {
15444 return;
15445 }
15446 const mySeq = ++loadSeq;
15447 const myFocusId = focusId;
15448 const cacheKey2 = `${focusId}:${focusPage}`;
15449 const cached = postsCache.get(cacheKey2);
15450 if (cached && performance.now() - cached.fetchedAt < POSTS_CACHE_TTL_MS) {
15451 applyPostsResult(cached, myFocusId);
15452 return;
15453 }
15454 const cfg = client.getConfig();
15455 const url = new URL(cfg.postsUrl);
15456 url.searchParams.set("tags", String(focusId));
15457 url.searchParams.set("per_page", String(POST_PER_PAGE));
15458 url.searchParams.set("page", String(focusPage));
15459 url.searchParams.set("status", "any");
15460 url.searchParams.set("_fields", "id,title,status");
15461 try {
15462 const response = await fetchShellJson(client, url.toString());
15463 if (mySeq !== loadSeq || focusId !== myFocusId) {
15464 return;
15465 }
15466 const raw = response.json ?? [];
15467 const totalPages = Math.max(
15468 1,
15469 parseInt(response.headers.get("X-WP-TotalPages") ?? "1", 10) || 1
15470 );
15471 const realTotalParsed = parseInt(response.headers.get("X-WP-Total") ?? "", 10);
15472 const realTotal = Number.isFinite(realTotalParsed) ? realTotalParsed : -1;
15473 const items = raw.map((p) => ({
15474 id: p.id,
15475 title: stripTags(p.title?.rendered || `#${p.id}`),
15476 editUrl: `${cfg.editPostUrlBase}?post=${p.id}&action=edit`
15477 }));
15478 const entry = {
15479 items,
15480 totalPages,
15481 realTotal,
15482 fetchedAt: performance.now()
15483 };
15484 postsCache.set(cacheKey2, entry);
15485 applyPostsResult(entry, myFocusId);
15486 } catch (err) {
15487 showError(__("Couldn’t load posts:"), err);
15488 }
15489 }
15490 function renderPosts(items) {
15491 clearPosts();
15492 if (focusId === null) {
15493 return;
15494 }
15495 const center = tags.get(focusId);
15496 if (!center) {
15497 return;
15498 }
15499 const count = items.length;
15500 const ringR = POST_RING_RADIUS + Math.max(0, count - 8) * 6;
15501 const tone = hslToInt(center.hue, 70, 48);
15502 items.forEach((item, idx) => {
15503 const angle = 2 * Math.PI / Math.max(1, count) * idx - Math.PI / 2;
15504 const tx = center.x + Math.cos(angle) * ringR;
15505 const ty = center.y + Math.sin(angle) * ringR;
15506 const gfx = new pixi.Graphics();
15507 postLayer.addChild(gfx);
15508 const post = {
15509 id: item.id,
15510 title: item.title,
15511 editUrl: item.editUrl,
15512 angle,
15513 r: ringR,
15514 x: center.x,
15515 y: center.y,
15516 tx,
15517 ty,
15518 gfx,
15519 tone
15520 };
15521 postNodes.set(item.id, post);
15522 ensurePostChip(post);
15523 });
15524 repaintPager();
15525 }
15526 function repaintPager() {
15527 if (focusId === null || focusTotalPages <= 1) {
15528 pager.visible = false;
15529 return;
15530 }
15531 pager.visible = true;
15532 const center = tags.get(focusId);
15533 if (!center) {
15534 pager.visible = false;
15535 return;
15536 }
15537 const prevDisabled = focusPage <= 1;
15538 const nextDisabled = focusPage >= focusTotalPages;
15539 drawPagerButton(pagerPrev, "◀", prevDisabled);
15540 drawPagerButton(pagerNext, "▶", nextDisabled);
15541 pagerPrev.cursor = prevDisabled ? "default" : "pointer";
15542 pagerNext.cursor = nextDisabled ? "default" : "pointer";
15543 pagerLabel.text = `${focusPage} / ${focusTotalPages}`;
15544 pagerPrev.x = -38;
15545 pagerPrev.y = 0;
15546 pagerNext.x = 38;
15547 pagerNext.y = 0;
15548 pagerLabel.x = 0;
15549 pagerLabel.y = 0;
15550 pager.x = center.x;
15551 pager.y = center.y + POST_RING_RADIUS + 60;
15552 }
15553 function drawPagerButton(gfx, glyph, disabled) {
15554 gfx.clear();
15555 gfx.circle(0, 0, 14);
15556 gfx.fill({
15557 color: disabled ? 15921906 : 16777215,
15558 alpha: disabled ? 0.7 : 1
15559 });
15560 gfx.stroke({
15561 color: 0,
15562 width: 1,
15563 alpha: 0.12
15564 });
15565 const children = gfx.children;
15566 const label = children?.[0] ?? null;
15567 if (!label) {
15568 const t = new pixi.Text({
15569 text: glyph,
15570 style: {
15571 fill: disabled ? 11580344 : 5265246,
15572 fontSize: 14,
15573 fontFamily: FONT_FAMILY,
15574 fontWeight: "600"
15575 }
15576 });
15577 t.anchor.set(0.5);
15578 gfx.addChild(t);
15579 } else {
15580 label.text = glyph;
15581 label.style.fill = disabled ? 11580344 : 5265246;
15582 }
15583 }
15584 function openInPostsTab(_id, editUrl, title) {
15585 const wm = api?.windowManager;
15586 const derive = api?.deriveWindowId;
15587 const postsWin = wm && typeof wm.getById === "function" ? wm.getById("desktop-mode-posts") : void 0;
15588 if (postsWin && typeof postsWin.isFullscreen === "function" && typeof postsWin.toggleFullscreen === "function" && postsWin.isFullscreen()) {
15589 postsWin.toggleFullscreen();
15590 }
15591 if (wm && typeof derive === "function") {
15592 const id = derive(editUrl);
15593 wm.open({
15594 id,
15595 baseId: id,
15596 url: editUrl,
15597 title: title ?? editUrl,
15598 icon: "dashicons-admin-post"
15599 });
15600 return;
15601 }
15602 try {
15603 window.open(editUrl, "_blank");
15604 } catch {
15605 window.location.assign(editUrl);
15606 }
15607 }
15608 function paintDraftSidebar() {
15609 const header = document.createElement("div");
15610 header.className = "wpd-tagcloud__sidebar-header";
15611 const dot = document.createElement("span");
15612 dot.className = "wpd-tagcloud__sidebar-dot";
15613 dot.style.background = `hsl( ${themeHue}deg 60% 55% )`;
15614 const label = document.createElement("code");
15615 label.className = "wpd-tagcloud__sidebar-slug";
15616 label.textContent = __("New tag");
15617 header.appendChild(dot);
15618 header.appendChild(label);
15619 sidebar.appendChild(header);
15620 const nameLabel = document.createElement("label");
15621 nameLabel.className = "wpd-tagcloud__sidebar-label";
15622 nameLabel.textContent = __("Name");
15623 sidebar.appendChild(nameLabel);
15624 const nameInput = document.createElement("input");
15625 nameInput.type = "text";
15626 nameInput.className = "wpd-tagcloud__editor-name";
15627 nameInput.placeholder = __("e.g. featured");
15628 sidebar.appendChild(nameInput);
15629 requestAnimationFrame(() => nameInput.focus());
15630 const descLabel = document.createElement("label");
15631 descLabel.className = "wpd-tagcloud__sidebar-label";
15632 descLabel.textContent = __("Description");
15633 sidebar.appendChild(descLabel);
15634 const descInput = document.createElement("textarea");
15635 descInput.className = "wpd-tagcloud__editor-desc";
15636 descInput.placeholder = __("Description (optional)");
15637 descInput.rows = 4;
15638 sidebar.appendChild(descInput);
15639 const actions = document.createElement("div");
15640 actions.className = "wpd-tagcloud__editor-actions";
15641 const createBtn = document.createElement("button");
15642 createBtn.type = "button";
15643 createBtn.className = "wpd-tagcloud__btn wpd-tagcloud__btn--primary";
15644 createBtn.textContent = __("Create");
15645 const cancelBtn = document.createElement("button");
15646 cancelBtn.type = "button";
15647 cancelBtn.className = "wpd-tagcloud__btn wpd-tagcloud__btn--danger";
15648 cancelBtn.textContent = __("Cancel");
15649 const handleCreate = async () => {
15650 const name = nameInput.value.trim();
15651 if (!name) {
15652 nameInput.focus();
15653 return;
15654 }
15655 createBtn.disabled = true;
15656 try {
15657 const created = await client.createTag(name);
15658 const next = {
15659 id: created.id,
15660 name: created.name,
15661 slug: created.slug || "",
15662 parent: 0,
15663 count: 0,
15664 description: created.description || "",
15665 isDefault: false
15666 };
15667 if (!terms.some((t) => t.id === next.id)) {
15668 terms = terms.concat(next);
15669 }
15670 const desc = descInput.value.trim();
15671 if (desc) {
15672 try {
15673 const updated = await client.updateTerm(
15674 "tags",
15675 created.id,
15676 { description: desc }
15677 );
15678 terms = terms.map(
15679 (t) => t.id === updated.id ? {
15680 ...t,
15681 description: updated.description ?? desc
15682 } : t
15683 );
15684 } catch {
15685 showError(
15686 __("Tag created but description failed:"),
15687 null
15688 );
15689 }
15690 }
15691 draft = null;
15692 buildCloud();
15693 focusId = created.id;
15694 paintSidebar();
15695 await loadPostsForFocus();
15696 } catch (err) {
15697 createBtn.disabled = false;
15698 showError(__("Couldn’t create:"), err);
15699 }
15700 };
15701 createBtn.addEventListener("click", () => {
15702 void handleCreate();
15703 });
15704 cancelBtn.addEventListener("click", () => {
15705 draft = null;
15706 paintSidebar();
15707 });
15708 nameInput.addEventListener("keydown", (e) => {
15709 if (e.key === "Enter") {
15710 e.preventDefault();
15711 void handleCreate();
15712 } else if (e.key === "Escape") {
15713 draft = null;
15714 paintSidebar();
15715 }
15716 });
15717 actions.appendChild(createBtn);
15718 actions.appendChild(cancelBtn);
15719 sidebar.appendChild(actions);
15720 }
15721 function paintSidebar() {
15722 sidebar.replaceChildren();
15723 if (draft !== null) {
15724 paintDraftSidebar();
15725 return;
15726 }
15727 if (focusId === null) {
15728 const empty = document.createElement("div");
15729 empty.className = "wpd-tagcloud__sidebar-empty";
15730 const icon = document.createElement("span");
15731 icon.className = "dashicons dashicons-tag";
15732 icon.setAttribute("aria-hidden", "true");
15733 empty.appendChild(icon);
15734 const title = document.createElement("h3");
15735 title.className = "wpd-tagcloud__sidebar-empty-title";
15736 title.textContent = __("No tag selected");
15737 empty.appendChild(title);
15738 const help = document.createElement("p");
15739 help.className = "wpd-tagcloud__sidebar-empty-hint";
15740 help.textContent = __(
15741 "Click a tag on the cloud to edit it, or click + Add tag to create a new one."
15742 );
15743 empty.appendChild(help);
15744 sidebar.appendChild(empty);
15745 return;
15746 }
15747 const box = tags.get(focusId);
15748 if (!box) {
15749 focusId = null;
15750 paintSidebar();
15751 return;
15752 }
15753 const id = box.id;
15754 const header = document.createElement("div");
15755 header.className = "wpd-tagcloud__sidebar-header";
15756 const dot = document.createElement("span");
15757 dot.className = "wpd-tagcloud__sidebar-dot";
15758 dot.style.background = `hsl( ${box.hue}deg 60% 55% )`;
15759 const term = terms.find((t) => t.id === id);
15760 const idLabel = document.createElement("code");
15761 idLabel.className = "wpd-tagcloud__sidebar-slug";
15762 idLabel.textContent = `#${id}`;
15763 header.appendChild(dot);
15764 header.appendChild(idLabel);
15765 sidebar.appendChild(header);
15766 const nameLabel = document.createElement("label");
15767 nameLabel.className = "wpd-tagcloud__sidebar-label";
15768 nameLabel.textContent = __("Name");
15769 sidebar.appendChild(nameLabel);
15770 const nameInput = document.createElement("input");
15771 nameInput.type = "text";
15772 nameInput.className = "wpd-tagcloud__editor-name";
15773 nameInput.value = box.name;
15774 nameInput.placeholder = __("Name");
15775 sidebar.appendChild(nameInput);
15776 const slugLabel = document.createElement("label");
15777 slugLabel.className = "wpd-tagcloud__sidebar-label";
15778 slugLabel.textContent = __("Slug");
15779 sidebar.appendChild(slugLabel);
15780 const slugInput = document.createElement("input");
15781 slugInput.type = "text";
15782 slugInput.className = "wpd-tagcloud__editor-name";
15783 slugInput.value = term?.slug || "";
15784 slugInput.placeholder = __("auto-from-name");
15785 slugInput.spellcheck = false;
15786 slugInput.autocapitalize = "off";
15787 slugInput.addEventListener("input", () => {
15788 const v = slugInput.value;
15789 const norm = v.toLowerCase().replace(/[^a-z0-9-]+/g, "-");
15790 if (v !== norm) {
15791 const sel = slugInput.selectionStart ?? norm.length;
15792 slugInput.value = norm;
15793 slugInput.setSelectionRange(sel, sel);
15794 }
15795 });
15796 sidebar.appendChild(slugInput);
15797 const descLabel = document.createElement("label");
15798 descLabel.className = "wpd-tagcloud__sidebar-label";
15799 descLabel.textContent = __("Description");
15800 sidebar.appendChild(descLabel);
15801 const descInput = document.createElement("textarea");
15802 descInput.className = "wpd-tagcloud__editor-desc";
15803 descInput.value = box.description || "";
15804 descInput.placeholder = __("Description (optional)");
15805 descInput.rows = 4;
15806 sidebar.appendChild(descInput);
15807 const meta = document.createElement("p");
15808 meta.className = "wpd-tagcloud__sidebar-meta";
15809 meta.textContent = sprintf(
15810 /* translators: %d: post count. */
15811 __("%d posts tagged with this."),
15812 box.count
15813 );
15814 sidebar.appendChild(meta);
15815 const actions = document.createElement("div");
15816 actions.className = "wpd-tagcloud__editor-actions";
15817 const saveBtn = document.createElement("button");
15818 saveBtn.type = "button";
15819 saveBtn.className = "wpd-tagcloud__btn wpd-tagcloud__btn--primary";
15820 saveBtn.textContent = __("Save");
15821 saveBtn.addEventListener("click", async () => {
15822 const name = nameInput.value.trim();
15823 if (!name) {
15824 return;
15825 }
15826 const description = descInput.value;
15827 const slugRaw = slugInput.value.trim();
15828 const currentSlug = term?.slug ?? "";
15829 if (name === box.name && description === (box.description || "") && slugRaw === currentSlug) {
15830 return;
15831 }
15832 const patch = { name, description };
15833 if (slugRaw !== currentSlug) {
15834 patch.slug = slugRaw;
15835 }
15836 try {
15837 const updated = await client.updateTerm("tags", box.id, patch);
15838 box.name = updated.name;
15839 box.description = updated.description;
15840 box.slug = updated.slug ?? box.slug;
15841 box.hue = tagHue(box.slug || box.name, themeHue);
15842 box.rotation = tagRotation(box.slug || box.name);
15843 terms = terms.map(
15844 (t) => t.id === box.id ? {
15845 ...t,
15846 name: updated.name,
15847 description: updated.description,
15848 slug: updated.slug ?? t.slug
15849 } : t
15850 );
15851 layoutChip(box);
15852 paintSidebar();
15853 } catch (err) {
15854 showError(__("Couldn’t save:"), err);
15855 }
15856 });
15857 const delBtn = document.createElement("button");
15858 delBtn.type = "button";
15859 delBtn.className = "wpd-tagcloud__btn wpd-tagcloud__btn--danger";
15860 delBtn.textContent = __("Delete");
15861 let armResetTimer = null;
15862 const armDelete = () => {
15863 delBtn.textContent = __("Click again to delete");
15864 delBtn.classList.add("is-armed");
15865 if (armResetTimer !== null) {
15866 window.clearTimeout(armResetTimer);
15867 }
15868 armResetTimer = window.setTimeout(() => {
15869 delBtn.textContent = __("Delete");
15870 delBtn.classList.remove("is-armed");
15871 armResetTimer = null;
15872 }, 2500);
15873 };
15874 delBtn.addEventListener("click", async () => {
15875 if (!delBtn.classList.contains("is-armed")) {
15876 armDelete();
15877 return;
15878 }
15879 if (armResetTimer !== null) {
15880 window.clearTimeout(armResetTimer);
15881 armResetTimer = null;
15882 }
15883 try {
15884 await client.deleteTerm("tags", box.id);
15885 terms = terms.filter((t) => t.id !== box.id);
15886 persistedPositions.delete(box.id);
15887 writePersistedPositions(positionsKey, persistedPositions);
15888 focusId = null;
15889 clearPosts();
15890 buildCloud();
15891 paintSidebar();
15892 } catch (err) {
15893 showError(__("Couldn’t delete:"), err);
15894 }
15895 });
15896 actions.appendChild(saveBtn);
15897 actions.appendChild(delBtn);
15898 sidebar.appendChild(actions);
15899 }
15900 function startDraft() {
15901 draft = true;
15902 paintSidebar();
15903 }
15904 addTagBtn.addEventListener("click", () => {
15905 startDraft();
15906 });
15907 function fitToView(opts = {}) {
15908 const padding = opts.padding ?? 90;
15909 const animate = opts.animate ?? false;
15910 const r = stage.getBoundingClientRect();
15911 if (tags.size === 0 || r.width === 0 || r.height === 0) {
15912 const cx2 = r.width / 2;
15913 const cy2 = r.height / 2;
15914 targetScale = 1;
15915 targetWorldX = cx2;
15916 targetWorldY = cy2;
15917 if (!animate) {
15918 world.x = cx2;
15919 world.y = cy2;
15920 world.scale.set(1);
15921 }
15922 return;
15923 }
15924 let minX = Infinity;
15925 let minY = Infinity;
15926 let maxX = -Infinity;
15927 let maxY = -Infinity;
15928 for (const box of tags.values()) {
15929 minX = Math.min(minX, box.tx - box.width / 2);
15930 minY = Math.min(minY, box.ty - box.height / 2);
15931 maxX = Math.max(maxX, box.tx + box.width / 2);
15932 maxY = Math.max(maxY, box.ty + box.height / 2);
15933 }
15934 const w = Math.max(1, maxX - minX);
15935 const h = Math.max(1, maxY - minY);
15936 const sx = (r.width - padding * 2) / w;
15937 const sy = (r.height - padding * 2) / h;
15938 const scale = Math.max(0.2, Math.min(1.5, Math.min(sx, sy)));
15939 const cx = (minX + maxX) / 2;
15940 const cy = (minY + maxY) / 2;
15941 const newWorldX = r.width / 2 - cx * scale;
15942 const newWorldY = r.height / 2 - cy * scale;
15943 targetScale = scale;
15944 targetWorldX = newWorldX;
15945 targetWorldY = newWorldY;
15946 if (!animate) {
15947 world.scale.set(scale);
15948 world.x = newWorldX;
15949 world.y = newWorldY;
15950 }
15951 }
15952 function recenterCamera() {
15953 if (focusId !== null) {
15954 const focused = tags.get(focusId);
15955 const r = stage.getBoundingClientRect();
15956 if (focused && r.width > 0 && r.height > 0) {
15957 const half = POST_RING_RADIUS + 70;
15958 const sx = r.width * 0.85 / (2 * half);
15959 const sy = r.height * 0.85 / (2 * half);
15960 const newScale = Math.max(
15961 0.5,
15962 Math.min(1.6, Math.min(sx, sy))
15963 );
15964 targetScale = newScale;
15965 targetWorldX = r.width / 2 - focused.x * newScale;
15966 targetWorldY = r.height / 2 - focused.y * newScale;
15967 return;
15968 }
15969 }
15970 fitToView({ animate: true });
15971 }
15972 recenterBtn.addEventListener("click", () => recenterCamera());
15973 reflowBtn.addEventListener("click", () => {
15974 persistedPositions.clear();
15975 writePersistedPositions(positionsKey, persistedPositions);
15976 for (const box of tags.values()) {
15977 box.tx = 0;
15978 box.ty = 0;
15979 }
15980 const allBoxes = Array.from(tags.values());
15981 allBoxes.sort((a, b) => b.count - a.count);
15982 packBoxesWithClusters(
15983 allBoxes,
15984 [],
15985 /* @__PURE__ */ new Map(),
15986 cooccurrenceMap
15987 );
15988 fitToView({ animate: true });
15989 void refreshCooccurrence();
15990 });
15991 app.canvas.addEventListener("click", (e) => {
15992 const now = performance.now();
15993 if (now - lastFocusChange < 250 || now - pixiInteractionAt < 250) {
15994 return;
15995 }
15996 if (panMovedDist > 4) {
15997 return;
15998 }
15999 const target = e.target;
16000 if (target === app.canvas && !dragChip && focusId !== null) {
16001 closeFocus();
16002 }
16003 });
16004 async function refreshCountsViaBulk() {
16005 if (terms.length === 0) {
16006 return;
16007 }
16008 const cfg = client.getConfig();
16009 const url = new URL(
16010 joinRestUrl(cfg.restRoot, "desktop-mode/v1/term-counts")
16011 );
16012 url.searchParams.set("taxonomy", "post_tag");
16013 url.searchParams.set(
16014 "ids",
16015 terms.map((t) => t.id).join(",")
16016 );
16017 try {
16018 const response = await fetchShellJson(client, url.toString());
16019 const map = response.json;
16020 let dirty = false;
16021 terms = terms.map((t) => {
16022 const fresh = map[String(t.id)];
16023 if (typeof fresh === "number" && fresh !== t.count) {
16024 dirty = true;
16025 const box = tags.get(t.id);
16026 if (box) {
16027 box.count = fresh;
16028 }
16029 return { ...t, count: fresh };
16030 }
16031 return t;
16032 });
16033 if (dirty) {
16034 const maxCount = Math.max(
16035 1,
16036 ...terms.map((t) => t.count)
16037 );
16038 for (const t of terms) {
16039 const box = tags.get(t.id);
16040 if (!box) {
16041 continue;
16042 }
16043 box.count = t.count;
16044 box.fontSize = fontSizeFor(t.count, maxCount);
16045 layoutChip(box);
16046 }
16047 if (focusId !== null) {
16048 paintSidebar();
16049 }
16050 }
16051 } catch {
16052 }
16053 }
16054 function relayoutWithCooccurrence() {
16055 const placed = [];
16056 const placedById = /* @__PURE__ */ new Map();
16057 const toRepack = [];
16058 for (const box of tags.values()) {
16059 if (persistedPositions.has(box.id)) {
16060 placed.push({
16061 x: box.tx - box.width / 2,
16062 y: box.ty - box.height / 2,
16063 w: box.width,
16064 h: box.height
16065 });
16066 placedById.set(box.id, { x: box.tx, y: box.ty });
16067 } else {
16068 toRepack.push(box);
16069 }
16070 }
16071 toRepack.sort((a, b) => b.count - a.count);
16072 packBoxesWithClusters(toRepack, placed, placedById, cooccurrenceMap);
16073 }
16074 async function refreshCooccurrence() {
16075 try {
16076 const fetched = await client.fetchTagCooccurrence("tags", 8);
16077 cooccurrenceMap = fetched;
16078 if (cooccurrenceMap.size > 0) {
16079 relayoutWithCooccurrence();
16080 }
16081 } catch {
16082 }
16083 }
16084 buildCloud();
16085 paintSidebar();
16086 raf = requestAnimationFrame(tick);
16087 void refreshCountsViaBulk();
16088 void refreshCooccurrence();
16089 if (terms.length === 0) {
16090 const empty = document.createElement("div");
16091 empty.className = "wpd-tagcloud__empty";
16092 empty.textContent = __(
16093 'No tags yet. Click "Add tag" to start building the cloud.'
16094 );
16095 stage.appendChild(empty);
16096 }
16097 let currentMatches = [];
16098 let selectedIndex = 0;
16099 const repaintHighlight = () => {
16100 const items = searchResults.querySelectorAll(
16101 ".wpd-tagcloud__search-result"
16102 );
16103 items.forEach((el, i) => {
16104 const active = i === selectedIndex;
16105 el.classList.toggle("is-active", active);
16106 if (active) {
16107 el.scrollIntoView({ block: "nearest" });
16108 }
16109 });
16110 };
16111 const selectMatch = (t) => {
16112 searchInput.value = "";
16113 searchResults.hidden = true;
16114 searchResults.replaceChildren();
16115 currentMatches = [];
16116 selectedIndex = 0;
16117 void focusTag(t.id);
16118 };
16119 const renderSearchResults = () => {
16120 const q = searchInput.value.trim().toLowerCase();
16121 if (q.length === 0) {
16122 searchResults.hidden = true;
16123 searchResults.replaceChildren();
16124 currentMatches = [];
16125 selectedIndex = 0;
16126 return;
16127 }
16128 currentMatches = Array.from(tags.values()).filter(
16129 (t) => t.name.toLowerCase().includes(q) || t.slug.toLowerCase().includes(q)
16130 ).sort((a, b) => b.count - a.count).slice(0, 10);
16131 selectedIndex = 0;
16132 searchResults.replaceChildren();
16133 currentMatches.forEach((t, i) => {
16134 const li = document.createElement("li");
16135 const btn = document.createElement("button");
16136 btn.type = "button";
16137 btn.className = "wpd-tagcloud__search-result";
16138 if (i === 0) {
16139 btn.classList.add("is-active");
16140 }
16141 const nameEl = document.createElement("span");
16142 nameEl.className = "wpd-tagcloud__search-title";
16143 nameEl.textContent = t.name || `#${t.id}`;
16144 const countEl = document.createElement("span");
16145 countEl.className = "wpd-tagcloud__search-meta";
16146 countEl.textContent = sprintf(
16147 /* translators: %d: number of posts assigned to a tag. */
16148 __("%d posts"),
16149 t.count
16150 );
16151 btn.appendChild(nameEl);
16152 btn.appendChild(countEl);
16153 btn.addEventListener("mousedown", (ev) => {
16154 ev.preventDefault();
16155 selectMatch(t);
16156 });
16157 btn.addEventListener("mouseenter", () => {
16158 selectedIndex = i;
16159 repaintHighlight();
16160 });
16161 li.appendChild(btn);
16162 searchResults.appendChild(li);
16163 });
16164 searchResults.hidden = currentMatches.length === 0;
16165 };
16166 searchInput.addEventListener("input", renderSearchResults);
16167 searchInput.addEventListener("focus", renderSearchResults);
16168 searchInput.addEventListener("keydown", (ev) => {
16169 if (ev.key === "ArrowDown") {
16170 if (currentMatches.length === 0) {
16171 return;
16172 }
16173 ev.preventDefault();
16174 selectedIndex = Math.min(
16175 selectedIndex + 1,
16176 currentMatches.length - 1
16177 );
16178 repaintHighlight();
16179 } else if (ev.key === "ArrowUp") {
16180 if (currentMatches.length === 0) {
16181 return;
16182 }
16183 ev.preventDefault();
16184 selectedIndex = Math.max(selectedIndex - 1, 0);
16185 repaintHighlight();
16186 } else if (ev.key === "Enter") {
16187 if (currentMatches.length === 0) {
16188 return;
16189 }
16190 ev.preventDefault();
16191 selectMatch(currentMatches[selectedIndex]);
16192 } else if (ev.key === "Escape") {
16193 searchInput.value = "";
16194 searchResults.hidden = true;
16195 searchResults.replaceChildren();
16196 currentMatches = [];
16197 selectedIndex = 0;
16198 }
16199 });
16200 searchInput.addEventListener("blur", () => {
16201 setTimeout(() => {
16202 searchResults.hidden = true;
16203 }, 120);
16204 });
16205 const onDocClickSearch = (ev) => {
16206 if (!searchWrap.contains(ev.target)) {
16207 searchResults.hidden = true;
16208 }
16209 };
16210 document.addEventListener("click", onDocClickSearch);
16211 return () => {
16212 if (raf !== null) {
16213 cancelAnimationFrame(raf);
16214 raf = null;
16215 }
16216 if (settleTimer !== null) {
16217 window.clearTimeout(settleTimer);
16218 settleTimer = null;
16219 }
16220 ro.disconnect();
16221 stage.removeEventListener("wheel", onWheel);
16222 document.removeEventListener("click", onDocClickSearch);
16223 try {
16224 app.ticker?.stop();
16225 } catch {
16226 }
16227 try {
16228 app.canvas?.remove();
16229 } catch {
16230 }
16231 host.replaceChildren();
16232 host.classList.remove("wpd-tagcloud");
16233 };
16234 }
16235 function fontSizeFor(count, max) {
16236 const ratio = Math.sqrt(count / Math.max(1, max));
16237 return Math.round(
16238 MIN_FONT_SIZE + (MAX_FONT_SIZE - MIN_FONT_SIZE) * ratio
16239 );
16240 }
16241 function truncateChipName(name) {
16242 return name.length > CHIP_NAME_MAX_CHARS ? name.slice(0, CHIP_NAME_MAX_CHARS - 1) + "…" : name;
16243 }
16244 function aabbIntersect(a, b) {
16245 return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
16246 }
16247 function slugHash(slug) {
16248 let h = 0;
16249 for (let i = 0; i < slug.length; i++) {
16250 h = (h * 31 + slug.charCodeAt(i)) % 2147483647;
16251 }
16252 return h;
16253 }
16254 function tagHue(slug, baseHue) {
16255 const h = slugHash(slug);
16256 return ((baseHue + h % 256 * 1.4) % 360 + 360) % 360;
16257 }
16258 function tagRotation(slug) {
16259 const h = slugHash(slug);
16260 const sign = h % 2 === 0 ? -1 : 1;
16261 const mag = Math.floor(h / 2) % 4 * 0.011;
16262 return sign * mag;
16263 }
16264 function readAdminThemeHue() {
16265 try {
16266 const value = getComputedStyle(document.documentElement).getPropertyValue("--wp-admin-theme-color").trim();
16267 if (!value) {
16268 return 210;
16269 }
16270 const c = document.createElement("span");
16271 c.style.color = value;
16272 document.body.appendChild(c);
16273 const rgb = getComputedStyle(c).color;
16274 c.remove();
16275 const m = rgb.match(/\d+/g);
16276 if (!m || m.length < 3) {
16277 return 210;
16278 }
16279 return rgbToHue(
16280 parseInt(m[0], 10),
16281 parseInt(m[1], 10),
16282 parseInt(m[2], 10)
16283 );
16284 } catch {
16285 return 210;
16286 }
16287 }
16288 function rgbToHue(r, g, b) {
16289 const rn = r / 255;
16290 const gn = g / 255;
16291 const bn = b / 255;
16292 const max = Math.max(rn, gn, bn);
16293 const min = Math.min(rn, gn, bn);
16294 const d = max - min;
16295 if (d === 0) {
16296 return 210;
16297 }
16298 let h;
16299 switch (max) {
16300 case rn:
16301 h = (gn - bn) / d + (gn < bn ? 6 : 0);
16302 break;
16303 case gn:
16304 h = (bn - rn) / d + 2;
16305 break;
16306 default:
16307 h = (rn - gn) / d + 4;
16308 break;
16309 }
16310 return Math.round(h * 60);
16311 }
16312 function hslToInt(h, s, l) {
16313 const sn = s / 100;
16314 const ln = l / 100;
16315 const c = (1 - Math.abs(2 * ln - 1)) * sn;
16316 const hp = h / 60;
16317 const x = c * (1 - Math.abs(hp % 2 - 1));
16318 let r = 0;
16319 let g = 0;
16320 let b = 0;
16321 if (hp < 1) {
16322 r = c;
16323 g = x;
16324 } else if (hp < 2) {
16325 r = x;
16326 g = c;
16327 } else if (hp < 3) {
16328 g = c;
16329 b = x;
16330 } else if (hp < 4) {
16331 g = x;
16332 b = c;
16333 } else if (hp < 5) {
16334 r = x;
16335 b = c;
16336 } else {
16337 r = c;
16338 b = x;
16339 }
16340 const m = ln - c / 2;
16341 const ri = Math.round((r + m) * 255);
16342 const gi = Math.round((g + m) * 255);
16343 const bi = Math.round((b + m) * 255);
16344 return ri * 65536 + gi * 256 + bi;
16345 }
16346 function stripTags(html2) {
16347 const tmp = document.createElement("div");
16348 tmp.innerHTML = html2;
16349 return tmp.textContent || tmp.innerText || "";
16350 }
16351 function showToast(title, err) {
16352 const reason = err instanceof Error ? err.message : String(err);
16353 const api = window.wp?.desktop;
16354 if (api && typeof api.showToast === "function") {
16355 api.showToast({
16356 message: `${title} ${reason}`.trim(),
16357 duration: 6e3
16358 });
16359 return;
16360 }
16361 console.error(title, err);
16362 }
16363 async function fetchShellJson(client, url) {
16364 const cfg = client.getConfig();
16365 const init = {
16366 method: "GET",
16367 credentials: "same-origin",
16368 headers: {
16369 "X-WP-Nonce": cfg.restNonce,
16370 Accept: "application/json"
16371 }
16372 };
16373 const response = await trackedFetch(url, init, {
16374 windowId: "desktop-mode-posts"
16375 });
16376 if (!response.ok) {
16377 throw new Error(`${response.status} ${response.statusText}`);
16378 }
16379 const json = await response.json();
16380 return { json, headers: response.headers };
16381 }
16382 function computePositionsKey() {
16383 try {
16384 const host = window.location.host || "unknown";
16385 const path = window.location.pathname.replace(/\/?wp-admin\/?.*$/, "");
16386 return `wpd-tagcloud-positions:${host}${path}`;
16387 } catch {
16388 return "wpd-tagcloud-positions:fallback";
16389 }
16390 }
16391 function readPersistedPositions(key) {
16392 try {
16393 const raw = window.localStorage.getItem(key);
16394 if (!raw) {
16395 return /* @__PURE__ */ new Map();
16396 }
16397 const parsed = JSON.parse(raw);
16398 if (!parsed || typeof parsed !== "object") {
16399 return /* @__PURE__ */ new Map();
16400 }
16401 const out = /* @__PURE__ */ new Map();
16402 for (const [k, v] of Object.entries(
16403 parsed
16404 )) {
16405 const id = parseInt(k, 10);
16406 if (!Number.isFinite(id)) {
16407 continue;
16408 }
16409 const pos = v;
16410 if (typeof pos?.x === "number" && typeof pos?.y === "number") {
16411 out.set(id, { x: pos.x, y: pos.y });
16412 }
16413 }
16414 return out;
16415 } catch {
16416 return /* @__PURE__ */ new Map();
16417 }
16418 }
16419 function writePersistedPositions(key, positions) {
16420 try {
16421 const obj = {};
16422 for (const [id, pos] of positions) {
16423 obj[String(id)] = pos;
16424 }
16425 window.localStorage.setItem(key, JSON.stringify(obj));
16426 } catch {
16427 }
16428 }
16429 function createTagChip(pixi, chipLayer, term, fontSize, hue) {
16430 const container = new pixi.Container();
16431 container.eventMode = "static";
16432 container.cursor = "pointer";
16433 const shadow = new pixi.Graphics();
16434 container.addChild(shadow);
16435 const bg = new pixi.Graphics();
16436 container.addChild(bg);
16437 const hashText = new pixi.Text({
16438 text: "#",
16439 style: {
16440 fill: hslToInt(hue, 65, 42),
16441 fontSize,
16442 fontFamily: FONT_FAMILY,
16443 fontWeight: "700"
16444 },
16445 resolution: CHIP_TEXT_RES
16446 });
16447 container.addChild(hashText);
16448 const nameText = new pixi.Text({
16449 text: truncateChipName(term.name),
16450 style: {
16451 fill: 1909543,
16452 fontSize,
16453 fontFamily: FONT_FAMILY,
16454 fontWeight: "600"
16455 },
16456 resolution: CHIP_TEXT_RES
16457 });
16458 container.addChild(nameText);
16459 const countText = new pixi.Text({
16460 text: String(term.count),
16461 style: {
16462 fill: 16777215,
16463 fontSize: Math.max(10, Math.round(fontSize * 0.55)),
16464 fontFamily: FONT_FAMILY,
16465 fontWeight: "700"
16466 },
16467 resolution: CHIP_TEXT_RES
16468 });
16469 container.addChild(countText);
16470 chipLayer.addChild(container);
16471 return {
16472 container,
16473 shadow,
16474 bg,
16475 hashText,
16476 nameText,
16477 countText,
16478 width: 0,
16479 height: 0,
16480 cachedName: "",
16481 cachedCount: -1,
16482 cachedFocused: false,
16483 cachedHover: false,
16484 cachedHue: -1
16485 };
16486 }
16487 const tagsCloud = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
16488 __proto__: null,
16489 mountTagsCloud
16490 }, Symbol.toStringTag, { value: "Module" }));
16491 async function showUsersIntroDialog() {
16492 return new Promise((resolve) => {
16493 const backdrop = document.createElement("div");
16494 backdrop.className = "desktop-mode-users-intro__backdrop";
16495 backdrop.setAttribute("role", "presentation");
16496 Object.assign(backdrop.style, {
16497 position: "fixed",
16498 inset: "0",
16499 background: "color-mix(in srgb, var(--wp-admin-theme-color, #1d2327) 60%, transparent)",
16500 backdropFilter: "blur(2px)",
16501 zIndex: "100000",
16502 display: "flex",
16503 alignItems: "center",
16504 justifyContent: "center",
16505 padding: "24px"
16506 });
16507 const dialog = document.createElement("div");
16508 dialog.setAttribute("role", "dialog");
16509 dialog.setAttribute("aria-modal", "true");
16510 dialog.setAttribute(
16511 "aria-labelledby",
16512 "desktop-mode-users-intro-title"
16513 );
16514 dialog.className = "desktop-mode-users-intro";
16515 Object.assign(dialog.style, {
16516 background: "var(--wp-admin-theme-bg, #fff)",
16517 color: "var(--wp-admin-theme-fg, #1d2327)",
16518 borderRadius: "14px",
16519 boxShadow: "0 24px 60px rgba(0,0,0,.28)",
16520 maxWidth: "520px",
16521 width: "100%",
16522 maxHeight: "90vh",
16523 overflow: "auto",
16524 padding: "28px 32px 24px",
16525 fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
16526 });
16527 dialog.innerHTML = renderDialogMarkup();
16528 backdrop.appendChild(dialog);
16529 document.body.appendChild(backdrop);
16530 const primaryBtn = dialog.querySelector(
16531 '[data-action="confirm"]'
16532 );
16533 const settingsBtn = dialog.querySelector(
16534 '[data-action="settings"]'
16535 );
16536 primaryBtn?.focus();
16537 let resolved = false;
16538 const cleanup = (result) => {
16539 if (resolved) {
16540 return;
16541 }
16542 resolved = true;
16543 document.removeEventListener("keydown", onKey, true);
16544 backdrop.remove();
16545 resolve(result);
16546 };
16547 const onKey = (e) => {
16548 if (e.key === "Escape") {
16549 e.preventDefault();
16550 cleanup("cancel");
16551 }
16552 };
16553 document.addEventListener("keydown", onKey, true);
16554 backdrop.addEventListener("click", (e) => {
16555 if (e.target === backdrop) {
16556 cleanup("cancel");
16557 }
16558 });
16559 primaryBtn?.addEventListener("click", () => cleanup("confirm"));
16560 settingsBtn?.addEventListener("click", () => cleanup("settings"));
16561 });
16562 }
16563 function renderDialogMarkup() {
16564 const title = __("Welcome to the new Users window");
16565 const lede = __(
16566 "Same data you already manage, with the polish the Users list has been waiting for."
16567 );
16568 const highlights = [
16569 __("Live online indicator on every row — see who is around right now."),
16570 __("Last-login column so you finally know who is actually using the site."),
16571 __("Bulk role change with strict role-permission enforcement — never accidentally promote anyone above your own level."),
16572 __("One-click password reset and resend-welcome buttons, with sensible rate-limiting."),
16573 __("Click-to-copy email and a long-overdue search that matches name, username, AND email."),
16574 __("Per-user content stats: posts, pages, comments at a glance.")
16575 ];
16576 const li = (arr) => arr.map(
16577 (s) => `<li><span class="dot" aria-hidden="true"></span>${escapeHtml(s)}</li>`
16578 ).join("");
16579 return `
16580 <style>
16581 .desktop-mode-users-intro h2 {
16582 margin: 0 0 8px;
16583 font-size: 22px;
16584 font-weight: 600;
16585 letter-spacing: -0.01em;
16586 }
16587 .desktop-mode-users-intro p.lede {
16588 margin: 0 0 20px;
16589 color: var(--wp-admin-theme-fg-muted, #50575e);
16590 font-size: 14px;
16591 line-height: 1.5;
16592 }
16593 .desktop-mode-users-intro__list {
16594 list-style: none;
16595 margin: 0 0 22px;
16596 padding: 0;
16597 font-size: 14px;
16598 line-height: 1.5;
16599 }
16600 .desktop-mode-users-intro__list li {
16601 display: flex;
16602 align-items: flex-start;
16603 gap: 10px;
16604 padding: 6px 0;
16605 }
16606 .desktop-mode-users-intro__list .dot {
16607 flex: 0 0 auto;
16608 width: 6px;
16609 height: 6px;
16610 margin-top: 9px;
16611 border-radius: 50%;
16612 background: var(--wp-admin-theme-color, #2271b1);
16613 }
16614 .desktop-mode-users-intro__footer {
16615 display: flex;
16616 justify-content: flex-end;
16617 gap: 8px;
16618 margin-top: 8px;
16619 }
16620 .desktop-mode-users-intro__footer button {
16621 appearance: none;
16622 border: 1px solid var(--wp-admin-theme-border, #dcdcde);
16623 background: var(--wp-admin-theme-bg, #fff);
16624 color: inherit;
16625 padding: 8px 14px;
16626 border-radius: 6px;
16627 font-size: 13px;
16628 cursor: pointer;
16629 }
16630 .desktop-mode-users-intro__footer button.primary {
16631 border-color: var(--wp-admin-theme-color, #2271b1);
16632 background: var(--wp-admin-theme-color, #2271b1);
16633 color: #fff;
16634 font-weight: 500;
16635 }
16636 .desktop-mode-users-intro__footer button:hover { filter: brightness(1.05); }
16637 .desktop-mode-users-intro__footer button:focus-visible {
16638 outline: 2px solid var(--wp-admin-theme-color, #2271b1);
16639 outline-offset: 2px;
16640 }
16641 </style>
16642 <h2 id="desktop-mode-users-intro-title">${escapeHtml(title)}</h2>
16643 <p class="lede">${escapeHtml(lede)}</p>
16644 <ul class="desktop-mode-users-intro__list">${li(highlights)}</ul>
16645 <div class="desktop-mode-users-intro__footer">
16646 <button type="button" data-action="settings">${escapeHtml(
16647 __("Take me to settings")
16648 )}</button>
16649 <button type="button" class="primary" data-action="confirm">${escapeHtml(
16650 __("Got it")
16651 )}</button>
16652 </div>
16653 `;
16654 }
16655 function escapeHtml(s) {
16656 const t = document.createElement("div");
16657 t.textContent = s;
16658 return t.innerHTML;
16659 }
16660 const _initial = {
16661 userId: null,
16662 requestedAt: 0,
16663 tabRequested: false
16664 };
16665 let _store = null;
16666 function getStore() {
16667 if (_store) {
16668 return _store;
16669 }
16670 const w = window;
16671 const factory = w.wp?.desktop?.createSharedStore;
16672 if (typeof factory !== "function") {
16673 return null;
16674 }
16675 _store = factory(
16676 "desktop-mode/user-edit/target",
16677 () => ({ ..._initial })
16678 );
16679 return _store;
16680 }
16681 function setUserEditTarget(userId) {
16682 const store = getStore();
16683 if (store) {
16684 store.state.userId = userId;
16685 store.state.requestedAt = Date.now();
16686 store.state.tabRequested = true;
16687 store.notify();
16688 return;
16689 }
16690 const w = window;
16691 w._wpdUserEditTarget = {
16692 userId,
16693 requestedAt: Date.now(),
16694 tabRequested: true
16695 };
16696 }
16697 function readUserEditTarget() {
16698 const store = getStore();
16699 if (store) {
16700 return { ...store.state };
16701 }
16702 const w = window;
16703 return w._wpdUserEditTarget ?? { ..._initial };
16704 }
16705 function clearUserEditTarget() {
16706 const store = getStore();
16707 if (store) {
16708 store.state.userId = null;
16709 store.state.requestedAt = 0;
16710 store.state.tabRequested = false;
16711 store.notify();
16712 }
16713 const w = window;
16714 if (w._wpdUserEditTarget) {
16715 w._wpdUserEditTarget = {
16716 userId: null,
16717 requestedAt: 0,
16718 tabRequested: false
16719 };
16720 }
16721 }
16722 function setUserEditTabRequested(requested) {
16723 const store = getStore();
16724 if (store) {
16725 store.state.tabRequested = requested;
16726 store.notify();
16727 return;
16728 }
16729 const w = window;
16730 const prev = w._wpdUserEditTarget ?? { ..._initial };
16731 w._wpdUserEditTarget = { ...prev, tabRequested: requested };
16732 }
16733 function subscribeUserEditTarget(cb) {
16734 const store = getStore();
16735 if (!store) {
16736 return () => {
16737 };
16738 }
16739 return store.subscribe((state) => cb({ ...state }));
16740 }
16741 const userEditTarget = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
16742 __proto__: null,
16743 clearUserEditTarget,
16744 readUserEditTarget,
16745 setUserEditTabRequested,
16746 setUserEditTarget,
16747 subscribeUserEditTarget
16748 }, Symbol.toStringTag, { value: "Module" }));
16749 function wpdConfirmGlobal(options) {
16750 const w = window;
16751 const fn = w.wp?.desktop?.confirm;
16752 if (typeof fn !== "function") {
16753 return Promise.resolve(window.confirm(options.message));
16754 }
16755 return fn(options);
16756 }
16757 function notifyToast(body, opts = {}) {
16758 const w = window;
16759 const api = w.wp?.desktop;
16760 if (api?.notify) {
16761 api.notify({ body, kind: opts.kind });
16762 return;
16763 }
16764 console.info("[users-window]", body);
16765 }
16766 function openUserEditWindow(userId) {
16767 if (!Number.isFinite(userId) || userId <= 0) {
16768 return;
16769 }
16770 setUserEditTarget(userId);
16771 console.info(
16772 "[users-window] opening user-edit window for user",
16773 userId
16774 );
16775 const w = window;
16776 const fn = w.wp?.desktop?.openWindow;
16777 if (typeof fn !== "function") {
16778 console.error(
16779 "[users-window] wp.desktop.openWindow is missing — desktop shell may not be ready."
16780 );
16781 notifyToast(
16782 __("Could not open profile window — desktop shell unavailable."),
16783 { kind: "error" }
16784 );
16785 return;
16786 }
16787 const opened = fn("desktop-mode-user-edit", {
16788 source: "users-window/row-click"
16789 });
16790 if (!opened) {
16791 console.error(
16792 '[users-window] openWindow("desktop-mode-user-edit") returned false — window not registered server-side. Check includes/user-edit-window/window.php.'
16793 );
16794 notifyToast(
16795 __("Profile window not registered — see console."),
16796 { kind: "error" }
16797 );
16798 }
16799 }
16800 const ROOT = "[data-desktop-mode-posts-root]";
16801 const STATUS = "[data-desktop-mode-posts-status]";
16802 const SEARCH = "[data-desktop-mode-posts-search]";
16803 const REFRESH = "[data-desktop-mode-posts-refresh]";
16804 const NEW_BTN = "[data-desktop-mode-posts-new]";
16805 const TABLE = "[data-desktop-mode-posts-table]";
16806 const BULK = "[data-desktop-mode-posts-bulk]";
16807 const COUNT = "[data-desktop-mode-posts-count]";
16808 const PAGE_INDICATOR = "[data-desktop-mode-posts-page-indicator]";
16809 const PREV = "[data-desktop-mode-posts-prev]";
16810 const NEXT = "[data-desktop-mode-posts-next]";
16811 const PER_PAGE = "[data-desktop-mode-posts-per-page]";
16812 const BULK_ACTIONS_HOST = "[data-desktop-mode-posts-bulk-actions]";
16813 const SEARCH_DEBOUNCE_MS = 250;
16814 function userCellKey(id, key) {
16815 return `${id}::${key}`;
16816 }
16817 function memoUserCell(cache, id, key, build) {
16818 const k = userCellKey(id, key);
16819 const cached = cache.get(k);
16820 if (cached) {
16821 return cached;
16822 }
16823 const node = build();
16824 cache.set(k, node);
16825 return node;
16826 }
16827 const _usersIntroShown = { v: false };
16828 function maybeShowUsersIntro(client) {
16829 if (_usersIntroShown.v) {
16830 return;
16831 }
16832 let cfg;
16833 try {
16834 cfg = client.getConfig();
16835 } catch {
16836 return;
16837 }
16838 if (cfg.introSeen) {
16839 return;
16840 }
16841 _usersIntroShown.v = true;
16842 void showUsersIntroDialog().then((result) => {
16843 if (result === "cancel") {
16844 _usersIntroShown.v = false;
16845 return;
16846 }
16847 void markUsersIntroSeen(client, cfg);
16848 if (result === "settings") {
16849 const w = window;
16850 w.wp?.desktop?.openOsSettings?.();
16851 }
16852 }).catch(() => {
16853 _usersIntroShown.v = false;
16854 });
16855 }
16856 async function markUsersIntroSeen(client, cfg) {
16857 if (!cfg.introUrl) {
16858 return;
16859 }
16860 try {
16861 await trackedFetch(
16862 cfg.introUrl,
16863 {
16864 method: "POST",
16865 credentials: "same-origin",
16866 headers: {
16867 "Content-Type": "application/json",
16868 "X-WP-Nonce": cfg.restNonce
16869 },
16870 body: JSON.stringify({ slug: "users" })
16871 },
16872 {
16873 windowId: client.windowId,
16874 source: "users-window/intro"
16875 }
16876 );
16877 cfg.introSeen = true;
16878 } catch {
16879 }
16880 }
16881 function buildIdentityCell(row, cfg) {
16882 const cell = document.createElement("span");
16883 cell.style.cssText = "display:flex;align-items:center;gap:10px;min-width:0;";
16884 const avatar = document.createElement("wpd-avatar");
16885 avatar.setAttribute("size", "32");
16886 if (row.name) {
16887 avatar.setAttribute("name", row.name);
16888 }
16889 const presence = row.desktop_mode_presence ?? "offline";
16890 avatar.setAttribute("presence", presence);
16891 const avatars = row.avatar_urls ?? {};
16892 const rawAvatar = avatars["48"] ?? avatars["96"] ?? avatars["24"] ?? "";
16893 if (rawAvatar) {
16894 applyAvatarSrc(avatar, rawAvatar);
16895 }
16896 cell.appendChild(avatar);
16897 const text = document.createElement("span");
16898 text.style.cssText = "display:flex;flex-direction:column;min-width:0;line-height:1.25;";
16899 const nameRow = document.createElement("span");
16900 const name = document.createElement("a");
16901 name.href = `${cfg.editPostUrlBase}?user_id=${row.id}`;
16902 name.textContent = row.name || `#${row.id}`;
16903 name.title = name.textContent;
16904 name.setAttribute("data-noclick", "");
16905 name.style.cssText = "font-weight:600;color:inherit;text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:240px;";
16906 name.addEventListener("mouseenter", () => {
16907 name.style.textDecoration = "underline";
16908 });
16909 name.addEventListener("mouseleave", () => {
16910 name.style.textDecoration = "none";
16911 });
16912 name.addEventListener("click", (e) => {
16913 e.preventDefault();
16914 e.stopPropagation();
16915 void openUserEditWindow(row.id);
16916 });
16917 nameRow.appendChild(name);
16918 text.appendChild(nameRow);
16919 if (row.slug) {
16920 const sub = document.createElement("span");
16921 sub.textContent = `@${row.slug}`;
16922 sub.style.cssText = "font-size:11px;color:var(--wp-admin-theme-fg-muted, #8c8f94);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:240px;";
16923 text.appendChild(sub);
16924 }
16925 cell.appendChild(text);
16926 return cell;
16927 }
16928 function buildEmailCell(row) {
16929 const cell = document.createElement("button");
16930 cell.type = "button";
16931 const email = typeof row.email === "string" ? row.email : "";
16932 cell.textContent = email || "—";
16933 cell.disabled = email === "";
16934 cell.title = email ? __("Click to copy email") : "";
16935 Object.assign(cell.style, {
16936 appearance: "none",
16937 background: "transparent",
16938 border: "none",
16939 padding: "2px 6px",
16940 font: "inherit",
16941 color: "inherit",
16942 cursor: email ? "copy" : "default",
16943 textAlign: "left",
16944 fontSize: "13px",
16945 borderRadius: "4px",
16946 maxWidth: "100%",
16947 overflow: "hidden",
16948 textOverflow: "ellipsis",
16949 whiteSpace: "nowrap"
16950 });
16951 cell.addEventListener("click", (e) => {
16952 e.stopPropagation();
16953 if (!email) {
16954 return;
16955 }
16956 void navigator.clipboard?.writeText(email).then(() => {
16957 const orig = cell.textContent;
16958 cell.textContent = __("Copied!");
16959 cell.style.color = "var(--wp-admin-theme-color, #2271b1)";
16960 setTimeout(() => {
16961 cell.textContent = orig;
16962 cell.style.color = "";
16963 }, 1200);
16964 }).catch(() => {
16965 });
16966 });
16967 return cell;
16968 }
16969 function buildRoleCell(row, cfg) {
16970 const cell = document.createElement("span");
16971 cell.style.cssText = "display:inline-flex;flex-wrap:wrap;gap:4px;min-width:0;";
16972 const roles = Array.isArray(row.roles) ? row.roles : [];
16973 const labels = cfg.allRoles ?? {};
16974 if (roles.length === 0) {
16975 const none = document.createElement("span");
16976 none.textContent = __("No role");
16977 none.style.cssText = "color:var(--wp-admin-theme-fg-muted, #8c8f94);font-style:italic;";
16978 cell.appendChild(none);
16979 return cell;
16980 }
16981 for (const slug of roles) {
16982 const chip = document.createElement("span");
16983 chip.textContent = labels[slug] ?? slug;
16984 chip.style.cssText = [
16985 "display:inline-flex",
16986 "align-items:center",
16987 "padding:2px 8px",
16988 "border-radius:10px",
16989 "font-size:11px",
16990 "font-weight:600",
16991 "background:rgba(34,113,177,0.10)",
16992 "color:#0a4b78",
16993 "white-space:nowrap"
16994 ].join(";");
16995 cell.appendChild(chip);
16996 }
16997 return cell;
16998 }
16999 function buildStatsCell(row) {
17000 const stats = row.desktop_mode_user_stats ?? {
17001 posts: 0,
17002 pages: 0,
17003 comments: 0
17004 };
17005 const cell = document.createElement("span");
17006 cell.style.cssText = "display:inline-flex;align-items:center;gap:10px;font-size:12px;font-variant-numeric:tabular-nums;";
17007 const mk = (dashicon, count, label) => {
17008 const span = document.createElement("span");
17009 span.style.cssText = "display:inline-flex;align-items:center;gap:3px;";
17010 span.title = label;
17011 const ic = document.createElement("wpd-icon");
17012 ic.setAttribute("name", dashicon);
17013 ic.setAttribute("size", "14");
17014 ic.style.color = "var(--wp-admin-theme-fg-muted, #8c8f94)";
17015 span.appendChild(ic);
17016 const txt = document.createElement("span");
17017 txt.textContent = String(count);
17018 if (count === 0) {
17019 txt.style.color = "var(--wp-admin-theme-fg-muted, #8c8f94)";
17020 }
17021 span.appendChild(txt);
17022 return span;
17023 };
17024 cell.appendChild(mk("admin-post", stats.posts, __("Posts")));
17025 cell.appendChild(mk("admin-page", stats.pages, __("Pages")));
17026 cell.appendChild(
17027 mk("admin-comments", stats.comments, __("Comments"))
17028 );
17029 return cell;
17030 }
17031 function relativeTime(ts) {
17032 const now = Math.floor(Date.now() / 1e3);
17033 const delta = now - ts;
17034 if (delta < 60) {
17035 return __("just now");
17036 }
17037 if (delta < 3600) {
17038 const m = Math.floor(delta / 60);
17039 return sprintf(__("%d min ago"), m);
17040 }
17041 if (delta < 86400) {
17042 const h = Math.floor(delta / 3600);
17043 return sprintf(__("%d h ago"), h);
17044 }
17045 if (delta < 86400 * 30) {
17046 const d = Math.floor(delta / 86400);
17047 return sprintf(__("%d d ago"), d);
17048 }
17049 if (delta < 86400 * 365) {
17050 const mo = Math.floor(delta / (86400 * 30));
17051 return sprintf(__("%d mo ago"), mo);
17052 }
17053 const y = Math.floor(delta / (86400 * 365));
17054 return sprintf(__("%d y ago"), y);
17055 }
17056 function buildLastLoginCell(row) {
17057 const cell = document.createElement("span");
17058 cell.style.cssText = "font-size:13px;font-variant-numeric:tabular-nums;";
17059 const ts = row.desktop_mode_last_login;
17060 if (!ts || typeof ts !== "number") {
17061 cell.textContent = __("Never");
17062 cell.style.color = "var(--wp-admin-theme-fg-muted, #8c8f94)";
17063 return cell;
17064 }
17065 cell.textContent = relativeTime(ts);
17066 const dt = new Date(ts * 1e3);
17067 cell.title = dt.toLocaleString();
17068 return cell;
17069 }
17070 function buildRegisteredCell(row) {
17071 const cell = document.createElement("span");
17072 cell.style.cssText = "font-size:13px;font-variant-numeric:tabular-nums;";
17073 const raw = typeof row.registered_date === "string" ? row.registered_date : "";
17074 if (!raw) {
17075 cell.textContent = "—";
17076 cell.style.color = "var(--wp-admin-theme-fg-muted, #8c8f94)";
17077 return cell;
17078 }
17079 const hasTz = /[Zz]|[+-]\d{2}:?\d{2}$/.test(raw);
17080 const ts = Math.floor(Date.parse(hasTz ? raw : raw + "Z") / 1e3);
17081 if (!Number.isFinite(ts)) {
17082 cell.textContent = raw;
17083 return cell;
17084 }
17085 cell.textContent = relativeTime(ts);
17086 cell.title = new Date(ts * 1e3).toLocaleString();
17087 return cell;
17088 }
17089 function buildActionsCell(row, cfg, client) {
17090 const cell = document.createElement("span");
17091 cell.style.cssText = "display:inline-flex;gap:4px;align-items:center;";
17092 const canEditViewer = cfg.canEdit === true;
17093 const canEditRow = row.desktop_mode_can_edit === true;
17094 if (!canEditViewer || !canEditRow) {
17095 cell.textContent = "—";
17096 cell.style.color = "var(--wp-admin-theme-fg-muted, #8c8f94)";
17097 return cell;
17098 }
17099 const mk = (label, dashicon, fn) => {
17100 const btn = document.createElement("button");
17101 btn.type = "button";
17102 btn.title = label;
17103 btn.setAttribute("aria-label", label);
17104 Object.assign(btn.style, {
17105 appearance: "none",
17106 border: "1px solid var(--wp-admin-theme-border, #dcdcde)",
17107 background: "var(--wp-admin-theme-bg, #fff)",
17108 color: "inherit",
17109 padding: "4px 6px",
17110 borderRadius: "4px",
17111 cursor: "pointer",
17112 lineHeight: "1"
17113 });
17114 const ic = document.createElement("wpd-icon");
17115 ic.setAttribute("name", dashicon);
17116 ic.setAttribute("size", "14");
17117 btn.appendChild(ic);
17118 btn.addEventListener("click", (e) => {
17119 e.stopPropagation();
17120 fn();
17121 });
17122 return btn;
17123 };
17124 cell.appendChild(
17125 mk(
17126 __("Send password reset"),
17127 "email-alt",
17128 async () => {
17129 const ok = await wpdConfirmGlobal({
17130 title: __("Send password reset email?"),
17131 message: sprintf(
17132 // translators: %s is a user name.
17133 __("WordPress will email %s a password-reset link."),
17134 row.name
17135 ),
17136 confirmLabel: __("Send reset email")
17137 });
17138 if (!ok) {
17139 return;
17140 }
17141 const result = await client.sendPasswordReset(row.id);
17142 if (result.ok) {
17143 notifyToast(
17144 sprintf(
17145 // translators: %s is the user's email address.
17146 __("Reset email sent to %s."),
17147 result.email ?? row.email ?? ""
17148 ),
17149 { kind: "success" }
17150 );
17151 } else {
17152 notifyToast(
17153 sprintf(
17154 // translators: %s is an error code.
17155 __("Could not send reset email (%s)."),
17156 result.error ?? "unknown"
17157 ),
17158 { kind: "error" }
17159 );
17160 }
17161 }
17162 )
17163 );
17164 cell.appendChild(
17165 mk(
17166 __("Resend welcome email"),
17167 "megaphone",
17168 async () => {
17169 const ok = await wpdConfirmGlobal({
17170 title: __("Resend welcome email?"),
17171 message: sprintf(
17172 // translators: %s is a user name.
17173 __(
17174 "WordPress will resend the original welcome email to %s."
17175 ),
17176 row.name
17177 ),
17178 confirmLabel: __("Resend")
17179 });
17180 if (!ok) {
17181 return;
17182 }
17183 const result = await client.resendWelcome(row.id);
17184 if (result.ok) {
17185 notifyToast(
17186 sprintf(
17187 // translators: %s is the user's email address.
17188 __("Welcome email resent to %s."),
17189 result.email ?? row.email ?? ""
17190 ),
17191 { kind: "success" }
17192 );
17193 } else {
17194 notifyToast(
17195 sprintf(
17196 // translators: %s is an error code.
17197 __("Could not resend welcome (%s)."),
17198 result.error ?? "unknown"
17199 ),
17200 { kind: "error" }
17201 );
17202 }
17203 }
17204 )
17205 );
17206 return cell;
17207 }
17208 function buildColumns(cache, cfg, client) {
17209 const cols = [
17210 {
17211 key: "identity",
17212 label: __("Name"),
17213 sortable: false,
17214 sticky: true,
17215 minWidth: "260px",
17216 render: (_v, row) => memoUserCell(
17217 cache,
17218 row.id,
17219 "identity",
17220 () => buildIdentityCell(row, cfg)
17221 )
17222 },
17223 {
17224 key: "email",
17225 label: __("Email"),
17226 minWidth: "220px",
17227 render: (_v, row) => memoUserCell(cache, row.id, "email", () => buildEmailCell(row))
17228 },
17229 {
17230 key: "role",
17231 label: __("Role"),
17232 width: "180px",
17233 render: (_v, row) => memoUserCell(
17234 cache,
17235 row.id,
17236 "role",
17237 () => buildRoleCell(row, cfg)
17238 )
17239 },
17240 {
17241 key: "stats",
17242 label: __("Content"),
17243 width: "160px",
17244 sortValue: (row) => {
17245 const s = row.desktop_mode_user_stats;
17246 return s ? s.posts + s.pages + s.comments : 0;
17247 },
17248 render: (_v, row) => memoUserCell(cache, row.id, "stats", () => buildStatsCell(row))
17249 },
17250 {
17251 key: "last_login",
17252 label: __("Last login"),
17253 width: "140px",
17254 sortable: false,
17255 sortValue: (row) => typeof row.desktop_mode_last_login === "number" ? row.desktop_mode_last_login : 0,
17256 render: (_v, row) => memoUserCell(
17257 cache,
17258 row.id,
17259 "last_login",
17260 () => buildLastLoginCell(row)
17261 )
17262 },
17263 {
17264 key: "registered",
17265 label: __("Registered"),
17266 width: "140px",
17267 sortable: true,
17268 render: (_v, row) => memoUserCell(
17269 cache,
17270 row.id,
17271 "registered",
17272 () => buildRegisteredCell(row)
17273 )
17274 }
17275 ];
17276 if (cfg.canEdit === true) {
17277 cols.push({
17278 key: "actions",
17279 label: __("Actions"),
17280 width: "110px",
17281 sortable: false,
17282 render: (_v, row) => (
17283 // Actions cell is intentionally NOT memoized — its closure
17284 // captures `row` and the row payload changes between
17285 // fetches. Cheap to rebuild, fewer surprises.
17286 buildActionsCell(row, cfg, client)
17287 )
17288 });
17289 }
17290 return cols;
17291 }
17292 function defaultStatusSegments() {
17293 return [
17294 { value: "", label: __("All") },
17295 { value: "online", label: __("Online") },
17296 { value: "recent", label: __("Active 30d") },
17297 { value: "never", label: __("Never logged in") }
17298 ];
17299 }
17300 function applyClientStatusFilter(rows, status) {
17301 if (!status) {
17302 return rows;
17303 }
17304 if (status === "online") {
17305 return rows.filter((r) => r.desktop_mode_presence === "online");
17306 }
17307 if (status === "recent") {
17308 const now = Math.floor(Date.now() / 1e3);
17309 return rows.filter((r) => {
17310 const ts = r.desktop_mode_last_login;
17311 return typeof ts === "number" && ts > 0 && now - ts < 86400 * 30;
17312 });
17313 }
17314 if (status === "never") {
17315 return rows.filter(
17316 (r) => !r.desktop_mode_last_login || typeof r.desktop_mode_last_login !== "number"
17317 );
17318 }
17319 return rows;
17320 }
17321 async function renderUsersWindow(body, client) {
17322 const root = body.querySelector(ROOT);
17323 const table = body.querySelector(TABLE);
17324 if (!root || !table) {
17325 return;
17326 }
17327 table.addEventListener("wpd-table-row-click", (e) => {
17328 const detail = e.detail;
17329 const id = detail?.row?.id;
17330 if (typeof id !== "number" || id <= 0) {
17331 return;
17332 }
17333 void openUserEditWindow(id);
17334 });
17335 maybeShowUsersIntro(client);
17336 const cfg = client.getConfig();
17337 const view = {
17338 page: 1,
17339 perPage: Math.max(1, cfg.defaultPerPage || 20),
17340 search: "",
17341 status: "",
17342 orderby: "name",
17343 order: "asc",
17344 roles: [],
17345 searchDebounce: null
17346 };
17347 const cellCache = /* @__PURE__ */ new Map();
17348 table.columns = buildColumns(cellCache, cfg, client);
17349 table.getRowId = (row) => row.id;
17350 table.sort = { key: "name", direction: "asc" };
17351 if (!cfg.canEdit && !cfg.canPromote && !cfg.canDelete) {
17352 table.removeAttribute("selectable");
17353 }
17354 let totalPages = 0;
17355 let totalRows = 0;
17356 let refreshSeq = 0;
17357 const perPageEl = root.querySelector(PER_PAGE);
17358 if (perPageEl) {
17359 perPageEl.value = String(view.perPage);
17360 }
17361 const indicator = root.querySelector(PAGE_INDICATOR);
17362 const prevBtn = root.querySelector(PREV);
17363 const nextBtn = root.querySelector(NEXT);
17364 const bulkBar = root.querySelector(BULK);
17365 const countEl = root.querySelector(COUNT);
17366 const bulkActionsHost = root.querySelector(BULK_ACTIONS_HOST);
17367 const statusHost = root.querySelector(STATUS);
17368 if (statusHost) {
17369 statusHost.replaceChildren();
17370 for (const seg of defaultStatusSegments()) {
17371 const el = document.createElement("wpd-segment");
17372 el.setAttribute("value", seg.value);
17373 el.textContent = seg.label;
17374 statusHost.appendChild(el);
17375 }
17376 statusHost.addEventListener("wpd-pick", (e) => {
17377 const detail = e.detail;
17378 view.status = detail?.value ?? "";
17379 view.page = 1;
17380 void refresh();
17381 });
17382 }
17383 const searchEl = root.querySelector(SEARCH);
17384 if (searchEl) {
17385 searchEl.addEventListener("input", () => {
17386 if (view.searchDebounce !== null) {
17387 clearTimeout(view.searchDebounce);
17388 }
17389 view.searchDebounce = window.setTimeout(() => {
17390 view.search = searchEl.value.trim();
17391 view.page = 1;
17392 void refresh();
17393 }, SEARCH_DEBOUNCE_MS);
17394 });
17395 }
17396 const refreshBtn = root.querySelector(REFRESH);
17397 refreshBtn?.addEventListener("click", () => {
17398 void refresh();
17399 });
17400 const newBtn = root.querySelector(NEW_BTN);
17401 if (newBtn) {
17402 if (!cfg.canCreate) {
17403 newBtn.style.display = "none";
17404 } else {
17405 newBtn.addEventListener("click", (e) => {
17406 e.preventDefault();
17407 const tabs = body.querySelector(
17408 "[data-desktop-mode-users-tabs]"
17409 );
17410 if (!tabs) {
17411 return;
17412 }
17413 tabs.value = "add-new";
17414 tabs.setAttribute("value", "add-new");
17415 });
17416 }
17417 }
17418 perPageEl?.addEventListener("change", () => {
17419 const n = parseInt(perPageEl.value, 10);
17420 if (Number.isFinite(n) && n > 0) {
17421 view.perPage = n;
17422 view.page = 1;
17423 void refresh();
17424 }
17425 });
17426 const renderBulkBar = () => {
17427 if (!bulkBar || !bulkActionsHost) {
17428 return;
17429 }
17430 const sel = table.selection;
17431 const ids = sel ? Array.from(sel) : [];
17432 if (ids.length === 0) {
17433 bulkBar.hidden = true;
17434 return;
17435 }
17436 bulkBar.hidden = false;
17437 if (countEl) {
17438 countEl.textContent = sprintf(
17439 // translators: %d is a count of selected users.
17440 __("%d selected"),
17441 ids.length
17442 );
17443 }
17444 bulkActionsHost.replaceChildren();
17445 const assignable = cfg.assignableRoles ?? {};
17446 const assignableKeys = Object.keys(assignable);
17447 if (cfg.canPromote && assignableKeys.length > 0) {
17448 const wrap = document.createElement("span");
17449 wrap.style.cssText = "display:inline-flex;align-items:center;gap:6px;";
17450 const roleDropdown = document.createElement("select");
17451 Object.assign(roleDropdown.style, {
17452 padding: "4px 8px",
17453 borderRadius: "4px",
17454 border: "1px solid var(--wp-admin-theme-border, #dcdcde)",
17455 background: "var(--wp-admin-theme-bg, #fff)",
17456 color: "inherit",
17457 font: "inherit",
17458 fontSize: "13px"
17459 });
17460 const placeholder = document.createElement("option");
17461 placeholder.value = "";
17462 placeholder.textContent = __("Set role to…");
17463 roleDropdown.appendChild(placeholder);
17464 for (const slug of assignableKeys) {
17465 const opt = document.createElement("option");
17466 opt.value = slug;
17467 opt.textContent = assignable[slug];
17468 roleDropdown.appendChild(opt);
17469 }
17470 const apply = document.createElement("wpd-button");
17471 apply.setAttribute("variant", "primary");
17472 apply.textContent = __("Apply");
17473 apply.addEventListener("click", async (e) => {
17474 e.preventDefault();
17475 const role = roleDropdown.value;
17476 if (!role) {
17477 return;
17478 }
17479 const ok = await wpdConfirmGlobal({
17480 title: __("Change role for selected users?"),
17481 message: sprintf(
17482 // translators: %1$d is a user count, %2$s is a role label.
17483 __("Set %1$d user(s)' role to %2$s?"),
17484 ids.length,
17485 assignable[role]
17486 ),
17487 confirmLabel: __("Set role")
17488 });
17489 if (!ok) {
17490 return;
17491 }
17492 const out = await client.bulkSetRole(ids, role).catch((err) => {
17493 notifyToast(
17494 String(err.message ?? err),
17495 { kind: "error" }
17496 );
17497 return null;
17498 });
17499 if (!out) {
17500 return;
17501 }
17502 const successes = Object.values(out.results).filter(
17503 (r) => r.ok
17504 ).length;
17505 const failures = ids.length - successes;
17506 if (successes > 0) {
17507 notifyToast(
17508 sprintf(
17509 // translators: %1$d users updated, %2$d failed.
17510 __("Role updated for %1$d user(s) (%2$d skipped)."),
17511 successes,
17512 failures
17513 ),
17514 { kind: failures > 0 ? "info" : "success" }
17515 );
17516 } else {
17517 notifyToast(__("No users updated."), { kind: "error" });
17518 }
17519 void refresh();
17520 });
17521 wrap.appendChild(roleDropdown);
17522 wrap.appendChild(apply);
17523 bulkActionsHost.appendChild(wrap);
17524 }
17525 };
17526 table.addEventListener("wpd-table-selection-change", renderBulkBar);
17527 prevBtn?.addEventListener("click", () => {
17528 if (view.page > 1) {
17529 view.page -= 1;
17530 void refresh();
17531 }
17532 });
17533 nextBtn?.addEventListener("click", () => {
17534 if (view.page < totalPages) {
17535 view.page += 1;
17536 void refresh();
17537 }
17538 });
17539 const updatePager = () => {
17540 if (indicator) {
17541 indicator.textContent = sprintf(
17542 // translators: %1$d current page, %2$d total pages, %3$d total rows.
17543 __("Page %1$d of %2$d · %3$d users"),
17544 view.page,
17545 Math.max(1, totalPages),
17546 totalRows
17547 );
17548 }
17549 if (prevBtn) {
17550 prevBtn.disabled = view.page <= 1;
17551 }
17552 if (nextBtn) {
17553 nextBtn.disabled = view.page >= totalPages;
17554 }
17555 };
17556 const buildParams = () => {
17557 return {
17558 page: view.page,
17559 perPage: view.perPage,
17560 search: view.search || void 0,
17561 roles: view.roles.length > 0 ? view.roles : void 0,
17562 orderby: view.orderby,
17563 order: view.order
17564 };
17565 };
17566 const refresh = async () => {
17567 const mySeq = ++refreshSeq;
17568 table.toggleAttribute("loading", true);
17569 try {
17570 const result = await client.fetchUsers(buildParams());
17571 if (mySeq !== refreshSeq) {
17572 return;
17573 }
17574 if (result.items.length === 0 && view.page > 1 && result.totalPages > 0 && view.page > result.totalPages) {
17575 view.page = 1;
17576 await refresh();
17577 return;
17578 }
17579 cellCache.clear();
17580 const filtered = applyClientStatusFilter(result.items, view.status);
17581 table.data = filtered;
17582 totalRows = result.total;
17583 totalPages = result.totalPages;
17584 updatePager();
17585 renderBulkBar();
17586 } catch (err) {
17587 console.error("[users-window] fetch failed:", err);
17588 notifyToast(
17589 __("Could not load users. Try Refresh."),
17590 { kind: "error" }
17591 );
17592 } finally {
17593 table.toggleAttribute("loading", false);
17594 }
17595 };
17596 mountAddUserForm(body, client, cfg, {
17597 afterCreate: () => {
17598 const tabs = body.querySelector(
17599 "[data-desktop-mode-users-tabs]"
17600 );
17601 if (tabs) {
17602 tabs.value = "all";
17603 tabs.setAttribute("value", "all");
17604 }
17605 view.page = 1;
17606 void refresh();
17607 }
17608 });
17609 wireProfileSubTab(body, cfg);
17610 const patchUserRow = async (id) => {
17611 try {
17612 const updated = await client.fetchOneUser(id);
17613 const list = table.data;
17614 const idx = list.findIndex((r) => r.id === id);
17615 if (idx < 0) {
17616 return;
17617 }
17618 if (!updated) {
17619 const next2 = list.slice();
17620 next2.splice(idx, 1);
17621 table.data = next2;
17622 return;
17623 }
17624 for (const k of Array.from(cellCache.keys())) {
17625 if (k.startsWith(`${id}::`)) {
17626 cellCache.delete(k);
17627 }
17628 }
17629 const next = list.slice();
17630 next[idx] = updated;
17631 table.data = applyClientStatusFilter(next, view.status);
17632 } catch (err) {
17633 console.warn("[users-window] row patch failed, falling back to refresh", err);
17634 void refresh();
17635 }
17636 };
17637 const subscribeApi = window.wp?.desktop;
17638 const unsubscribe = subscribeApi?.subscribe?.(
17639 "desktop-mode.user.changed",
17640 (payload) => {
17641 const ids = payload?.ids;
17642 if (!Array.isArray(ids)) {
17643 return;
17644 }
17645 for (const raw of ids) {
17646 const id = typeof raw === "number" ? raw : Number(raw);
17647 if (Number.isFinite(id) && id > 0) {
17648 void patchUserRow(id);
17649 }
17650 }
17651 }
17652 );
17653 if (unsubscribe) {
17654 document.addEventListener(
17655 "desktop-mode-window-closed",
17656 (e) => {
17657 const detail = e.detail;
17658 if (detail?.windowId === "desktop-mode-users") {
17659 unsubscribe();
17660 }
17661 },
17662 { once: false }
17663 );
17664 }
17665 void refresh();
17666 }
17667 function wireProfileSubTab(body, cfg) {
17668 const profile = body.querySelector(
17669 "wpd-user-profile[data-wpd-user-profile-self]"
17670 );
17671 if (!profile) {
17672 return;
17673 }
17674 const viewerId = cfg.currentUserId;
17675 if (typeof viewerId === "number" && viewerId > 0) {
17676 profile.setAttribute("user-id", String(viewerId));
17677 }
17678 }
17679 function mountAddUserForm(body, client, cfg, opts) {
17680 const formNullable = body.querySelector(
17681 "[data-desktop-mode-users-add-form]"
17682 );
17683 if (!formNullable) {
17684 return;
17685 }
17686 const form = formNullable;
17687 const defaultRole = cfg.defaultRole ?? "subscriber";
17688 const assignableRoles = cfg.assignableRoles && Object.keys(cfg.assignableRoles).length > 0 ? cfg.assignableRoles : { [defaultRole]: defaultRole };
17689 mountSelect(form, "role", __("Role"), assignableRoles, defaultRole);
17690 mountSelect(
17691 form,
17692 "locale",
17693 __("Language"),
17694 cfg.locales ?? { "": __("Site default") },
17695 ""
17696 );
17697 const generateBtn = form.querySelector(
17698 '[data-action="generate-password"]'
17699 );
17700 generateBtn?.addEventListener("click", (e) => {
17701 e.preventDefault();
17702 e.stopPropagation();
17703 const pwd = generateStrongPassword(18);
17704 const pwdField = form.querySelector(
17705 'wpd-text-field[name="password"]'
17706 );
17707 if (pwdField) {
17708 pwdField.value = pwd;
17709 pwdField.setAttribute("value", pwd);
17710 }
17711 void navigator.clipboard?.writeText(pwd).catch(() => {
17712 });
17713 notifyToast(__("Generated password copied to clipboard."), {
17714 kind: "success"
17715 });
17716 });
17717 let pending = false;
17718 form.addEventListener("wpd-form-submit", (e) => {
17719 const detail = e.detail;
17720 void onSubmit(detail.values);
17721 });
17722 async function onSubmit(values) {
17723 if (pending) {
17724 return;
17725 }
17726 pending = true;
17727 form.setBusy(true);
17728 form.clearErrors();
17729 const payload = {
17730 username: String(values.username ?? "").trim(),
17731 email: String(values.email ?? "").trim(),
17732 first_name: optionalString(values.first_name),
17733 last_name: optionalString(values.last_name),
17734 url: optionalString(values.url),
17735 locale: String(values.locale ?? ""),
17736 password: optionalString(values.password),
17737 role: optionalString(values.role),
17738 send_notification: Boolean(values.send_notification)
17739 };
17740 const result = await client.createUser(payload);
17741 pending = false;
17742 form.setBusy(false);
17743 if (!result.ok) {
17744 handleCreateError(form, result.error, result.message, payload);
17745 return;
17746 }
17747 notifyToast(
17748 sprintf(
17749 // translators: %s is the user's email address.
17750 __("User created — welcome email sent to %s."),
17751 result.email ?? payload.email
17752 ),
17753 { kind: "success" }
17754 );
17755 opts.afterCreate();
17756 }
17757 }
17758 function mountSelect(form, name, _label, optionsMap, initialValue) {
17759 const select = form.querySelector(
17760 `wpd-select[name="${name}"]`
17761 );
17762 if (!select) {
17763 return;
17764 }
17765 const items = Object.entries(optionsMap).map(([value, label]) => ({
17766 value,
17767 label
17768 }));
17769 select.items = items;
17770 if (initialValue && optionsMap[initialValue] !== void 0) {
17771 select.value = initialValue;
17772 select.setAttribute("value", initialValue);
17773 }
17774 }
17775 function handleCreateError(form, code, message, payload) {
17776 let summary = message;
17777 if (!summary) {
17778 switch (code) {
17779 case "desktop_mode_users_username_exists":
17780 case "existing_user_login":
17781 summary = __("That username is already in use.");
17782 break;
17783 case "desktop_mode_users_email_exists":
17784 case "existing_user_email":
17785 summary = __("That email is already in use.");
17786 break;
17787 case "desktop_mode_users_username_invalid":
17788 summary = __("Username is not valid.");
17789 break;
17790 case "desktop_mode_users_email_invalid":
17791 summary = __("A valid email address is required.");
17792 break;
17793 case "desktop_mode_users_role_forbidden":
17794 summary = __("You are not allowed to assign that role.");
17795 break;
17796 default:
17797 summary = __("Could not create the user.");
17798 }
17799 }
17800 form.setError(summary);
17801 if (code === "desktop_mode_users_username_exists" || code === "existing_user_login" || code === "desktop_mode_users_username_invalid") {
17802 form.setFieldInvalid("username");
17803 }
17804 if (code === "desktop_mode_users_email_exists" || code === "existing_user_email" || code === "desktop_mode_users_email_invalid") {
17805 form.setFieldInvalid("email");
17806 }
17807 if (code === "desktop_mode_users_role_forbidden") {
17808 form.setFieldInvalid("role");
17809 }
17810 notifyToast(summary, { kind: "error" });
17811 console.warn("[users-window] create failed", { code, payload });
17812 }
17813 function optionalString(value) {
17814 if (typeof value !== "string") {
17815 return void 0;
17816 }
17817 const trimmed = value.trim();
17818 return trimmed === "" ? void 0 : trimmed;
17819 }
17820 function generateStrongPassword(length) {
17821 const upper = "ABCDEFGHJKLMNPQRSTUVWXYZ";
17822 const lower = "abcdefghjkmnpqrstuvwxyz";
17823 const digits = "23456789";
17824 const symbols = "!@#$%^&*-_=+";
17825 const all = upper + lower + digits + symbols;
17826 const buf = new Uint32Array(length);
17827 crypto.getRandomValues(buf);
17828 let out = "";
17829 for (let i = 0; i < length; i += 1) {
17830 out += all[buf[i] % all.length];
17831 }
17832 return out;
17833 }
17834 const usersRender = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
17835 __proto__: null,
17836 renderUsersWindow
17837 }, Symbol.toStringTag, { value: "Module" }));
17838 exports.renderPostsWindow = renderPostsWindow;
17839 Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
17840 return exports;
17841 }({});
17842