PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.8.7
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.8.7
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.7, at assets/js/posts-window.js

17,839 lines 607.2 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 name = this._attr("name") || "";
7151 const rows = Number(this._attr("rows")) || 3;
7152 const maxLength = this._attr("maxlength");
7153 const minLength = this._attr("minlength");
7154 const invalid = this._boolAttr("invalid");
7155 const hostId = this.id || "wpd-unnamed";
7156 const fieldId = `${hostId}__field`;
7157 return html`
7158 ${label ? html`<label class="wpd-textarea__label" for=${fieldId}>${label}</label>` : html``}
7159 <textarea
7160 id=${fieldId}
7161 part="textarea"
7162 .value=${value}
7163 placeholder=${placeholder}
7164 ?disabled=${disabled}
7165 ?readonly=${readonly}
7166 rows=${rows}
7167 maxlength=${maxLength ?? ""}
7168 minlength=${minLength ?? ""}
7169 name=${name}
7170 aria-invalid=${invalid ? "true" : "false"}
7171 aria-label=${label || ""}
7172 @input=${(e) => this._onInput(e)}
7173 @change=${(e) => this._onChange(e)}
7174 @keydown=${(e) => this._onKeyDown(e)}
7175 ></textarea>
7176 `;
7177 }
7178 _attr(name) {
7179 return this.getAttribute(name);
7180 }
7181 _boolAttr(name) {
7182 return this.getAttribute(name) !== null;
7183 }
7184 _onInput(e) {
7185 const ta = e.target;
7186 this._textareaEl = ta;
7187 this.setAttribute("value", ta.value);
7188 this.emit("wpd-input-change", { value: ta.value });
7189 if (this._boolAttr("auto-grow")) {
7190 this._autosize(ta);
7191 }
7192 }
7193 _onChange(e) {
7194 const ta = e.target;
7195 this.emit("wpd-input-commit", { value: ta.value });
7196 }
7197 _onKeyDown(e) {
7198 if (!this._boolAttr("submit-on-enter")) {
7199 return;
7200 }
7201 if (e.key === "Enter" && !e.shiftKey && !e.altKey && !e.metaKey && !e.ctrlKey) {
7202 e.preventDefault();
7203 const ta = e.target;
7204 this.emit("wpd-submit", { value: ta.value });
7205 }
7206 }
7207 /**
7208 * Grow the textarea height to fit content, capped at `max-rows`.
7209 * Resets to scroll-height each input then clamps; cheap because
7210 * the browser caches layout.
7211 */
7212 _autosize(ta) {
7213 const maxRows = Number(this._attr("max-rows")) || 8;
7214 const cs = window.getComputedStyle(ta);
7215 const fontSize = parseFloat(cs.fontSize) || 13;
7216 const lineHeightRaw = cs.lineHeight;
7217 const lineHeight = lineHeightRaw === "normal" ? fontSize * 1.45 : parseFloat(lineHeightRaw) || fontSize * 1.45;
7218 const paddingTop = parseFloat(cs.paddingTop) || 0;
7219 const paddingBottom = parseFloat(cs.paddingBottom) || 0;
7220 const max = lineHeight * maxRows + paddingTop + paddingBottom;
7221 ta.style.height = "auto";
7222 const next = Math.min(ta.scrollHeight, max);
7223 ta.style.height = `${next}px`;
7224 }
7225 /** Public helper for callers that programmatically set `.value` and want autosize to re-run. */
7226 refreshAutosize() {
7227 if (this._textareaEl && this._boolAttr("auto-grow")) {
7228 this._autosize(this._textareaEl);
7229 }
7230 }
7231 /** Imperatively focus the underlying textarea. */
7232 focusInput() {
7233 const root = this.shadowRoot ?? this;
7234 const ta = root.querySelector("textarea");
7235 ta?.focus();
7236 }
7237 /** Imperatively clear the value. */
7238 clear() {
7239 this.setAttribute("value", "");
7240 const root = this.shadowRoot ?? this;
7241 const ta = root.querySelector("textarea");
7242 if (ta) {
7243 ta.value = "";
7244 if (this._boolAttr("auto-grow")) {
7245 this._autosize(ta);
7246 }
7247 }
7248 }
7249 };
7250 _WpdTextarea.props = [
7251 "label",
7252 "value",
7253 "placeholder",
7254 "disabled",
7255 "readonly",
7256 "name",
7257 "rows",
7258 "maxlength",
7259 "minlength",
7260 "invalid",
7261 "autoGrow",
7262 "maxRows",
7263 "submitOnEnter"
7264 ];
7265 _WpdTextarea.styles = [textareaStyles];
7266 _WpdTextarea.help = {
7267 title: "Textarea",
7268 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).",
7269 status: "stable",
7270 since: "0.22.0",
7271 props: [
7272 { name: "label", type: "string", description: "Visible label above the textarea." },
7273 { name: "value", type: "string", description: "Current value; reflected two-way." },
7274 { name: "placeholder", type: "string", description: "Native placeholder." },
7275 { name: "disabled", type: "boolean attribute" },
7276 { name: "readonly", type: "boolean attribute" },
7277 { name: "name", type: "string", description: "Forwarded to native textarea for form submission." },
7278 { name: "rows", type: "integer (string)", default: "3", description: "Initial visible row count." },
7279 { name: "maxlength", type: "integer (string)" },
7280 { name: "minlength", type: "integer (string)" },
7281 { name: "invalid", type: "boolean attribute", description: "Sets aria-invalid + error styling." },
7282 { name: "auto-grow", type: "boolean attribute", description: "Grows up to max-rows as the user types." },
7283 { name: "max-rows", type: "integer (string)", default: "8" },
7284 {
7285 name: "submit-on-enter",
7286 type: "boolean attribute",
7287 description: "Enter fires wpd-submit; Shift+Enter inserts a newline."
7288 }
7289 ],
7290 events: [
7291 { name: "wpd-input-change", description: "Fires on every keystroke.", detail: "{ value: string }" },
7292 { name: "wpd-input-commit", description: "Fires on blur / native change.", detail: "{ value: string }" },
7293 {
7294 name: "wpd-submit",
7295 description: "Fires on Enter (without Shift) when submit-on-enter is set.",
7296 detail: "{ value: string }"
7297 }
7298 ],
7299 example: html`
7300 <wpd-textarea label="Message" rows="3" auto-grow max-rows="8" submit-on-enter></wpd-textarea>
7301 `
7302 };
7303 let WpdTextarea = _WpdTextarea;
7304 defineComponent("wpd-textarea", WpdTextarea);
7305 let _mountsPromise = null;
7306 function loadMounts() {
7307 if (!_mountsPromise) {
7308 _mountsPromise = Promise.resolve().then(() => userEditRender);
7309 }
7310 return _mountsPromise;
7311 }
7312 class WpdUserProfile extends HTMLElement {
7313 constructor() {
7314 super(...arguments);
7315 this._initialized = false;
7316 this._mountedFor = null;
7317 }
7318 static get observedAttributes() {
7319 return ["user-id"];
7320 }
7321 connectedCallback() {
7322 if (!this._initialized) {
7323 this._initialized = true;
7324 this._renderShell();
7325 }
7326 void this._mountIfNeeded();
7327 }
7328 attributeChangedCallback(name, oldValue, newValue) {
7329 if (name !== "user-id" || oldValue === newValue) {
7330 return;
7331 }
7332 if (this._initialized) {
7333 void this._mountIfNeeded();
7334 }
7335 }
7336 /**
7337 * Build the layout shell (sidebar + main column + activity
7338 * region). Same class names as the inline Profile tab in the
7339 * Users window so the existing posts-window.css rules style
7340 * both contexts identically.
7341 */
7342 _renderShell() {
7343 this.classList.add("desktop-mode-user-profile");
7344 this.innerHTML = `
7345 <div class="desktop-mode-users__edit-layout" data-wpd-user-profile-layout>
7346 <aside class="desktop-mode-users__edit-aside" data-wpd-user-profile-aside></aside>
7347 <main class="desktop-mode-users__edit-main">
7348 <div data-wpd-user-profile-form></div>
7349 <div class="desktop-mode-users__edit-activity" data-wpd-user-profile-activity></div>
7350 </main>
7351 </div>
7352 `;
7353 }
7354 async _mountIfNeeded() {
7355 const userIdAttr = this.getAttribute("user-id");
7356 const userId = userIdAttr ? parseInt(userIdAttr, 10) : 0;
7357 if (!Number.isFinite(userId) || userId <= 0) {
7358 return;
7359 }
7360 if (userId === this._mountedFor) {
7361 return;
7362 }
7363 this._mountedFor = userId;
7364 const formHost = this.querySelector(
7365 "[data-wpd-user-profile-form]"
7366 );
7367 const asideHost = this.querySelector(
7368 "[data-wpd-user-profile-aside]"
7369 );
7370 const activityHost = this.querySelector(
7371 "[data-wpd-user-profile-activity]"
7372 );
7373 if (!formHost || !asideHost || !activityHost) {
7374 return;
7375 }
7376 const mounts = await loadMounts();
7377 void mounts.mountProfileFormAt(formHost, userId);
7378 void mounts.mountProfileAsideAt(asideHost, userId, false);
7379 void mounts.mountProfileActivityAt(activityHost, userId, false);
7380 }
7381 }
7382 if (typeof customElements !== "undefined" && !customElements.get("wpd-user-profile")) {
7383 customElements.define("wpd-user-profile", WpdUserProfile);
7384 }
7385 const FALLBACK_BASE = "http://localhost/";
7386 function joinRestUrl(restRoot, path) {
7387 const base = typeof window !== "undefined" && window.location ? window.location.href : FALLBACK_BASE;
7388 const url = new URL(restRoot, base);
7389 const trimmed = path.replace(/^\/+/, "");
7390 const queryAt = trimmed.indexOf("?");
7391 const route = queryAt === -1 ? trimmed : trimmed.slice(0, queryAt);
7392 const extraQuery = queryAt === -1 ? "" : trimmed.slice(queryAt + 1);
7393 if (url.searchParams.has("rest_route")) {
7394 const existing = url.searchParams.get("rest_route") ?? "/";
7395 const prefix = existing.endsWith("/") ? existing : existing + "/";
7396 url.searchParams.set("rest_route", prefix + route);
7397 } else {
7398 const pathname = url.pathname.endsWith("/") ? url.pathname : url.pathname + "/";
7399 url.pathname = pathname + route;
7400 }
7401 if (extraQuery) {
7402 const extras = new URLSearchParams(extraQuery);
7403 extras.forEach((value, key) => {
7404 url.searchParams.append(key, value);
7405 });
7406 }
7407 return url.toString();
7408 }
7409 function broadcastTermChange(taxonomy, action, id) {
7410 const api = window.wp?.desktop;
7411 if (api && typeof api.broadcast === "function") {
7412 api.broadcast("desktop-mode.term.changed", {
7413 source: "posts-window",
7414 taxonomy,
7415 action,
7416 id
7417 });
7418 }
7419 }
7420 function createPostsWindowClient(windowId) {
7421 const getConfig = () => {
7422 const store = window.desktopModeWindowConfig;
7423 const cfg = store ? store[windowId] : void 0;
7424 if (!cfg) {
7425 throw new Error(
7426 `[${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\`.`
7427 );
7428 }
7429 return cfg;
7430 };
7431 const shellFetch = (input, init) => {
7432 return trackedFetch(input, init, { windowId });
7433 };
7434 const request = async (url, init = {}) => {
7435 const cfg = getConfig();
7436 const response = await shellFetch(url, {
7437 ...init,
7438 credentials: "same-origin",
7439 headers: {
7440 "X-WP-Nonce": cfg.restNonce,
7441 Accept: "application/json",
7442 ...init.body ? { "Content-Type": "application/json" } : {},
7443 ...init.headers ?? {}
7444 }
7445 });
7446 if (!response.ok) {
7447 let message = `${response.status} ${response.statusText}`;
7448 try {
7449 const json = await response.json();
7450 if (json && typeof json.message === "string") {
7451 message = json.message;
7452 }
7453 } catch {
7454 }
7455 throw new Error(message);
7456 }
7457 const data = init.expectJson === false ? null : await response.json();
7458 return { data, headers: response.headers };
7459 };
7460 const fetchPosts = async (params = {}) => {
7461 const cfg = getConfig();
7462 const url = new URL(cfg.postsUrl);
7463 for (const [key, value] of Object.entries(cfg.queryArgs ?? {})) {
7464 if (typeof value === "string" && value !== "") {
7465 url.searchParams.set(key, value);
7466 }
7467 }
7468 if (params.page) {
7469 url.searchParams.set("page", String(params.page));
7470 }
7471 if (params.perPage) {
7472 url.searchParams.set("per_page", String(params.perPage));
7473 }
7474 if (params.search) {
7475 url.searchParams.set("search", params.search);
7476 }
7477 if (params.status) {
7478 url.searchParams.set("status", params.status);
7479 } else {
7480 url.searchParams.set("status", "any");
7481 }
7482 if (params.orderby) {
7483 url.searchParams.set("orderby", params.orderby);
7484 }
7485 if (params.order) {
7486 url.searchParams.set("order", params.order);
7487 }
7488 const appendIds = (key, v) => {
7489 const list = Array.isArray(v) ? v : [v];
7490 for (const id of list) {
7491 if (Number.isFinite(id) && id > 0) {
7492 url.searchParams.append(`${key}[]`, String(id));
7493 }
7494 }
7495 };
7496 if (params.author) {
7497 appendIds("author", params.author);
7498 }
7499 if (params.tag) {
7500 appendIds("tags", params.tag);
7501 }
7502 const { data, headers } = await request(
7503 url.toString(),
7504 { method: "GET" }
7505 );
7506 return {
7507 items: Array.isArray(data) ? data : [],
7508 total: parseInt(headers.get("X-WP-Total") ?? "0", 10) || 0,
7509 totalPages: parseInt(headers.get("X-WP-TotalPages") ?? "0", 10) || 0
7510 };
7511 };
7512 const trashPost = async (id) => {
7513 const cfg = getConfig();
7514 try {
7515 await request(`${cfg.postsUrl}/${id}`, {
7516 method: "DELETE"
7517 });
7518 return { id, ok: true };
7519 } catch (err) {
7520 return {
7521 id,
7522 ok: false,
7523 error: err instanceof Error ? err.message : String(err)
7524 };
7525 }
7526 };
7527 const buildEditPostUrl = (id) => {
7528 const cfg = getConfig();
7529 const sep = cfg.editPostUrlBase.includes("?") ? "&" : "?";
7530 return `${cfg.editPostUrlBase}${sep}post=${id}&action=edit`;
7531 };
7532 const searchTags = async (query, signal) => {
7533 const cfg = getConfig();
7534 const url = new URL(joinRestUrl(cfg.restRoot, "wp/v2/tags"));
7535 url.searchParams.set("per_page", "20");
7536 url.searchParams.set("_fields", "id,name,slug,count");
7537 url.searchParams.set("orderby", "count");
7538 url.searchParams.set("order", "desc");
7539 if (query) {
7540 url.searchParams.set("search", query);
7541 url.searchParams.set("orderby", "name");
7542 url.searchParams.set("order", "asc");
7543 }
7544 const { data } = await request(url.toString(), {
7545 method: "GET",
7546 signal
7547 });
7548 return Array.isArray(data) ? data : [];
7549 };
7550 const createTag = async (name) => {
7551 const cfg = getConfig();
7552 const url = joinRestUrl(cfg.restRoot, "wp/v2/tags");
7553 try {
7554 const { data } = await request(url, {
7555 method: "POST",
7556 body: JSON.stringify({ name })
7557 });
7558 broadcastTermChange("post_tag", "created", data.id);
7559 return data;
7560 } catch (err) {
7561 const message = err instanceof Error ? err.message : String(err);
7562 if (/term[\s_]?exists/i.test(message)) {
7563 const matches = await searchTags(name);
7564 const exact = matches.find(
7565 (t) => t.name.toLowerCase() === name.toLowerCase()
7566 );
7567 if (exact) {
7568 return exact;
7569 }
7570 }
7571 throw err;
7572 }
7573 };
7574 const updatePostTags = async (postId, tagIds) => {
7575 const cfg = getConfig();
7576 const url = `${cfg.postsUrl}/${postId}`;
7577 const { data } = await request(url, {
7578 method: "POST",
7579 body: JSON.stringify({ tags: tagIds })
7580 });
7581 return data;
7582 };
7583 const fetchAllCategories = async (signal) => {
7584 const cfg = getConfig();
7585 const url = new URL(joinRestUrl(cfg.restRoot, "wp/v2/categories"));
7586 url.searchParams.set("per_page", "100");
7587 url.searchParams.set("_fields", "id,name,slug,parent");
7588 url.searchParams.set("orderby", "name");
7589 url.searchParams.set("order", "asc");
7590 const { data } = await request(url.toString(), {
7591 method: "GET",
7592 signal
7593 });
7594 return Array.isArray(data) ? data : [];
7595 };
7596 const fetchAuthorOptions = async (signal) => {
7597 const cfg = getConfig();
7598 const url = new URL(joinRestUrl(cfg.restRoot, "wp/v2/users"));
7599 url.searchParams.set("per_page", "100");
7600 url.searchParams.set("who", "authors");
7601 url.searchParams.set("_fields", "id,name");
7602 url.searchParams.set("orderby", "name");
7603 url.searchParams.set("order", "asc");
7604 try {
7605 const { data } = await request(url.toString(), {
7606 method: "GET",
7607 signal
7608 });
7609 return Array.isArray(data) ? data : [];
7610 } catch {
7611 return [];
7612 }
7613 };
7614 const fetchTagOptions = async (page = 1, perPage = 50, signal) => {
7615 const cfg = getConfig();
7616 const url = new URL(joinRestUrl(cfg.restRoot, "wp/v2/tags"));
7617 url.searchParams.set("per_page", String(Math.max(1, perPage)));
7618 url.searchParams.set("page", String(Math.max(1, page)));
7619 url.searchParams.set("_fields", "id,name,count");
7620 url.searchParams.set("orderby", "count");
7621 url.searchParams.set("order", "desc");
7622 try {
7623 const { data, headers } = await request(
7624 url.toString(),
7625 { method: "GET", signal }
7626 );
7627 return {
7628 items: Array.isArray(data) ? data : [],
7629 totalPages: parseInt(headers.get("X-WP-TotalPages") ?? "0", 10) || 0
7630 };
7631 } catch {
7632 return { items: [], totalPages: 0 };
7633 }
7634 };
7635 const createCategory = async (name, parent = 0, opts = {}) => {
7636 const cfg = getConfig();
7637 const url = joinRestUrl(cfg.restRoot, "wp/v2/categories");
7638 const body = { name, parent };
7639 if (opts.slug) {
7640 body.slug = opts.slug;
7641 }
7642 if (opts.description) {
7643 body.description = opts.description;
7644 }
7645 try {
7646 const { data } = await request(url, {
7647 method: "POST",
7648 body: JSON.stringify(body)
7649 });
7650 broadcastTermChange("category", "created", data.id);
7651 return data;
7652 } catch (err) {
7653 const message = err instanceof Error ? err.message : String(err);
7654 if (/term[\s_]?exists/i.test(message)) {
7655 const matches = await fetchAllCategories();
7656 const exact = matches.find(
7657 (t) => t.name.toLowerCase() === name.toLowerCase() && t.parent === parent
7658 );
7659 if (exact) {
7660 return exact;
7661 }
7662 }
7663 throw err;
7664 }
7665 };
7666 const updatePostCategories = async (postId, categoryIds) => {
7667 const cfg = getConfig();
7668 const url = `${cfg.postsUrl}/${postId}`;
7669 const { data } = await request(
7670 url,
7671 {
7672 method: "POST",
7673 body: JSON.stringify({ categories: categoryIds })
7674 }
7675 );
7676 return data;
7677 };
7678 const fetchTerms = async (taxonomy, params = {}) => {
7679 const cfg = getConfig();
7680 const url = new URL(joinRestUrl(cfg.restRoot, `wp/v2/${taxonomy}`));
7681 url.searchParams.set("per_page", String(params.perPage ?? 50));
7682 url.searchParams.set("page", String(params.page ?? 1));
7683 url.searchParams.set(
7684 "_fields",
7685 "id,name,slug,parent,count,description,desktop_mode_count,desktop_mode_is_default"
7686 );
7687 url.searchParams.set("orderby", params.orderby ?? "name");
7688 url.searchParams.set("order", params.order ?? "asc");
7689 if (params.search) {
7690 url.searchParams.set("search", params.search);
7691 }
7692 if (typeof params.parent === "number" && params.parent >= 0) {
7693 url.searchParams.set("parent", String(params.parent));
7694 }
7695 const { data, headers } = await request(
7696 url.toString(),
7697 { method: "GET" }
7698 );
7699 const items = Array.isArray(data) ? data.map((t) => {
7700 const anyCount = t.desktop_mode_count;
7701 const isDefault = t.desktop_mode_is_default === true;
7702 return {
7703 id: t.id ?? 0,
7704 name: t.name ?? "",
7705 slug: t.slug ?? "",
7706 parent: t.parent ?? 0,
7707 count: typeof anyCount === "number" ? anyCount : t.count ?? 0,
7708 description: t.description ?? "",
7709 isDefault
7710 };
7711 }) : [];
7712 return {
7713 items,
7714 total: parseInt(headers.get("X-WP-Total") ?? "0", 10) || 0,
7715 totalPages: parseInt(headers.get("X-WP-TotalPages") ?? "0", 10) || 0
7716 };
7717 };
7718 const fetchTagCooccurrence = async (taxonomy = "tags", limit = 8) => {
7719 const cfg = getConfig();
7720 const url = new URL(
7721 joinRestUrl(
7722 cfg.restRoot,
7723 "desktop-mode/v1/tag-cooccurrence"
7724 )
7725 );
7726 url.searchParams.set(
7727 "taxonomy",
7728 taxonomy === "tags" ? "post_tag" : "category"
7729 );
7730 url.searchParams.set("limit", String(limit));
7731 const { data } = await request(url.toString(), { method: "GET" });
7732 const out = /* @__PURE__ */ new Map();
7733 const pairs = data && typeof data === "object" && !Array.isArray(data) ? data.pairs : void 0;
7734 if (!pairs) {
7735 return out;
7736 }
7737 for (const [key, neighbors] of Object.entries(pairs)) {
7738 const id = parseInt(key, 10);
7739 if (!Number.isFinite(id) || id <= 0) {
7740 continue;
7741 }
7742 const clean = [];
7743 for (const raw of neighbors) {
7744 const nid = Number(raw?.id);
7745 const sh = Number(raw?.shared);
7746 if (Number.isFinite(nid) && nid > 0 && Number.isFinite(sh) && sh > 0) {
7747 clean.push({ id: nid, shared: sh });
7748 }
7749 }
7750 if (clean.length > 0) {
7751 out.set(id, clean);
7752 }
7753 }
7754 return out;
7755 };
7756 const updateTerm = async (taxonomy, id, patch) => {
7757 const cfg = getConfig();
7758 const url = joinRestUrl(cfg.restRoot, `wp/v2/${taxonomy}/${id}`);
7759 const { data } = await request(url, {
7760 method: "POST",
7761 body: JSON.stringify(patch)
7762 });
7763 broadcastTermChange(
7764 taxonomy === "categories" ? "category" : "post_tag",
7765 "updated",
7766 id
7767 );
7768 return {
7769 id: data.id ?? id,
7770 name: data.name ?? "",
7771 slug: data.slug ?? "",
7772 parent: data.parent ?? 0,
7773 count: data.count ?? 0,
7774 description: data.description ?? "",
7775 isDefault: data.isDefault ?? false
7776 };
7777 };
7778 const deleteTerm = async (taxonomy, id) => {
7779 const cfg = getConfig();
7780 const url = new URL(
7781 joinRestUrl(cfg.restRoot, `wp/v2/${taxonomy}/${id}`)
7782 );
7783 url.searchParams.set("force", "true");
7784 await request(url.toString(), { method: "DELETE" });
7785 broadcastTermChange(
7786 taxonomy === "categories" ? "category" : "post_tag",
7787 "deleted",
7788 id
7789 );
7790 };
7791 return {
7792 windowId,
7793 getConfig,
7794 fetchPosts,
7795 trashPost,
7796 buildEditPostUrl,
7797 searchTags,
7798 createTag,
7799 updatePostTags,
7800 fetchAllCategories,
7801 fetchAuthorOptions,
7802 fetchTagOptions,
7803 createCategory,
7804 updatePostCategories,
7805 fetchTerms,
7806 fetchTagCooccurrence,
7807 updateTerm,
7808 deleteTerm
7809 };
7810 }
7811 function createUsersWindowClient(windowId = "desktop-mode-users") {
7812 const getConfig = () => {
7813 const store = window.desktopModeWindowConfig;
7814 const cfg = store?.[windowId];
7815 if (!cfg) {
7816 throw new Error(
7817 `[${windowId}] config blob is missing — was the window opened without registration? See \`includes/users-window/window.php\`.`
7818 );
7819 }
7820 return cfg;
7821 };
7822 const shellFetch = (input, init, options) => {
7823 return trackedFetch(input, init, {
7824 windowId,
7825 source: options?.source ?? "users-window/rest",
7826 silent: options?.silent
7827 });
7828 };
7829 const fetchUsers = async (params) => {
7830 const cfg = getConfig();
7831 const baseUrl = cfg.usersUrl || cfg.postsUrl;
7832 const url = new URL(baseUrl);
7833 for (const [key, value] of Object.entries(cfg.queryArgs ?? {})) {
7834 if (typeof value === "string" && value !== "") {
7835 url.searchParams.set(key, value);
7836 }
7837 }
7838 url.searchParams.set("page", String(Math.max(1, params.page)));
7839 url.searchParams.set(
7840 "per_page",
7841 String(Math.max(1, params.perPage))
7842 );
7843 if (params.search) {
7844 url.searchParams.set("search", params.search);
7845 }
7846 if (params.roles && params.roles.length > 0) {
7847 for (const r of params.roles) {
7848 url.searchParams.append("roles", r);
7849 }
7850 }
7851 if (params.orderby) {
7852 url.searchParams.set("orderby", params.orderby);
7853 }
7854 if (params.order) {
7855 url.searchParams.set("order", params.order);
7856 }
7857 const res = await shellFetch(
7858 url.toString(),
7859 {
7860 method: "GET",
7861 credentials: "same-origin",
7862 headers: {
7863 Accept: "application/json",
7864 "X-WP-Nonce": cfg.restNonce
7865 }
7866 },
7867 { source: "users-window/list" }
7868 );
7869 if (!res.ok) {
7870 throw new Error(
7871 `[users-window] list fetch failed: ${res.status}`
7872 );
7873 }
7874 const items = await res.json();
7875 const total = parseInt(res.headers.get("X-WP-Total") ?? "0", 10);
7876 const totalPages = parseInt(
7877 res.headers.get("X-WP-TotalPages") ?? "0",
7878 10
7879 );
7880 return { items, total, totalPages };
7881 };
7882 const fetchOneUser = async (id) => {
7883 const cfg = getConfig();
7884 const baseUrl = cfg.usersUrl || cfg.postsUrl;
7885 const url = new URL(`${baseUrl.replace(/\/$/, "")}/${id}`);
7886 for (const [key, value] of Object.entries(cfg.queryArgs ?? {})) {
7887 if (typeof value === "string" && value !== "") {
7888 url.searchParams.set(key, value);
7889 }
7890 }
7891 const res = await shellFetch(
7892 url.toString(),
7893 {
7894 method: "GET",
7895 credentials: "same-origin",
7896 headers: {
7897 Accept: "application/json",
7898 "X-WP-Nonce": cfg.restNonce
7899 }
7900 },
7901 { source: "users-window/one", silent: true }
7902 );
7903 if (res.status === 404) {
7904 return null;
7905 }
7906 if (!res.ok) {
7907 throw new Error(
7908 `[users-window] one fetch failed: ${res.status}`
7909 );
7910 }
7911 return await res.json();
7912 };
7913 const bulkSetRole = async (ids, role) => {
7914 const cfg = getConfig();
7915 const url = cfg.bulkRoleUrl ?? joinRestUrl(cfg.restRoot, "desktop-mode/v1/users/bulk-role");
7916 const res = await shellFetch(
7917 url,
7918 {
7919 method: "POST",
7920 credentials: "same-origin",
7921 headers: {
7922 "Content-Type": "application/json",
7923 "X-WP-Nonce": cfg.restNonce
7924 },
7925 body: JSON.stringify({ ids, role })
7926 },
7927 { source: "users-window/bulk-role" }
7928 );
7929 if (!res.ok) {
7930 throw new Error(
7931 `[users-window] bulk-role failed: ${res.status}`
7932 );
7933 }
7934 return await res.json();
7935 };
7936 const sendPasswordReset = async (id) => {
7937 const cfg = getConfig();
7938 const base = cfg.sendResetUrlBase ?? joinRestUrl(cfg.restRoot, "desktop-mode/v1/users/");
7939 const res = await shellFetch(
7940 joinRestUrl(base, `${id}/send-password-reset`),
7941 {
7942 method: "POST",
7943 credentials: "same-origin",
7944 headers: {
7945 "Content-Type": "application/json",
7946 "X-WP-Nonce": cfg.restNonce
7947 }
7948 },
7949 { source: "users-window/send-password-reset" }
7950 );
7951 if (!res.ok) {
7952 const body = await res.json().catch(() => ({}));
7953 return {
7954 ok: false,
7955 error: typeof body.code === "string" ? body.code : `http_${res.status}`
7956 };
7957 }
7958 const data = await res.json();
7959 return { ok: data.ok === true, email: data.email };
7960 };
7961 const resendWelcome = async (id) => {
7962 const cfg = getConfig();
7963 const base = cfg.sendResetUrlBase ?? joinRestUrl(cfg.restRoot, "desktop-mode/v1/users/");
7964 const res = await shellFetch(
7965 joinRestUrl(base, `${id}/resend-welcome`),
7966 {
7967 method: "POST",
7968 credentials: "same-origin",
7969 headers: {
7970 "Content-Type": "application/json",
7971 "X-WP-Nonce": cfg.restNonce
7972 }
7973 },
7974 { source: "users-window/resend-welcome" }
7975 );
7976 if (!res.ok) {
7977 const body = await res.json().catch(() => ({}));
7978 return {
7979 ok: false,
7980 error: typeof body.code === "string" ? body.code : `http_${res.status}`
7981 };
7982 }
7983 const data = await res.json();
7984 return { ok: data.ok === true, email: data.email };
7985 };
7986 const createUser = async (body) => {
7987 const cfg = getConfig();
7988 const url = cfg.createUserUrl ?? joinRestUrl(cfg.restRoot, "desktop-mode/v1/users");
7989 const res = await shellFetch(
7990 url,
7991 {
7992 method: "POST",
7993 credentials: "same-origin",
7994 headers: {
7995 "Content-Type": "application/json",
7996 "X-WP-Nonce": cfg.restNonce
7997 },
7998 body: JSON.stringify(body)
7999 },
8000 { source: "users-window/create" }
8001 );
8002 if (!res.ok) {
8003 const data2 = await res.json().catch(() => ({}));
8004 const code = data2.code;
8005 const message = data2.message;
8006 return {
8007 ok: false,
8008 error: typeof code === "string" ? code : `http_${res.status}`,
8009 message: typeof message === "string" ? message : void 0
8010 };
8011 }
8012 const data = await res.json();
8013 return {
8014 ok: data.ok === true,
8015 user_id: data.user_id,
8016 email: data.email
8017 };
8018 };
8019 const bulkDeleteUsers = async (ids, reassign) => {
8020 const cfg = getConfig();
8021 const url = cfg.bulkDeleteUrl ?? joinRestUrl(cfg.restRoot, "desktop-mode/v1/users/bulk-delete");
8022 const body = { ids };
8023 if (typeof reassign === "number" && reassign > 0) {
8024 body.reassign = reassign;
8025 }
8026 const res = await shellFetch(
8027 url,
8028 {
8029 method: "POST",
8030 credentials: "same-origin",
8031 headers: {
8032 "Content-Type": "application/json",
8033 "X-WP-Nonce": cfg.restNonce
8034 },
8035 body: JSON.stringify(body)
8036 },
8037 { source: "users-window/bulk-delete" }
8038 );
8039 if (!res.ok) {
8040 throw new Error(
8041 `[users-window] bulk-delete failed: ${res.status}`
8042 );
8043 }
8044 return await res.json();
8045 };
8046 return {
8047 windowId,
8048 getConfig,
8049 fetchUsers,
8050 fetchOneUser,
8051 bulkSetRole,
8052 sendPasswordReset,
8053 resendWelcome,
8054 createUser,
8055 bulkDeleteUsers
8056 };
8057 }
8058 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}`;
8059 const _WpdButton = class _WpdButton extends Component {
8060 render() {
8061 const disabled = this.disabled !== null;
8062 const type = this.type || "button";
8063 return html`
8064 <button part="button" type=${type} ?disabled=${disabled}>
8065 <slot></slot>
8066 </button>
8067 `;
8068 }
8069 };
8070 _WpdButton.props = ["variant", "disabled", "type", "busy", "fill-cell"];
8071 _WpdButton.styles = [styles$2];
8072 _WpdButton.help = {
8073 title: "Button",
8074 summary: "Thin wrapper around <button> with consistent variant styling and a slot for the label.",
8075 status: "stable",
8076 since: "0.9.0",
8077 props: [
8078 {
8079 name: "variant",
8080 type: "'primary' | 'secondary' | 'ghost' | 'danger' | 'link'",
8081 default: "ghost",
8082 description: "Visual weight of the button. Use primary for the single attention-grabbing action per surface."
8083 },
8084 {
8085 name: "disabled",
8086 type: "boolean attribute",
8087 description: "Disable pointer + keyboard interaction and dim the chrome."
8088 },
8089 {
8090 name: "type",
8091 type: "'button' | 'submit' | 'reset'",
8092 default: "button",
8093 description: "Forwarded to the underlying native <button>."
8094 },
8095 {
8096 name: "busy",
8097 type: "boolean attribute",
8098 description: "Marks the button as in-progress (e.g., awaiting a fetch)."
8099 },
8100 {
8101 name: "fill-cell",
8102 type: "boolean attribute",
8103 description: "Grow to fill the parent flex/grid cell. Useful for tiled keypads."
8104 }
8105 ],
8106 slots: [{ name: "(default)", description: "Button label." }],
8107 parts: [{ name: "button", description: "Underlying <button> element." }],
8108 cssProps: [
8109 { name: "--wpd-button-bg", description: "Background color." },
8110 { name: "--wpd-button-fg", description: "Text color." },
8111 { name: "--wpd-button-border", description: "Border shorthand." },
8112 { name: "--wpd-button-border-radius", default: "6px" },
8113 { name: "--wpd-button-padding", default: "6px 12px" },
8114 {
8115 name: "--wpd-button-min-height",
8116 description: "Minimum height when fill-cell is set."
8117 }
8118 ],
8119 example: html`
8120 <wpd-cluster gap="8">
8121 <wpd-button variant="primary">Primary</wpd-button>
8122 <wpd-button variant="secondary">Secondary</wpd-button>
8123 <wpd-button variant="ghost">Ghost</wpd-button>
8124 <wpd-button variant="danger">Danger</wpd-button>
8125 <wpd-button variant="link">Link</wpd-button>
8126 </wpd-cluster>
8127 `
8128 };
8129 let WpdButton = _WpdButton;
8130 defineComponent("wpd-button", WpdButton);
8131 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}`;
8132 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}`;
8133 const _WpdSegment = class _WpdSegment extends Component {
8134 render() {
8135 this.setAttribute("role", "radio");
8136 return html`
8137 <button type="button" @click=${() => this._onPick()}>
8138 <slot></slot>
8139 </button>
8140 `;
8141 }
8142 _onPick() {
8143 this.emit("wpd-segment-pick", {
8144 value: this.value
8145 });
8146 }
8147 };
8148 _WpdSegment.props = ["value"];
8149 _WpdSegment.styles = [segmentStyles];
8150 _WpdSegment.help = {
8151 title: "Segment",
8152 summary: "Single pill inside a <wpd-segmented> group. Value identifies it for selection; aria-checked is mirrored by the parent.",
8153 status: "stable",
8154 since: "0.9.0",
8155 props: [
8156 {
8157 name: "value",
8158 type: "string",
8159 description: "Identifier this segment contributes to the parent group selection."
8160 }
8161 ],
8162 slots: [
8163 { name: "(default)", description: "Visible segment label." }
8164 ],
8165 events: [
8166 {
8167 name: "wpd-segment-pick",
8168 description: "Internal event bubbled to the parent <wpd-segmented>. Consumers should listen for wpd-pick on the group instead.",
8169 detail: "{ value: string }"
8170 }
8171 ]
8172 };
8173 let WpdSegment = _WpdSegment;
8174 defineComponent("wpd-segment", WpdSegment);
8175 const _WpdSegmented = class _WpdSegmented extends Component {
8176 connectedCallback() {
8177 super.connectedCallback();
8178 this.addEventListener("wpd-segment-pick", (e) => {
8179 const detail = e.detail;
8180 e.stopPropagation();
8181 this.value = detail.value;
8182 this.emit("wpd-pick", { value: detail.value });
8183 });
8184 }
8185 /**
8186 * Declarative item-list setter. Replaces the existing
8187 * `<wpd-segment>` children with a fresh set built from a
8188 * `{ value, label }` array; preserves the current selection
8189 * when the value still matches an entry, otherwise falls back
8190 * to the first item.
8191 *
8192 * Collapses the pre-0.11 imperative dance (clear children,
8193 * `createElement`, set `textContent`, `appendChild`, then
8194 * `setAttribute('value', …)` on the group — order matters) to
8195 * a single assignment:
8196 *
8197 * ```js
8198 * segmented.items = [
8199 * { value: 'm', label: 'm' },
8200 * { value: 'km', label: 'km' },
8201 * ];
8202 * ```
8203 *
8204 * @since 0.11.0
8205 */
8206 set items(list) {
8207 const existing = this.querySelectorAll(":scope > wpd-segment");
8208 for (const el of Array.from(existing)) {
8209 el.remove();
8210 }
8211 for (const item of list) {
8212 const seg = document.createElement("wpd-segment");
8213 seg.setAttribute("value", item.value);
8214 seg.textContent = item.label;
8215 this.appendChild(seg);
8216 }
8217 const current = this.value;
8218 const stillValid = current !== null && list.some((i) => i.value === current);
8219 if (!stillValid && list.length > 0) {
8220 this.value = list[0].value;
8221 } else {
8222 this.requestUpdate();
8223 }
8224 }
8225 render() {
8226 const label = this.label || "";
8227 if (label) {
8228 this.setAttribute("aria-label", label);
8229 }
8230 this.setAttribute("role", "radiogroup");
8231 const current = this.value;
8232 queueMicrotask(() => {
8233 const segs = this.querySelectorAll("wpd-segment");
8234 for (const seg of Array.from(segs)) {
8235 const v = seg.getAttribute("value");
8236 seg.setAttribute(
8237 "aria-checked",
8238 v === current ? "true" : "false"
8239 );
8240 }
8241 });
8242 return html`<slot></slot>`;
8243 }
8244 };
8245 _WpdSegmented.props = ["value", "label"];
8246 _WpdSegmented.styles = [segmentedStyles];
8247 _WpdSegmented.help = {
8248 title: "Segmented",
8249 summary: "iOS-style segmented radio group. Pill-shaped bar of equal-width <wpd-segment> children where exactly one is active.",
8250 status: "stable",
8251 since: "0.9.0",
8252 props: [
8253 {
8254 name: "value",
8255 type: "string",
8256 description: "Currently selected segment value. Mirrored onto child aria-checked."
8257 },
8258 {
8259 name: "label",
8260 type: "string",
8261 description: "aria-label for the radiogroup."
8262 }
8263 ],
8264 slots: [
8265 { name: "(default)", description: '<wpd-segment value="…"> children.' }
8266 ],
8267 events: [
8268 {
8269 name: "wpd-pick",
8270 description: "Fires when the selected segment changes.",
8271 detail: "{ value: string }"
8272 }
8273 ],
8274 cssProps: [
8275 { name: "--desktop-mode-window-bg", description: "Pill background." },
8276 { name: "--desktop-mode-text", description: "Active label colour." },
8277 { name: "--desktop-mode-muted", description: "Inactive label colour." }
8278 ],
8279 example: html`
8280 <wpd-segmented value="md" label="Dock size">
8281 <wpd-segment value="sm">Small</wpd-segment>
8282 <wpd-segment value="md">Medium</wpd-segment>
8283 <wpd-segment value="lg">Large</wpd-segment>
8284 </wpd-segmented>
8285 `
8286 };
8287 let WpdSegmented = _WpdSegmented;
8288 defineComponent("wpd-segmented", WpdSegmented);
8289 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}`;
8290 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 )}`;
8291 const _WpdMenu = class _WpdMenu extends Component {
8292 connectedCallback() {
8293 super.connectedCallback();
8294 this.setAttribute("role", "menu");
8295 }
8296 render() {
8297 return html`<slot></slot>`;
8298 }
8299 };
8300 _WpdMenu.styles = [menuStyles];
8301 _WpdMenu.help = {
8302 title: "Menu",
8303 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.",
8304 status: "stable",
8305 since: "0.9.0",
8306 slots: [
8307 { name: "(default)", description: "<wpd-menu-item> children." }
8308 ],
8309 cssProps: [
8310 { name: "--desktop-mode-window-bg", description: "Menu background." },
8311 { name: "--desktop-mode-window-border", description: "Menu border." },
8312 { name: "--desktop-mode-text", description: "Item text colour." }
8313 ],
8314 example: html`
8315 <wpd-menu>
8316 <wpd-menu-item value="new" icon="dashicons-plus">Open another window</wpd-menu-item>
8317 <wpd-menu-item value="startup" role="menuitemcheckbox" checked>Open on startup</wpd-menu-item>
8318 <wpd-menu-item value="close">Close window</wpd-menu-item>
8319 </wpd-menu>
8320 `
8321 };
8322 let WpdMenu = _WpdMenu;
8323 defineComponent("wpd-menu", WpdMenu);
8324 const _WpdMenuItem = class _WpdMenuItem extends Component {
8325 connectedCallback() {
8326 super.connectedCallback();
8327 if (!this.hasAttribute("role")) {
8328 this.setAttribute("role", "menuitem");
8329 }
8330 }
8331 render() {
8332 const icon = this.icon || "";
8333 const isCheckbox = this.getAttribute("role") === "menuitemcheckbox";
8334 const checked = this.checked !== null;
8335 if (isCheckbox) {
8336 this.setAttribute("aria-checked", checked ? "true" : "false");
8337 }
8338 return html`
8339 <button type="button" @click=${(e) => this._onPick(e)}>
8340 <span
8341 class="wpd-menu-item__check"
8342 ?hidden=${!isCheckbox}
8343 ></span>
8344 <span
8345 class="wpd-menu-item__icon dashicons ${icon}"
8346 aria-hidden="true"
8347 ?hidden=${isCheckbox || !icon}
8348 ></span>
8349 <span class="wpd-menu-item__label">
8350 <slot></slot>
8351 </span>
8352 </button>
8353 `;
8354 }
8355 _onPick(e) {
8356 e.preventDefault();
8357 this.emit("wpd-menu-item-click", {
8358 value: this.value
8359 });
8360 }
8361 };
8362 _WpdMenuItem.props = ["icon", "value", "checked"];
8363 _WpdMenuItem.styles = [menuItemStyles];
8364 _WpdMenuItem.help = {
8365 title: "Menu item",
8366 summary: 'Single row inside a <wpd-menu>. Supports three looks: plain label, left-aligned dashicon (icon="dashicons-…"), or a checkbox indicator (role="menuitemcheckbox" + checked).',
8367 status: "stable",
8368 since: "0.9.0",
8369 props: [
8370 {
8371 name: "icon",
8372 type: "string (dashicons class)",
8373 description: 'Dashicons class rendered on the left. Ignored when role="menuitemcheckbox".'
8374 },
8375 {
8376 name: "value",
8377 type: "string",
8378 description: "Identifier emitted in wpd-menu-item-click.detail.value."
8379 },
8380 {
8381 name: "checked",
8382 type: "boolean attribute",
8383 description: 'Visible check indicator. Only honoured when role="menuitemcheckbox".'
8384 }
8385 ],
8386 slots: [
8387 { name: "(default)", description: "Menu item label." }
8388 ],
8389 events: [
8390 {
8391 name: "wpd-menu-item-click",
8392 description: "Fires when the item is clicked; bubbles so the <wpd-menu> parent can delegate.",
8393 detail: "{ value: string | null }"
8394 }
8395 ]
8396 };
8397 let WpdMenuItem = _WpdMenuItem;
8398 defineComponent("wpd-menu-item", WpdMenuItem);
8399 function wpdConfirmGlobal$1(options) {
8400 const fn = window.wp?.desktop?.confirm;
8401 if (typeof fn !== "function") {
8402 return Promise.reject(
8403 new Error(
8404 "[desktop-mode] wp.desktop.confirm is missing — the main desktop bundle must load before the posts-window script."
8405 )
8406 );
8407 }
8408 return fn(options);
8409 }
8410 const _introShown = /* @__PURE__ */ Object.create(null);
8411 document.addEventListener("desktop-mode-intros-reset", () => {
8412 for (const slug of Object.keys(_introShown)) {
8413 _introShown[slug] = false;
8414 }
8415 });
8416 function maybeShowIntro(client) {
8417 let cfg;
8418 try {
8419 cfg = client.getConfig();
8420 } catch {
8421 return;
8422 }
8423 const slug = cfg.introSlug || cfg.mode || "posts";
8424 if (_introShown[slug]) {
8425 return;
8426 }
8427 if (cfg.introSeen) {
8428 return;
8429 }
8430 _introShown[slug] = true;
8431 const dialogPromise = slug === "pages" ? Promise.resolve().then(() => pagesIntroDialog).then(
8432 (m) => m.showPagesIntroDialog()
8433 ) : showPostsIntroDialog();
8434 void dialogPromise.then((result) => {
8435 if (result === "cancel") {
8436 _introShown[slug] = false;
8437 return;
8438 }
8439 void markIntroSeen(cfg, slug, client);
8440 if (result === "settings") {
8441 openOsSettingsFeatures();
8442 }
8443 }).catch(() => {
8444 _introShown[slug] = false;
8445 });
8446 }
8447 async function markIntroSeen(cfg, slug, client) {
8448 if (!cfg.introUrl) {
8449 return;
8450 }
8451 try {
8452 await trackedFetch(
8453 cfg.introUrl,
8454 {
8455 method: "POST",
8456 credentials: "same-origin",
8457 headers: {
8458 "Content-Type": "application/json",
8459 "X-WP-Nonce": cfg.restNonce
8460 },
8461 body: JSON.stringify({ slug })
8462 },
8463 {
8464 windowId: client.windowId,
8465 source: `${slug}-window/intro`
8466 }
8467 );
8468 cfg.introSeen = true;
8469 } catch {
8470 }
8471 }
8472 function openOsSettingsFeatures() {
8473 const api = window.wp?.desktop;
8474 api?.openOsSettings?.();
8475 }
8476 const ROOT$1 = "[data-desktop-mode-posts-root]";
8477 const STATUS$1 = "[data-desktop-mode-posts-status]";
8478 const SEARCH$1 = "[data-desktop-mode-posts-search]";
8479 const REFRESH$1 = "[data-desktop-mode-posts-refresh]";
8480 const NEW_BTN$1 = "[data-desktop-mode-posts-new]";
8481 const TABLE$1 = "[data-desktop-mode-posts-table]";
8482 const BULK$1 = "[data-desktop-mode-posts-bulk]";
8483 const COUNT$1 = "[data-desktop-mode-posts-count]";
8484 const PAGE_INDICATOR$1 = "[data-desktop-mode-posts-page-indicator]";
8485 const PREV$1 = "[data-desktop-mode-posts-prev]";
8486 const NEXT$1 = "[data-desktop-mode-posts-next]";
8487 const PER_PAGE$1 = "[data-desktop-mode-posts-per-page]";
8488 const TOOLBAR_TRAILING_EXTRAS = "[data-desktop-mode-posts-toolbar-extras]";
8489 const BULK_ACTIONS_HOST$1 = "[data-desktop-mode-posts-bulk-actions]";
8490 const HOOK_FILTER_COLUMNS = "desktop_mode.postsWindow.columns";
8491 const HOOK_FILTER_STATUS_SEGMENTS = "desktop_mode.postsWindow.statusSegments";
8492 const HOOK_FILTER_BULK_ACTIONS = "desktop_mode.postsWindow.bulkActions";
8493 const HOOK_FILTER_TOOLBAR_TRAILING = "desktop_mode.postsWindow.toolbarTrailing";
8494 const HOOK_ACTION_OPENED = "desktop_mode.postsWindow.opened";
8495 const HOOK_ACTION_DATA_LOADED = "desktop_mode.postsWindow.dataLoaded";
8496 const SEARCH_DEBOUNCE_MS$1 = 250;
8497 const STATUS_LABELS = {
8498 publish: __("Published"),
8499 future: __("Scheduled"),
8500 draft: __("Draft"),
8501 pending: __("Pending"),
8502 private: __("Private"),
8503 trash: __("Trash")
8504 };
8505 function statusBadgeColor(status) {
8506 switch (status) {
8507 case "publish":
8508 return { bg: "#e6f4ea", fg: "#1d6f42" };
8509 case "draft":
8510 return { bg: "#fdecea", fg: "#a02622" };
8511 case "pending":
8512 return { bg: "#fef7e0", fg: "#8a6d00" };
8513 case "private":
8514 return { bg: "#e8f0fe", fg: "#1a52a8" };
8515 case "future":
8516 return { bg: "#ede7f6", fg: "#5b3aa0" };
8517 case "trash":
8518 return { bg: "#f1f1f2", fg: "#50575e" };
8519 default:
8520 return { bg: "#f1f1f2", fg: "#50575e" };
8521 }
8522 }
8523 function decodeTitle(raw) {
8524 const ta = document.createElement("textarea");
8525 ta.innerHTML = raw;
8526 return ta.value;
8527 }
8528 function authorOf(row) {
8529 const embedded = row._embedded?.author?.[0];
8530 if (embedded) {
8531 const avatars = embedded.avatar_urls ?? {};
8532 return {
8533 id: embedded.id,
8534 name: embedded.name,
8535 avatar: avatars["48"] ?? avatars["96"] ?? avatars["24"]
8536 };
8537 }
8538 return { id: row.author, name: __("Unknown") };
8539 }
8540 function termRecordsOf(row, taxonomy) {
8541 const groups = row._embedded?.["wp:term"] ?? [];
8542 for (const group of groups) {
8543 if (group.length === 0) {
8544 continue;
8545 }
8546 if (group[0].taxonomy === taxonomy) {
8547 return group.map((t) => ({ id: t.id, name: t.name }));
8548 }
8549 }
8550 return [];
8551 }
8552 function featuredMediaOf(row) {
8553 const media = row._embedded?.["wp:featuredmedia"]?.[0];
8554 if (!media) {
8555 return null;
8556 }
8557 const sizes = media.media_details?.sizes ?? {};
8558 const small = sizes.thumbnail?.source_url ?? sizes.medium?.source_url ?? media.source_url;
8559 return { url: small, alt: media.alt_text ?? "" };
8560 }
8561 function cacheKey(rowId, columnKey) {
8562 return `${rowId}|${columnKey}`;
8563 }
8564 function memoCell(cache, rowId, columnKey, build) {
8565 const key = cacheKey(rowId, columnKey);
8566 const cached = cache.get(key);
8567 if (cached) {
8568 return cached;
8569 }
8570 const built = build();
8571 cache.set(key, built);
8572 return built;
8573 }
8574 const REQUIRED_COLUMN_KEYS = /* @__PURE__ */ new Set(["title"]);
8575 function getHiddenColumns() {
8576 try {
8577 const api = window.wp?.desktop;
8578 if (api && typeof api.getOsSettings === "function") {
8579 const snap = api.getOsSettings();
8580 if (Array.isArray(snap.nativePostsHiddenColumns)) {
8581 return new Set(snap.nativePostsHiddenColumns);
8582 }
8583 }
8584 } catch {
8585 }
8586 return /* @__PURE__ */ new Set();
8587 }
8588 const EMPTY_FILTER_DATA = { authors: [], tags: [] };
8589 function buildAllColumns(cache, client, filterData = EMPTY_FILTER_DATA) {
8590 const cols = _buildBaseColumns(cache, filterData, client);
8591 const hooks = window.wp?.hooks;
8592 return hooks && typeof hooks.applyFilters === "function" ? hooks.applyFilters(
8593 HOOK_FILTER_COLUMNS,
8594 cols
8595 ) : cols;
8596 }
8597 function buildColumns$1(cache, client, filterData = EMPTY_FILTER_DATA) {
8598 const all = buildAllColumns(cache, client, filterData);
8599 const hidden = getHiddenColumns();
8600 if (hidden.size === 0) {
8601 return all;
8602 }
8603 return all.filter(
8604 (col) => REQUIRED_COLUMN_KEYS.has(col.key) || !hidden.has(col.key)
8605 );
8606 }
8607 function _buildBaseColumns(cache, filterData, client) {
8608 let mode = "posts";
8609 try {
8610 const cfg = client.getConfig();
8611 if (cfg.mode === "pages") {
8612 mode = "pages";
8613 }
8614 } catch {
8615 }
8616 const titleCol = {
8617 key: "title",
8618 label: __("Title"),
8619 sortable: true,
8620 sticky: true,
8621 render: (_v, row) => memoCell(cache, row.id, "title", () => buildTitleCell(row, client))
8622 };
8623 const authorCol = {
8624 key: "author",
8625 label: __("Author"),
8626 sortable: true,
8627 width: "180px",
8628 filterRender: (host, ctx) => renderMultiSelectFilter(host, ctx, filterData.authors, {
8629 label: __("All authors"),
8630 ariaLabel: __("Filter by author")
8631 }),
8632 render: (_v, row) => memoCell(cache, row.id, "author", () => buildAuthorCell(row))
8633 };
8634 const dateCol = {
8635 key: "date",
8636 label: __("Date"),
8637 sortable: true,
8638 width: "170px",
8639 sortValue: (row) => Date.parse(row.date_gmt + "Z") || 0,
8640 render: (_v, row) => memoCell(cache, row.id, "date", () => buildDateCell(row))
8641 };
8642 if (mode === "pages") {
8643 const parentCol = {
8644 key: "parent",
8645 label: __("Parent"),
8646 width: "200px",
8647 render: (_v, row) => memoCell(cache, row.id, "parent", () => buildParentCell(row))
8648 };
8649 const templateCol = {
8650 key: "template",
8651 label: __("Template"),
8652 width: "180px",
8653 render: (_v, row) => memoCell(cache, row.id, "template", () => buildTemplateCell(row, client))
8654 };
8655 const slugCol = {
8656 key: "slug",
8657 label: __("Slug"),
8658 width: "200px",
8659 render: (_v, row) => memoCell(cache, row.id, "slug", () => buildSlugCell(row))
8660 };
8661 const commentsCol = {
8662 key: "comments",
8663 label: __("Comments"),
8664 width: "110px",
8665 sortValue: (row) => typeof row.desktop_mode_comment_count === "number" ? row.desktop_mode_comment_count : 0,
8666 render: (_v, row) => memoCell(
8667 cache,
8668 row.id,
8669 "comments",
8670 () => buildCommentsCell(row)
8671 )
8672 };
8673 return [
8674 titleCol,
8675 authorCol,
8676 parentCol,
8677 templateCol,
8678 slugCol,
8679 commentsCol,
8680 dateCol
8681 ];
8682 }
8683 return [
8684 titleCol,
8685 authorCol,
8686 {
8687 key: "categories",
8688 label: __("Categories"),
8689 width: "260px",
8690 render: (_v, row) => memoCell(
8691 cache,
8692 row.id,
8693 "categories",
8694 () => buildCategoriesCell(row, client)
8695 )
8696 },
8697 {
8698 key: "tags",
8699 // Drop the fixed width so the column flexes with the
8700 // available space; pin a minimum that comfortably holds
8701 // ~4 chips on one line so the cell doesn't collapse the
8702 // tags into a vertical stack on narrow tables.
8703 label: __("Tags"),
8704 minWidth: "360px",
8705 filterRender: (host, ctx) => renderMultiSelectFilter(
8706 host,
8707 ctx,
8708 filterData.tags.map((t) => ({ id: t.id, name: t.name })),
8709 {
8710 label: __("All tags"),
8711 ariaLabel: __("Filter by tag"),
8712 dataKey: "tags",
8713 hasMore: !!filterData.tagsHasMore,
8714 onLoadMore: filterData.loadMoreTags
8715 }
8716 ),
8717 render: (_v, row) => memoCell(cache, row.id, "tags", () => buildTagsCell(row, client))
8718 },
8719 dateCol
8720 ];
8721 }
8722 const _parentTitleByPageRoster = /* @__PURE__ */ new Map();
8723 function buildParentCell(row) {
8724 const cell = document.createElement("span");
8725 cell.className = "desktop-mode-posts__parent";
8726 const pid = typeof row.parent === "number" ? row.parent : 0;
8727 if (pid === 0) {
8728 cell.classList.add("desktop-mode-posts__parent--top");
8729 cell.textContent = "—";
8730 cell.setAttribute("aria-label", __("Top-level page"));
8731 return cell;
8732 }
8733 cell.classList.add("desktop-mode-posts__parent--child");
8734 const titleFromRoster = _parentTitleByPageRoster.get(pid);
8735 if (titleFromRoster) {
8736 cell.textContent = `↳ ${titleFromRoster}`;
8737 } else {
8738 cell.textContent = sprintf(__("↳ #%d"), pid);
8739 }
8740 return cell;
8741 }
8742 function refreshParentTitleRoster(rows) {
8743 _parentTitleByPageRoster.clear();
8744 for (const row of rows) {
8745 _parentTitleByPageRoster.set(row.id, decodeTitle(row.title.rendered));
8746 }
8747 }
8748 function buildTemplateCell(row, client) {
8749 const cell = document.createElement("span");
8750 cell.className = "desktop-mode-posts__template";
8751 const slug = typeof row.template === "string" ? row.template : "";
8752 let label = slug;
8753 try {
8754 const cfg = client.getConfig();
8755 const map = cfg.pageTemplates ?? {};
8756 label = map[slug] ?? (slug === "" ? __("Default template") : slug);
8757 } catch {
8758 label = slug === "" ? __("Default template") : slug;
8759 }
8760 cell.textContent = label;
8761 if (slug !== "") {
8762 cell.title = slug;
8763 }
8764 return cell;
8765 }
8766 function buildSlugCell(row) {
8767 const cell = document.createElement("button");
8768 cell.type = "button";
8769 cell.className = "desktop-mode-posts__slug";
8770 const slug = typeof row.slug === "string" ? row.slug : "";
8771 cell.textContent = slug || "—";
8772 cell.disabled = slug === "";
8773 cell.title = slug ? __("Click to copy slug") : "";
8774 Object.assign(cell.style, {
8775 appearance: "none",
8776 background: "transparent",
8777 border: "none",
8778 padding: "2px 6px",
8779 font: "inherit",
8780 color: "inherit",
8781 cursor: slug ? "copy" : "default",
8782 textAlign: "left",
8783 fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace',
8784 fontSize: "12px",
8785 borderRadius: "4px",
8786 maxWidth: "100%",
8787 overflow: "hidden",
8788 textOverflow: "ellipsis",
8789 whiteSpace: "nowrap"
8790 });
8791 cell.addEventListener("click", (e) => {
8792 e.stopPropagation();
8793 if (!slug) {
8794 return;
8795 }
8796 void navigator.clipboard?.writeText(slug).then(() => {
8797 cell.textContent = __("Copied!");
8798 cell.style.color = "var(--wp-admin-theme-color, #2271b1)";
8799 setTimeout(() => {
8800 cell.textContent = slug;
8801 cell.style.color = "";
8802 }, 1200);
8803 }).catch(() => {
8804 });
8805 });
8806 return cell;
8807 }
8808 function buildCommentsCell(row) {
8809 const cell = document.createElement("span");
8810 cell.className = "desktop-mode-posts__comments";
8811 Object.assign(cell.style, {
8812 display: "inline-flex",
8813 alignItems: "center",
8814 gap: "6px",
8815 fontVariantNumeric: "tabular-nums"
8816 });
8817 const count = typeof row.desktop_mode_comment_count === "number" ? row.desktop_mode_comment_count : null;
8818 if (count === null) {
8819 cell.textContent = "—";
8820 cell.style.color = "var(--wp-admin-theme-fg-muted, #8c8f94)";
8821 return cell;
8822 }
8823 const icon = document.createElement("span");
8824 icon.className = "dashicons dashicons-admin-comments";
8825 icon.setAttribute("aria-hidden", "true");
8826 Object.assign(icon.style, {
8827 fontSize: "16px",
8828 width: "16px",
8829 height: "16px",
8830 color: count > 0 ? "var(--wp-admin-theme-color, #2271b1)" : "var(--wp-admin-theme-fg-muted, #8c8f94)"
8831 });
8832 const label = document.createElement("span");
8833 label.textContent = String(count);
8834 if (count === 0) {
8835 label.style.color = "var(--wp-admin-theme-fg-muted, #8c8f94)";
8836 }
8837 cell.appendChild(icon);
8838 cell.appendChild(label);
8839 cell.setAttribute(
8840 "aria-label",
8841 // translators: %d is the comment count for a row.
8842 `${sprintf(__("%d comments"), count)}`
8843 );
8844 return cell;
8845 }
8846 function renderMultiSelectFilter(host, ctx, all, opts) {
8847 const HOST_KEY = "wpdPostsFilterMounted";
8848 const tagged = host;
8849 const optionsForPicker = all.map((o) => ({
8850 value: String(o.id),
8851 label: o.name
8852 }));
8853 const nextSig = optionsForPicker.map((o) => `${o.value}:${o.label}`).join("|");
8854 if (tagged[HOST_KEY]) {
8855 const state = tagged[HOST_KEY];
8856 if (state.listSig !== nextSig) {
8857 state.picker.items = optionsForPicker;
8858 state.listSig = nextSig;
8859 }
8860 if (state.picker.getAttribute("value") !== ctx.value) {
8861 state.picker.setAttribute("value", ctx.value);
8862 }
8863 state.picker.hasMore = !!opts.hasMore;
8864 return;
8865 }
8866 const picker = document.createElement("wpd-multiselect");
8867 picker.setAttribute("placeholder", opts.label);
8868 picker.setAttribute("aria-label", opts.ariaLabel);
8869 picker.setAttribute("data-noclick", "");
8870 picker.setAttribute("value", ctx.value);
8871 if (opts.dataKey) {
8872 picker.setAttribute("data-key", opts.dataKey);
8873 }
8874 host.appendChild(picker);
8875 picker.items = optionsForPicker;
8876 picker.hasMore = !!opts.hasMore;
8877 picker.addEventListener("wpd-pick", (e) => {
8878 const detail = e.detail;
8879 const next = detail?.value ?? "";
8880 ctx.value = next;
8881 ctx.setValue(next);
8882 });
8883 if (opts.onLoadMore) {
8884 const onLoadMore = opts.onLoadMore;
8885 picker.addEventListener("wpd-multiselect-load-more", () => {
8886 picker.loadingMore = true;
8887 onLoadMore();
8888 });
8889 }
8890 tagged[HOST_KEY] = { picker, listSig: nextSig };
8891 }
8892 function mountKebabColumnToggles(body, cache, repaintColumns, client) {
8893 const winEl = body.closest(".desktop-mode-window");
8894 const panel = winEl?.querySelector(
8895 ".desktop-mode-window__menu-panel"
8896 );
8897 if (!panel) {
8898 return null;
8899 }
8900 const SECTION_CLASS = "desktop-mode-posts-window__menu-columns";
8901 const ITEM_CLASS = "desktop-mode-posts-window__menu-column-item";
8902 const VALUE_PREFIX = "desktop-mode-posts-column:";
8903 panel.querySelectorAll(`.${SECTION_CLASS}, .${ITEM_CLASS}`).forEach((n) => n.remove());
8904 const allCols = buildAllColumns(cache, client);
8905 const togglable = allCols.filter(
8906 (c) => !REQUIRED_COLUMN_KEYS.has(c.key)
8907 );
8908 if (togglable.length === 0) {
8909 return null;
8910 }
8911 const sectionLabel = document.createElement("div");
8912 sectionLabel.className = SECTION_CLASS;
8913 sectionLabel.setAttribute("role", "presentation");
8914 sectionLabel.textContent = __("Show columns");
8915 panel.appendChild(sectionLabel);
8916 const itemEls = /* @__PURE__ */ new Map();
8917 for (const col of togglable) {
8918 const item = document.createElement("wpd-menu-item");
8919 item.setAttribute("role", "menuitemcheckbox");
8920 item.setAttribute("value", VALUE_PREFIX + col.key);
8921 item.classList.add("desktop-mode-window__menu-item");
8922 item.classList.add(ITEM_CLASS);
8923 item.textContent = col.label || col.key;
8924 panel.appendChild(item);
8925 itemEls.set(col.key, item);
8926 }
8927 const paintChecked = () => {
8928 const hidden = getHiddenColumns();
8929 for (const [key, el] of itemEls) {
8930 if (hidden.has(key)) {
8931 el.removeAttribute("checked");
8932 } else {
8933 el.setAttribute("checked", "");
8934 }
8935 }
8936 };
8937 paintChecked();
8938 const onClick = (e) => {
8939 const detail = e.detail;
8940 const value = detail?.value;
8941 if (typeof value !== "string" || !value.startsWith(VALUE_PREFIX)) {
8942 return;
8943 }
8944 const key = value.slice(VALUE_PREFIX.length);
8945 if (!itemEls.has(key) || REQUIRED_COLUMN_KEYS.has(key)) {
8946 return;
8947 }
8948 const hidden = getHiddenColumns();
8949 if (hidden.has(key)) {
8950 hidden.delete(key);
8951 } else {
8952 hidden.add(key);
8953 }
8954 const next = Array.from(hidden).sort();
8955 const api = window.wp?.desktop;
8956 if (api && typeof api.updateOsSettings === "function") {
8957 api.updateOsSettings(
8958 { nativePostsHiddenColumns: next },
8959 { windowId: "desktop-mode-posts" }
8960 );
8961 }
8962 paintChecked();
8963 repaintColumns();
8964 };
8965 panel.addEventListener("wpd-menu-item-click", onClick);
8966 return {
8967 refresh: paintChecked,
8968 dispose: () => {
8969 panel.removeEventListener("wpd-menu-item-click", onClick);
8970 sectionLabel.remove();
8971 for (const el of itemEls.values()) {
8972 el.remove();
8973 }
8974 itemEls.clear();
8975 }
8976 };
8977 }
8978 function defaultStatusSegments$1() {
8979 return [
8980 { value: "", label: __("All") },
8981 { value: "publish", label: __("Published") },
8982 { value: "draft", label: __("Drafts") },
8983 { value: "pending", label: __("Pending") },
8984 { value: "future", label: __("Scheduled") },
8985 { value: "trash", label: __("Trash") }
8986 ];
8987 }
8988 function defaultBulkActions(client) {
8989 return [
8990 {
8991 id: "trash",
8992 label: __("Move to trash"),
8993 icon: "dashicons-trash",
8994 variant: "danger",
8995 /* translators: %d: row count. */
8996 confirm: __("Move %d post(s) to the trash?"),
8997 run: async (ids, ctx) => {
8998 const data = ctx.table.data ?? [];
8999 const trashable = ids.filter((id) => {
9000 const row = data.find((r) => r.id === id);
9001 return row && row.status !== "trash";
9002 });
9003 if (trashable.length === 0) {
9004 return;
9005 }
9006 const results = await Promise.all(
9007 trashable.map((id) => client.trashPost(id))
9008 );
9009 const errors = results.filter((r) => !r.ok);
9010 if (errors.length > 0) {
9011 console.error("[posts-window] some trashes failed", errors);
9012 }
9013 const okIds = results.filter((r) => r.ok).map((r) => r.id);
9014 const api = window.wp?.desktop;
9015 if (api && typeof api.broadcast === "function") {
9016 api.broadcast("desktop-mode.post.changed", {
9017 source: "posts-window",
9018 action: "trashed",
9019 ids: okIds
9020 });
9021 }
9022 }
9023 }
9024 ];
9025 }
9026 function resolveBulkActions(client) {
9027 const hooks = window.wp?.hooks;
9028 const defaults = defaultBulkActions(client);
9029 if (!hooks || typeof hooks.applyFilters !== "function") {
9030 return defaults;
9031 }
9032 try {
9033 const out = hooks.applyFilters(HOOK_FILTER_BULK_ACTIONS, defaults);
9034 return Array.isArray(out) ? out : defaults;
9035 } catch (err) {
9036 console.error(
9037 "[posts-window] bulk-actions filter threw; falling back to defaults:",
9038 err
9039 );
9040 return defaults;
9041 }
9042 }
9043 function resolveStatusSegments() {
9044 const hooks = window.wp?.hooks;
9045 const defaults = defaultStatusSegments$1();
9046 if (!hooks || typeof hooks.applyFilters !== "function") {
9047 return defaults;
9048 }
9049 try {
9050 const out = hooks.applyFilters(HOOK_FILTER_STATUS_SEGMENTS, defaults);
9051 return Array.isArray(out) && out.length > 0 ? out : defaults;
9052 } catch (err) {
9053 console.error(
9054 "[posts-window] status-segments filter threw; falling back to defaults:",
9055 err
9056 );
9057 return defaults;
9058 }
9059 }
9060 function resolveToolbarTrailing(ctx) {
9061 const hooks = window.wp?.hooks;
9062 if (!hooks || typeof hooks.applyFilters !== "function") {
9063 return [];
9064 }
9065 try {
9066 const out = hooks.applyFilters(HOOK_FILTER_TOOLBAR_TRAILING, [], ctx);
9067 if (!Array.isArray(out)) {
9068 return [];
9069 }
9070 return out.filter((el) => el instanceof HTMLElement);
9071 } catch (err) {
9072 console.error(
9073 "[posts-window] toolbar-trailing filter threw; ignoring:",
9074 err
9075 );
9076 return [];
9077 }
9078 }
9079 function buildTitleCell(row, client) {
9080 const cell = document.createElement("span");
9081 cell.style.cssText = "display:flex;flex-direction:column;gap:4px;min-width:0;";
9082 const titleRow = document.createElement("span");
9083 titleRow.style.cssText = "display:flex;align-items:center;gap:8px;min-width:0;";
9084 const link = document.createElement("a");
9085 link.href = client.buildEditPostUrl(row.id);
9086 link.setAttribute("data-noclick", "");
9087 const title = decodeTitle(row.title.rendered) || __("(no title)");
9088 link.textContent = title;
9089 link.title = title;
9090 link.style.cssText = "font-weight:600;color:inherit;text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:340px;";
9091 link.addEventListener("mouseenter", () => {
9092 link.style.textDecoration = "underline";
9093 });
9094 link.addEventListener("mouseleave", () => {
9095 link.style.textDecoration = "none";
9096 });
9097 link.addEventListener("click", (e) => {
9098 e.preventDefault();
9099 e.stopPropagation();
9100 openAdminUrl(link.href, {
9101 title,
9102 icon: "dashicons-admin-post"
9103 });
9104 });
9105 titleRow.appendChild(link);
9106 const lock = row.desktop_mode_lock ?? null;
9107 if (lock) {
9108 const lockBadge = document.createElement("span");
9109 lockBadge.style.cssText = [
9110 "display:inline-flex",
9111 "align-items:center",
9112 "gap:4px",
9113 "padding:2px 8px",
9114 "border-radius:10px",
9115 "font-size:11px",
9116 "font-weight:600",
9117 "background:rgba(179, 45, 46, 0.1)",
9118 "color:#b32d2e",
9119 "white-space:nowrap",
9120 "flex-shrink:0"
9121 ].join(";");
9122 const lockIcon = document.createElement("span");
9123 lockIcon.setAttribute("aria-hidden", "true");
9124 lockIcon.style.cssText = [
9125 "font-family:dashicons",
9126 "font-size:14px",
9127 "line-height:1",
9128 "display:inline-block",
9129 "speak:none",
9130 "-webkit-font-smoothing:antialiased"
9131 ].join(";");
9132 lockIcon.textContent = "";
9133 lockBadge.appendChild(lockIcon);
9134 const lockText = document.createElement("span");
9135 lockText.textContent = lock.userName;
9136 lockBadge.appendChild(lockText);
9137 const tipFmt = __("%s is currently editing", "desktop-mode");
9138 lockBadge.title = sprintf(tipFmt, lock.userName);
9139 titleRow.appendChild(lockBadge);
9140 }
9141 let cfgForBadges = null;
9142 try {
9143 cfgForBadges = client.getConfig();
9144 } catch {
9145 cfgForBadges = null;
9146 }
9147 if (cfgForBadges && cfgForBadges.mode === "pages") {
9148 if (typeof cfgForBadges.frontPageId === "number" && cfgForBadges.frontPageId === row.id) {
9149 titleRow.appendChild(
9150 buildAssignmentBadge(
9151 __("Front page"),
9152 "dashicons-admin-home",
9153 "#0a4b78",
9154 "rgba(34,113,177,0.12)"
9155 )
9156 );
9157 }
9158 if (typeof cfgForBadges.postsPageId === "number" && cfgForBadges.postsPageId === row.id) {
9159 titleRow.appendChild(
9160 buildAssignmentBadge(
9161 __("Posts page"),
9162 "dashicons-admin-post",
9163 "#5b3aa0",
9164 "rgba(91,58,160,0.12)"
9165 )
9166 );
9167 }
9168 }
9169 if (row.status && row.status !== "publish") {
9170 const badge = document.createElement("span");
9171 const colors = statusBadgeColor(row.status);
9172 badge.textContent = STATUS_LABELS[row.status] ?? row.status;
9173 badge.style.cssText = [
9174 "display:inline-flex",
9175 "align-items:center",
9176 "padding:2px 8px",
9177 "border-radius:10px",
9178 "font-size:11px",
9179 "font-weight:600",
9180 "text-transform:uppercase",
9181 "letter-spacing:0.04em",
9182 `background:${colors.bg}`,
9183 `color:${colors.fg}`,
9184 "white-space:nowrap",
9185 "flex-shrink:0"
9186 ].join(";");
9187 titleRow.appendChild(badge);
9188 }
9189 if (cfgForBadges?.mode === "pages" && typeof row.link === "string" && row.link && row.status === "publish") {
9190 const view = document.createElement("a");
9191 view.href = row.link;
9192 view.target = "_blank";
9193 view.rel = "noreferrer noopener";
9194 view.textContent = __("View");
9195 view.title = row.link;
9196 view.setAttribute("data-noclick", "");
9197 view.style.cssText = [
9198 "font-size:11px",
9199 "color:var(--wp-admin-theme-color, #2271b1)",
9200 "text-decoration:none",
9201 "flex-shrink:0"
9202 ].join(";");
9203 view.addEventListener("click", (e) => e.stopPropagation());
9204 view.addEventListener("mouseenter", () => {
9205 view.style.textDecoration = "underline";
9206 });
9207 view.addEventListener("mouseleave", () => {
9208 view.style.textDecoration = "none";
9209 });
9210 titleRow.appendChild(view);
9211 }
9212 cell.appendChild(titleRow);
9213 return cell;
9214 }
9215 function buildAssignmentBadge(label, dashicon, fg, bg) {
9216 const badge = document.createElement("span");
9217 badge.style.cssText = [
9218 "display:inline-flex",
9219 "align-items:center",
9220 "gap:4px",
9221 "padding:2px 8px",
9222 "border-radius:10px",
9223 "font-size:11px",
9224 "font-weight:600",
9225 `background:${bg}`,
9226 `color:${fg}`,
9227 "white-space:nowrap",
9228 "flex-shrink:0"
9229 ].join(";");
9230 const icon = document.createElement("span");
9231 icon.className = `dashicons ${dashicon}`;
9232 icon.setAttribute("aria-hidden", "true");
9233 icon.style.cssText = "font-size:13px;width:13px;height:13px;line-height:1;";
9234 const text = document.createElement("span");
9235 text.textContent = label;
9236 badge.appendChild(icon);
9237 badge.appendChild(text);
9238 return badge;
9239 }
9240 function buildAuthorCell(row) {
9241 const a = authorOf(row);
9242 const wrap = document.createElement("span");
9243 wrap.style.cssText = "display:inline-flex;align-items:center;gap:8px;min-width:0;";
9244 const avatar = document.createElement("wpd-avatar");
9245 avatar.setAttribute("size", "24");
9246 if (a.name) {
9247 avatar.setAttribute("name", a.name);
9248 }
9249 if (a.id > 0) {
9250 avatar.setAttribute("user-id", String(a.id));
9251 }
9252 if (a.avatar) {
9253 applyAvatarSrc(avatar, a.avatar);
9254 }
9255 wrap.appendChild(avatar);
9256 const name = document.createElement("span");
9257 name.textContent = a.name;
9258 name.style.cssText = "overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
9259 wrap.appendChild(name);
9260 return wrap;
9261 }
9262 function buildTagsCell(row, client) {
9263 const wrap = document.createElement("span");
9264 wrap.style.cssText = "display:inline-flex;align-items:center;width:100%;min-width:0;";
9265 const picker = document.createElement("wpd-tag-input");
9266 picker.setAttribute("creatable", "");
9267 picker.setAttribute("removable", "");
9268 picker.setAttribute("min-query", "0");
9269 picker.setAttribute("placeholder", __("Add tag…"));
9270 picker.setAttribute("add-label", __("Tag"));
9271 picker.setAttribute("data-noclick", "");
9272 const seed = termRecordsOf(row, "post_tag").map((t) => ({
9273 id: t.id,
9274 label: t.name
9275 }));
9276 picker.value = seed;
9277 const cellState = {
9278 // Mirror of `picker.value` we mutate optimistically. Keeping
9279 // it here (rather than reading back from the picker) avoids
9280 // double-source-of-truth bugs when two events fire in the
9281 // same tick.
9282 tags: seed.slice(),
9283 // AbortController for the in-flight suggest fetch.
9284 suggestAbort: null,
9285 suggestDebounce: null,
9286 // Last query the user typed — used to drop stale responses
9287 // even after AbortController has fired.
9288 lastQuery: ""
9289 };
9290 const setValue = (next) => {
9291 cellState.tags = next.slice();
9292 picker.value = next;
9293 };
9294 picker.addEventListener("wpd-tag-suggest", (e) => {
9295 const detail = e.detail;
9296 const query = detail?.query ?? "";
9297 cellState.lastQuery = query;
9298 if (cellState.suggestDebounce !== null) {
9299 window.clearTimeout(cellState.suggestDebounce);
9300 cellState.suggestDebounce = null;
9301 }
9302 cellState.suggestDebounce = window.setTimeout(async () => {
9303 cellState.suggestDebounce = null;
9304 if (cellState.suggestAbort) {
9305 cellState.suggestAbort.abort();
9306 }
9307 const ac = new AbortController();
9308 cellState.suggestAbort = ac;
9309 try {
9310 const matches = await client.searchTags(query, ac.signal);
9311 if (cellState.lastQuery !== query) {
9312 return;
9313 }
9314 const existingIds = new Set(cellState.tags.map((t) => t.id));
9315 picker.suggestions = matches.filter((m) => !existingIds.has(m.id)).map((m) => ({ id: m.id, label: m.name }));
9316 } catch (err) {
9317 if (err?.name === "AbortError") {
9318 return;
9319 }
9320 picker.suggestions = [];
9321 console.warn(
9322 "[posts-window] tag search failed",
9323 err
9324 );
9325 } finally {
9326 picker.suggestionsLoading = false;
9327 }
9328 }, 200);
9329 });
9330 picker.addEventListener("wpd-tag-add", async (e) => {
9331 const detail = e.detail;
9332 if (!detail?.tag) {
9333 return;
9334 }
9335 const optimistic = {
9336 id: detail.tag.id,
9337 label: detail.tag.label,
9338 pending: true
9339 };
9340 const next = [...cellState.tags, optimistic];
9341 setValue(next);
9342 try {
9343 let resolvedTag = null;
9344 if (detail.isNew || typeof detail.tag.id !== "number") {
9345 resolvedTag = await client.createTag(detail.tag.label);
9346 } else {
9347 resolvedTag = {
9348 id: Number(detail.tag.id),
9349 name: detail.tag.label,
9350 slug: ""
9351 };
9352 }
9353 const desiredIds = [
9354 ...cellState.tags.filter((t) => !t.pending).map((t) => Number(t.id)),
9355 resolvedTag.id
9356 ];
9357 await client.updatePostTags(row.id, desiredIds);
9358 setValue(
9359 cellState.tags.map((t) => {
9360 if (t.label.toLowerCase() === detail.tag.label.toLowerCase()) {
9361 return {
9362 id: resolvedTag.id,
9363 label: resolvedTag.name
9364 };
9365 }
9366 return t;
9367 })
9368 );
9369 const api = window.wp?.desktop;
9370 if (api && typeof api.broadcast === "function") {
9371 api.broadcast("desktop-mode.post.changed", {
9372 source: "posts-window",
9373 action: "tagged",
9374 ids: [row.id]
9375 });
9376 }
9377 } catch (err) {
9378 setValue(
9379 cellState.tags.filter(
9380 (t) => t.label.toLowerCase() !== detail.tag.label.toLowerCase()
9381 )
9382 );
9383 showTagError(
9384 sprintf(
9385 /* translators: %s: tag label */
9386 __('Couldn’t add tag "%s".'),
9387 detail.tag.label
9388 ),
9389 err
9390 );
9391 }
9392 });
9393 picker.addEventListener("wpd-tag-remove", async (e) => {
9394 const detail = e.detail;
9395 if (!detail?.tag) {
9396 return;
9397 }
9398 const removed = detail.tag;
9399 const previous = cellState.tags.slice();
9400 setValue(
9401 cellState.tags.map(
9402 (t) => t.label === removed.label ? { ...t, pending: true } : t
9403 )
9404 );
9405 try {
9406 const desiredIds = previous.filter((t) => t.label !== removed.label).map((t) => Number(t.id)).filter((n) => Number.isFinite(n));
9407 await client.updatePostTags(row.id, desiredIds);
9408 setValue(
9409 previous.filter((t) => t.label !== removed.label)
9410 );
9411 const api = window.wp?.desktop;
9412 if (api && typeof api.broadcast === "function") {
9413 api.broadcast("desktop-mode.post.changed", {
9414 source: "posts-window",
9415 action: "untagged",
9416 ids: [row.id]
9417 });
9418 }
9419 } catch (err) {
9420 setValue(previous);
9421 showTagError(
9422 sprintf(
9423 /* translators: %s: tag label */
9424 __('Couldn’t remove tag "%s".'),
9425 removed.label
9426 ),
9427 err
9428 );
9429 }
9430 });
9431 wrap.appendChild(picker);
9432 return wrap;
9433 }
9434 function showTagError(title, err) {
9435 const reason = err instanceof Error ? err.message : String(err);
9436 const api = window.wp?.desktop;
9437 if (api && typeof api.showToast === "function") {
9438 api.showToast({
9439 message: `${title} ${reason}`.trim(),
9440 duration: 6e3
9441 });
9442 return;
9443 }
9444 console.error(title, err);
9445 }
9446 function buildCategoriesCell(row, client) {
9447 const wrap = document.createElement("span");
9448 wrap.className = "wpd-cat-cell-dropzone";
9449 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;";
9450 const picker = document.createElement(
9451 "wpd-category-picker"
9452 );
9453 picker.setAttribute("placeholder", __("Search categories…"));
9454 picker.setAttribute("add-label", __("Categorize"));
9455 picker.setAttribute("data-noclick", "");
9456 _activePickers.add(picker);
9457 picker.value = row.categories ?? [];
9458 const seedItems = termRecordsOf(row, "category").map(
9459 (t) => ({ id: t.id, name: t.name, parent: 0 })
9460 );
9461 picker.items = seedItems;
9462 const cellState = {
9463 categoryIds: (row.categories ?? []).slice()
9464 };
9465 const setValue = (next) => {
9466 cellState.categoryIds = next.slice();
9467 picker.value = next;
9468 };
9469 void getCategoriesTree(client).then((tree) => {
9470 if (!picker.isConnected) {
9471 return;
9472 }
9473 picker.items = tree;
9474 }).catch((err) => {
9475 console.warn("[posts-window] category tree fetch failed", err);
9476 });
9477 picker.addEventListener("wpd-categories-open", () => {
9478 void primePickerFromCache(picker);
9479 });
9480 picker.addEventListener(
9481 "wpd-categories-create",
9482 async (e) => {
9483 const detail = e.detail;
9484 const parent = detail?.parent ?? 0;
9485 if (!detail || !detail.name) {
9486 picker.failCreating(parent);
9487 return;
9488 }
9489 try {
9490 const created = await client.createCategory(detail.name, parent);
9491 _categoryTreePromise = null;
9492 const nextItems = [
9493 ...picker.items,
9494 {
9495 id: created.id,
9496 name: created.name,
9497 parent: created.parent
9498 }
9499 ];
9500 picker.items = nextItems;
9501 const nextValue = [...cellState.categoryIds, created.id];
9502 setValue(nextValue);
9503 picker.endCreating(parent);
9504 try {
9505 await client.updatePostCategories(row.id, nextValue);
9506 const api = window.wp?.desktop;
9507 if (api && typeof api.broadcast === "function") {
9508 api.broadcast("desktop-mode.post.changed", {
9509 source: "posts-window",
9510 action: "categorized",
9511 ids: [row.id]
9512 });
9513 }
9514 } catch (err) {
9515 setValue(cellState.categoryIds.filter((id) => id !== created.id));
9516 showTagError(__("Couldn’t assign new category."), err);
9517 }
9518 } catch (err) {
9519 picker.failCreating(
9520 parent,
9521 err instanceof Error ? err.message : String(err)
9522 );
9523 showTagError(__("Couldn’t create category."), err);
9524 }
9525 }
9526 );
9527 picker.addEventListener("wpd-categories-change", async (e) => {
9528 const detail = e.detail;
9529 if (!detail || !Array.isArray(detail.value)) {
9530 return;
9531 }
9532 const previous = cellState.categoryIds.slice();
9533 const next = detail.value.slice();
9534 setValue(next);
9535 try {
9536 await client.updatePostCategories(row.id, next);
9537 const api = window.wp?.desktop;
9538 if (api && typeof api.broadcast === "function") {
9539 api.broadcast("desktop-mode.post.changed", {
9540 source: "posts-window",
9541 action: "categorized",
9542 ids: [row.id]
9543 });
9544 }
9545 } catch (err) {
9546 setValue(previous);
9547 showTagError(__("Couldn’t update categories."), err);
9548 }
9549 });
9550 picker.addEventListener("wpd-categories-delete", async (e) => {
9551 const detail = e.detail;
9552 if (!detail || typeof detail.id !== "number") {
9553 return;
9554 }
9555 const ok = await wpdConfirmGlobal$1({
9556 title: __("Delete category?"),
9557 message: sprintf(
9558 /* translators: %s: category name. */
9559 __(
9560 'Delete the category "%s"? Posts assigned only to it will fall back to Uncategorized.'
9561 ),
9562 detail.name
9563 ),
9564 confirmLabel: __("Delete"),
9565 danger: true
9566 });
9567 if (!ok) {
9568 return;
9569 }
9570 try {
9571 await client.deleteTerm("categories", detail.id);
9572 if (cellState.categoryIds.includes(detail.id)) {
9573 const next = cellState.categoryIds.filter(
9574 (id) => id !== detail.id
9575 );
9576 setValue(next);
9577 try {
9578 await client.updatePostCategories(row.id, next);
9579 } catch (err) {
9580 showTagError(
9581 __("Couldn’t update post categories after delete."),
9582 err
9583 );
9584 }
9585 }
9586 } catch (err) {
9587 showTagError(__("Couldn’t delete category."), err);
9588 }
9589 });
9590 picker.addEventListener("wpd-chain-segment-dragstart", (e) => {
9591 const detail = e.detail;
9592 if (!detail || !detail.dragEvent || !detail.dragEvent.dataTransfer) {
9593 return;
9594 }
9595 const ids = [];
9596 for (const seg of detail.segments) {
9597 if (typeof seg.id === "number") {
9598 ids.push(seg.id);
9599 }
9600 }
9601 if (ids.length === 0) {
9602 return;
9603 }
9604 const dt = detail.dragEvent.dataTransfer;
9605 dt.setData(
9606 "application/x-desktop-mode-categories",
9607 JSON.stringify({
9608 ids,
9609 source: "posts-window",
9610 sourcePostId: row.id
9611 })
9612 );
9613 dt.setData("text/plain", ids.join(","));
9614 dt.effectAllowed = "copy";
9615 });
9616 let dropEnterCount = 0;
9617 const setDropTargetActive = (on) => {
9618 if (on) {
9619 wrap.style.backgroundColor = "color-mix(in srgb, var(--wp-admin-theme-color, #2271b1) 12%, transparent)";
9620 wrap.style.boxShadow = "inset 0 0 0 2px var(--wp-admin-theme-color, #2271b1)";
9621 } else {
9622 wrap.style.backgroundColor = "";
9623 wrap.style.boxShadow = "";
9624 }
9625 };
9626 const acceptsCategoriesDrag = (e) => {
9627 const types = e.dataTransfer?.types;
9628 if (!types) {
9629 return false;
9630 }
9631 return Array.from(types).includes(
9632 "application/x-desktop-mode-categories"
9633 );
9634 };
9635 wrap.addEventListener("dragenter", (e) => {
9636 if (!acceptsCategoriesDrag(e)) {
9637 return;
9638 }
9639 e.preventDefault();
9640 dropEnterCount++;
9641 setDropTargetActive(true);
9642 });
9643 wrap.addEventListener("dragover", (e) => {
9644 if (!acceptsCategoriesDrag(e)) {
9645 return;
9646 }
9647 e.preventDefault();
9648 if (e.dataTransfer) {
9649 e.dataTransfer.dropEffect = "copy";
9650 }
9651 });
9652 wrap.addEventListener("dragleave", () => {
9653 if (dropEnterCount > 0) {
9654 dropEnterCount--;
9655 }
9656 if (dropEnterCount === 0) {
9657 setDropTargetActive(false);
9658 }
9659 });
9660 wrap.addEventListener("drop", async (e) => {
9661 dropEnterCount = 0;
9662 setDropTargetActive(false);
9663 if (!acceptsCategoriesDrag(e)) {
9664 return;
9665 }
9666 e.preventDefault();
9667 const json = e.dataTransfer?.getData(
9668 "application/x-desktop-mode-categories"
9669 );
9670 if (!json) {
9671 return;
9672 }
9673 let parsed;
9674 try {
9675 parsed = JSON.parse(json);
9676 } catch {
9677 return;
9678 }
9679 const payload = parsed;
9680 if (!payload || !Array.isArray(payload.ids)) {
9681 return;
9682 }
9683 const incoming = [];
9684 for (const v of payload.ids) {
9685 if (typeof v === "number" && Number.isFinite(v)) {
9686 incoming.push(v);
9687 }
9688 }
9689 if (incoming.length === 0) {
9690 return;
9691 }
9692 if (payload.sourcePostId === row.id && incoming.every((id) => cellState.categoryIds.includes(id))) {
9693 return;
9694 }
9695 const merged = Array.from(
9696 /* @__PURE__ */ new Set([...cellState.categoryIds, ...incoming])
9697 );
9698 if (merged.length === cellState.categoryIds.length) {
9699 return;
9700 }
9701 const previous = cellState.categoryIds.slice();
9702 setValue(merged);
9703 try {
9704 await client.updatePostCategories(row.id, merged);
9705 const api = window.wp?.desktop;
9706 if (api && typeof api.broadcast === "function") {
9707 api.broadcast("desktop-mode.post.changed", {
9708 source: "posts-window",
9709 action: "categorized",
9710 ids: [row.id]
9711 });
9712 }
9713 } catch (err) {
9714 setValue(previous);
9715 showTagError(__("Couldn’t add category."), err);
9716 }
9717 });
9718 wrap.appendChild(picker);
9719 return wrap;
9720 }
9721 let _categoryTreePromise = null;
9722 function getCategoriesTree(client) {
9723 if (!_categoryTreePromise) {
9724 _categoryTreePromise = client.fetchAllCategories().then(
9725 (terms) => terms.map((t) => ({
9726 id: t.id,
9727 name: t.name,
9728 parent: t.parent
9729 }))
9730 );
9731 }
9732 return _categoryTreePromise;
9733 }
9734 function clearCategoryTreeCache() {
9735 _categoryTreePromise = null;
9736 }
9737 const _activePickers = /* @__PURE__ */ new Set();
9738 function broadcastFreshCategoryTreeToPickers(client) {
9739 void getCategoriesTree(client).then((tree) => {
9740 for (const picker of _activePickers) {
9741 if (picker.isConnected) {
9742 picker.items = tree;
9743 } else {
9744 _activePickers.delete(picker);
9745 }
9746 }
9747 }).catch(() => {
9748 });
9749 }
9750 async function primePickerFromCache(picker) {
9751 if (!_categoryTreePromise) {
9752 return;
9753 }
9754 try {
9755 picker.items = await _categoryTreePromise;
9756 } catch {
9757 }
9758 }
9759 function buildDateCell(row) {
9760 const wrap = document.createElement("span");
9761 wrap.style.cssText = "display:flex;flex-direction:column;line-height:1.2;";
9762 const time = document.createElement("wpd-relative-time");
9763 time.setAttribute("datetime", row.date);
9764 wrap.appendChild(time);
9765 if (row.modified_gmt && row.modified_gmt !== row.date_gmt) {
9766 const meta = document.createElement("span");
9767 meta.textContent = __("modified");
9768 meta.style.cssText = "font-size:11px;color:#646970;";
9769 wrap.appendChild(meta);
9770 }
9771 return wrap;
9772 }
9773 function buildSubRow(row) {
9774 const wrap = document.createElement("div");
9775 wrap.style.cssText = "display:flex;gap:16px;padding:12px 16px;background:#fafafa;align-items:flex-start;";
9776 const featured = featuredMediaOf(row);
9777 if (featured) {
9778 const img = document.createElement("img");
9779 img.src = featured.url;
9780 img.alt = featured.alt;
9781 img.loading = "lazy";
9782 img.style.cssText = "width:96px;height:96px;border-radius:6px;object-fit:cover;flex-shrink:0;";
9783 wrap.appendChild(img);
9784 }
9785 const text = document.createElement("div");
9786 text.style.cssText = "flex:1;min-width:0;display:flex;flex-direction:column;gap:6px;";
9787 const heading = document.createElement("div");
9788 heading.style.cssText = "font-size:13px;color:#646970;text-transform:uppercase;letter-spacing:0.04em;";
9789 heading.textContent = __("Excerpt");
9790 text.appendChild(heading);
9791 const excerpt = document.createElement("div");
9792 excerpt.style.cssText = "color:#1d2327;line-height:1.5;";
9793 const raw = row.excerpt?.rendered ?? "";
9794 if (raw) {
9795 const stripped = raw.replace(/<[^>]+>/g, "").trim();
9796 excerpt.textContent = stripped || __("(no excerpt)");
9797 } else {
9798 excerpt.textContent = __("(no excerpt)");
9799 excerpt.style.color = "#a7aaad";
9800 }
9801 text.appendChild(excerpt);
9802 wrap.appendChild(text);
9803 return wrap;
9804 }
9805 async function renderPostsWindow(body, client) {
9806 const root = body.querySelector(ROOT$1);
9807 const table = body.querySelector(TABLE$1);
9808 if (!root || !table) {
9809 return;
9810 }
9811 maybeShowIntro(client);
9812 const catsHost = body.querySelector(
9813 "[data-desktop-mode-posts-cats-host]"
9814 );
9815 const tagsHost = body.querySelector(
9816 "[data-desktop-mode-posts-tags-host]"
9817 );
9818 let catsTeardown = null;
9819 let tagsTeardown = null;
9820 const tabsEl = body.querySelector(".desktop-mode-posts__tabs");
9821 if (tabsEl) {
9822 tabsEl.addEventListener("wpd-tab-change", (e) => {
9823 const detail = e.detail;
9824 const value = detail?.value;
9825 if (value === "categories" && catsHost && !catsTeardown) {
9826 void Promise.resolve().then(() => categoriesMindmap).then(
9827 async ({ mountCategoriesMindmap: mountCategoriesMindmap2 }) => {
9828 catsTeardown = await mountCategoriesMindmap2(catsHost, client);
9829 }
9830 );
9831 }
9832 if (value === "tags" && tagsHost && !tagsTeardown) {
9833 void Promise.resolve().then(() => tagsCloud).then(
9834 async ({ mountTagsCloud: mountTagsCloud2 }) => {
9835 tagsTeardown = await mountTagsCloud2(tagsHost, client);
9836 }
9837 );
9838 }
9839 });
9840 }
9841 const cfg = client.getConfig();
9842 const view = {
9843 page: 1,
9844 perPage: Math.max(1, cfg.defaultPerPage || 20),
9845 search: "",
9846 status: "",
9847 orderby: "date",
9848 order: "desc",
9849 author: [],
9850 tag: [],
9851 searchDebounce: null
9852 };
9853 const cellCache = /* @__PURE__ */ new Map();
9854 const filterData = { authors: [], tags: [] };
9855 table.columns = buildColumns$1(cellCache, client, filterData);
9856 table.getRowId = (row) => row.id;
9857 table.subTable = (row) => buildSubRow(row);
9858 table.sort = { key: "date", direction: "desc" };
9859 let totalPages = 0;
9860 let totalRows = 0;
9861 let refreshSeq = 0;
9862 const perPageEl = root.querySelector(PER_PAGE$1);
9863 if (perPageEl) {
9864 perPageEl.value = String(view.perPage);
9865 }
9866 const indicator = root.querySelector(PAGE_INDICATOR$1);
9867 const prevBtn = root.querySelector(PREV$1);
9868 const nextBtn = root.querySelector(NEXT$1);
9869 const bulkBar = root.querySelector(BULK$1);
9870 const countEl = root.querySelector(COUNT$1);
9871 const bulkActionsHost = root.querySelector(BULK_ACTIONS_HOST$1);
9872 const trailingExtras = root.querySelector(
9873 TOOLBAR_TRAILING_EXTRAS
9874 );
9875 const statusHost = root.querySelector(STATUS$1);
9876 const statusSegments = resolveStatusSegments();
9877 if (statusHost) {
9878 statusHost.replaceChildren();
9879 for (const seg of statusSegments) {
9880 const el = document.createElement("wpd-segment");
9881 el.setAttribute("value", seg.value);
9882 el.textContent = seg.label;
9883 statusHost.appendChild(el);
9884 }
9885 statusHost.setAttribute("value", view.status);
9886 }
9887 const updatePager = () => {
9888 if (indicator) {
9889 if (totalRows === 0) {
9890 indicator.textContent = __("No posts");
9891 } else {
9892 indicator.textContent = sprintf(
9893 /* translators: 1: current page, 2: total pages, 3: total posts. */
9894 __("Page %1$d of %2$d · %3$d posts"),
9895 view.page,
9896 Math.max(totalPages, 1),
9897 totalRows
9898 );
9899 }
9900 }
9901 if (prevBtn) {
9902 prevBtn.toggleAttribute("disabled", view.page <= 1);
9903 }
9904 if (nextBtn) {
9905 nextBtn.toggleAttribute("disabled", view.page >= totalPages);
9906 }
9907 };
9908 const updateBulkBar = () => {
9909 if (!bulkBar || !countEl) {
9910 return;
9911 }
9912 const sel = Array.from(table.selection ?? []);
9913 if (sel.length === 0) {
9914 bulkBar.hidden = true;
9915 return;
9916 }
9917 bulkBar.hidden = false;
9918 countEl.textContent = sprintf(
9919 /* translators: %d: selected row count. */
9920 __("%d selected"),
9921 sel.length
9922 );
9923 };
9924 const buildParams = () => ({
9925 page: view.page,
9926 perPage: view.perPage,
9927 search: view.search || void 0,
9928 status: view.status || void 0,
9929 orderby: view.orderby,
9930 order: view.order,
9931 author: view.author.length > 0 ? view.author : void 0,
9932 tag: view.tag.length > 0 ? view.tag : void 0
9933 });
9934 const ctx = {
9935 body,
9936 table,
9937 refresh: () => refresh(),
9938 getSelectedIds: () => Array.from(table.selection ?? []).map((id) => Number(id)),
9939 getSelectedRows: () => {
9940 const ids = new Set(ctx.getSelectedIds());
9941 return (table.data ?? []).filter((r) => ids.has(r.id));
9942 },
9943 getCurrentParams: () => buildParams()
9944 };
9945 const refresh = async () => {
9946 const mySeq = ++refreshSeq;
9947 table.toggleAttribute("loading", true);
9948 try {
9949 const result = await client.fetchPosts(buildParams());
9950 if (mySeq !== refreshSeq) {
9951 return;
9952 }
9953 if (result.items.length === 0 && view.page > 1 && result.totalPages > 0 && view.page > result.totalPages) {
9954 view.page = 1;
9955 await refresh();
9956 return;
9957 }
9958 cellCache.clear();
9959 refreshParentTitleRoster(result.items);
9960 table.data = result.items;
9961 totalRows = result.total;
9962 totalPages = result.totalPages;
9963 updatePager();
9964 const hooks2 = window.wp?.hooks;
9965 if (hooks2 && typeof hooks2.doAction === "function") {
9966 hooks2.doAction(HOOK_ACTION_DATA_LOADED, {
9967 items: result.items,
9968 total: result.total,
9969 totalPages: result.totalPages,
9970 page: view.page
9971 });
9972 }
9973 document.dispatchEvent(
9974 new CustomEvent("desktop-mode-posts-window-data-loaded", {
9975 detail: {
9976 items: result.items,
9977 total: result.total,
9978 totalPages: result.totalPages,
9979 page: view.page
9980 }
9981 })
9982 );
9983 } catch (err) {
9984 if (mySeq !== refreshSeq) {
9985 return;
9986 }
9987 console.error("[posts-window] list failed", err);
9988 table.data = [];
9989 totalRows = 0;
9990 totalPages = 0;
9991 updatePager();
9992 } finally {
9993 if (mySeq === refreshSeq) {
9994 table.toggleAttribute("loading", false);
9995 updateBulkBar();
9996 }
9997 }
9998 };
9999 const goToFirstPage = () => {
10000 if (view.page !== 1) {
10001 view.page = 1;
10002 }
10003 };
10004 root.querySelector(STATUS$1)?.addEventListener("wpd-pick", (e) => {
10005 const value = e.detail?.value ?? "";
10006 view.status = value;
10007 goToFirstPage();
10008 void refresh();
10009 });
10010 root.querySelector(SEARCH$1)?.addEventListener(
10011 "wpd-input-change",
10012 (e) => {
10013 const value = e.detail?.value ?? "";
10014 view.search = value;
10015 if (view.searchDebounce !== null) {
10016 window.clearTimeout(view.searchDebounce);
10017 }
10018 view.searchDebounce = window.setTimeout(() => {
10019 goToFirstPage();
10020 void refresh();
10021 }, SEARCH_DEBOUNCE_MS$1);
10022 }
10023 );
10024 body.addEventListener("click", (e) => {
10025 const target = e.target;
10026 if (!target) {
10027 return;
10028 }
10029 if (target.closest(REFRESH$1)) {
10030 void refresh();
10031 return;
10032 }
10033 if (target.closest(NEW_BTN$1)) {
10034 openAdminUrl(cfg.newPostUrl, {
10035 title: __("Add New Post"),
10036 icon: "dashicons-admin-post"
10037 });
10038 return;
10039 }
10040 if (target.closest(PREV$1)) {
10041 if (view.page > 1) {
10042 view.page -= 1;
10043 void refresh();
10044 }
10045 return;
10046 }
10047 if (target.closest(NEXT$1)) {
10048 if (view.page < totalPages) {
10049 view.page += 1;
10050 void refresh();
10051 }
10052 }
10053 });
10054 const bulkActions = resolveBulkActions(client);
10055 if (bulkActionsHost) {
10056 bulkActionsHost.replaceChildren();
10057 for (const action of bulkActions) {
10058 bulkActionsHost.appendChild(buildBulkActionButton(action, ctx));
10059 }
10060 }
10061 if (trailingExtras) {
10062 const extras = resolveToolbarTrailing(ctx);
10063 trailingExtras.replaceChildren(...extras);
10064 }
10065 perPageEl?.addEventListener("change", () => {
10066 const next = parseInt(perPageEl.value, 10);
10067 if (!Number.isFinite(next) || next < 1) {
10068 return;
10069 }
10070 view.perPage = next;
10071 goToFirstPage();
10072 void refresh();
10073 });
10074 table.addEventListener("wpd-table-selection-change", () => {
10075 updateBulkBar();
10076 });
10077 table.addEventListener("wpd-table-sort-change", (e) => {
10078 const detail = e.detail;
10079 if (!detail || !detail.sort) {
10080 view.orderby = "date";
10081 view.order = "desc";
10082 } else {
10083 view.orderby = mapColumnToOrderby(detail.sort.key);
10084 view.order = detail.sort.direction;
10085 }
10086 void refresh();
10087 });
10088 const parseIds = (raw) => raw.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => Number.isFinite(n) && n > 0);
10089 const sameIds = (a, b) => a.length === b.length && a.every((v, i) => v === b[i]);
10090 table.addEventListener("wpd-table-filter-change", (e) => {
10091 const detail = e.detail;
10092 const filters = detail?.filters ?? {};
10093 const nextAuthor = parseIds(filters.author ?? "");
10094 const nextTag = parseIds(filters.tags ?? "");
10095 const changed = !sameIds(nextAuthor, view.author) || !sameIds(nextTag, view.tag);
10096 if (!changed) {
10097 return;
10098 }
10099 view.author = nextAuthor;
10100 view.tag = nextTag;
10101 view.page = 1;
10102 void refresh();
10103 });
10104 activeRunBulkAction = async (action, actionCtx) => {
10105 const ids = actionCtx.getSelectedIds();
10106 if (ids.length === 0) {
10107 return;
10108 }
10109 if (action.confirm) {
10110 const ok = await wpdConfirmGlobal$1({
10111 message: sprintf(
10112 /* translators: %d: row count. */
10113 action.confirm,
10114 ids.length
10115 ),
10116 danger: true
10117 });
10118 if (!ok) {
10119 return;
10120 }
10121 }
10122 try {
10123 const result = await action.run(ids, actionCtx);
10124 if (result === false) {
10125 return;
10126 }
10127 } catch (err) {
10128 console.error(
10129 `[posts-window] bulk action "${action.id}" failed`,
10130 err
10131 );
10132 }
10133 table.clearSelection();
10134 await refresh();
10135 };
10136 const broadcastUnsubs = [];
10137 if (window.wp?.desktop && typeof window.wp.desktop.subscribe === "function") {
10138 const onChange = (payload) => {
10139 const detail = payload;
10140 if (detail?.source === "posts-window") {
10141 return;
10142 }
10143 void refresh();
10144 };
10145 broadcastUnsubs.push(
10146 window.wp.desktop.subscribe("desktop-mode.post.changed", onChange)
10147 );
10148 const onTermChange = (payload) => {
10149 const detail = payload;
10150 if (detail?.taxonomy === "category") {
10151 clearCategoryTreeCache();
10152 broadcastFreshCategoryTreeToPickers(client);
10153 }
10154 };
10155 broadcastUnsubs.push(
10156 window.wp.desktop.subscribe(
10157 "desktop-mode.term.changed",
10158 onTermChange
10159 )
10160 );
10161 }
10162 const repaintColumns = () => {
10163 cellCache.clear();
10164 table.columns = buildColumns$1(cellCache, client, filterData);
10165 };
10166 void client.fetchAuthorOptions().then((authors) => {
10167 filterData.authors = authors;
10168 repaintColumns();
10169 });
10170 let tagPage = 0;
10171 let tagTotalPages = 1;
10172 let tagFetching = false;
10173 const TAG_PAGE_SIZE = 50;
10174 const fetchNextTagPage = async () => {
10175 if (tagFetching || tagPage >= tagTotalPages) {
10176 return;
10177 }
10178 tagFetching = true;
10179 try {
10180 const next = tagPage + 1;
10181 const res = await client.fetchTagOptions(next, TAG_PAGE_SIZE);
10182 tagPage = next;
10183 tagTotalPages = Math.max(tagTotalPages, res.totalPages || next);
10184 const seen = new Set(filterData.tags.map((t) => t.id));
10185 for (const item of res.items) {
10186 if (!seen.has(item.id)) {
10187 filterData.tags.push(item);
10188 seen.add(item.id);
10189 }
10190 }
10191 filterData.tagsHasMore = tagPage < tagTotalPages;
10192 repaintColumns();
10193 } finally {
10194 tagFetching = false;
10195 }
10196 };
10197 filterData.loadMoreTags = () => {
10198 void fetchNextTagPage();
10199 };
10200 void fetchNextTagPage();
10201 const teardownKebabColumns = mountKebabColumnToggles(
10202 body,
10203 cellCache,
10204 repaintColumns,
10205 client
10206 );
10207 let unsubOsSettings = null;
10208 if (window.wp?.desktop && typeof window.wp.desktop.subscribeOsSettings === "function") {
10209 let lastHidden = JSON.stringify(
10210 Array.from(getHiddenColumns()).sort()
10211 );
10212 unsubOsSettings = window.wp.desktop.subscribeOsSettings(() => {
10213 const next = JSON.stringify(
10214 Array.from(getHiddenColumns()).sort()
10215 );
10216 if (next === lastHidden) {
10217 return;
10218 }
10219 lastHidden = next;
10220 repaintColumns();
10221 teardownKebabColumns?.refresh();
10222 });
10223 }
10224 const onWindowClosed = (e) => {
10225 const detail = e.detail;
10226 if (detail?.windowId !== "desktop-mode-posts") {
10227 return;
10228 }
10229 document.removeEventListener("desktop-mode-window-closed", onWindowClosed);
10230 for (const unsub of broadcastUnsubs) {
10231 try {
10232 unsub();
10233 } catch {
10234 }
10235 }
10236 broadcastUnsubs.length = 0;
10237 teardownKebabColumns?.dispose();
10238 unsubOsSettings?.();
10239 catsTeardown?.();
10240 catsTeardown = null;
10241 tagsTeardown?.();
10242 tagsTeardown = null;
10243 if (view.searchDebounce !== null) {
10244 window.clearTimeout(view.searchDebounce);
10245 view.searchDebounce = null;
10246 }
10247 clearCategoryTreeCache();
10248 };
10249 document.addEventListener("desktop-mode-window-closed", onWindowClosed);
10250 await refresh();
10251 const hooks = window.wp?.hooks;
10252 if (hooks && typeof hooks.doAction === "function") {
10253 hooks.doAction(HOOK_ACTION_OPENED, ctx);
10254 }
10255 document.dispatchEvent(
10256 new CustomEvent("desktop-mode-posts-window-opened", {
10257 detail: ctx
10258 })
10259 );
10260 }
10261 function buildBulkActionButton(action, ctx) {
10262 const btn = document.createElement("wpd-button");
10263 btn.setAttribute("variant", action.variant ?? "secondary");
10264 btn.setAttribute("data-desktop-mode-posts-bulk-action", action.id);
10265 if (action.icon) {
10266 const icon = document.createElement("span");
10267 icon.className = `dashicons ${action.icon}`;
10268 icon.setAttribute("aria-hidden", "true");
10269 btn.appendChild(icon);
10270 }
10271 btn.appendChild(document.createTextNode(" " + action.label));
10272 btn.addEventListener("click", () => {
10273 void runBulkActionFor(action, ctx);
10274 });
10275 return btn;
10276 }
10277 let activeRunBulkAction = async () => {
10278 };
10279 async function runBulkActionFor(action, ctx) {
10280 await activeRunBulkAction(action, ctx);
10281 }
10282 function openAdminUrl(url, opts = {}) {
10283 const api = window.wp?.desktop;
10284 if (!api || !api.windowManager || !api.deriveWindowId) {
10285 window.location.href = url;
10286 return;
10287 }
10288 const id = api.deriveWindowId(url);
10289 api.windowManager.open({
10290 id,
10291 baseId: id,
10292 url,
10293 title: opts.title ?? url,
10294 icon: opts.icon ?? "dashicons-admin-generic"
10295 });
10296 }
10297 function mapColumnToOrderby(key) {
10298 switch (key) {
10299 case "title":
10300 return "title";
10301 case "author":
10302 return "author";
10303 case "date":
10304 return "date";
10305 case "modified":
10306 return "modified";
10307 case "comments":
10308 return "comment_count";
10309 default:
10310 return "date";
10311 }
10312 }
10313 const registry = window.desktopModeNativeWindows ?? (window.desktopModeNativeWindows = {});
10314 registry["desktop-mode-posts"] = (body) => {
10315 const client = createPostsWindowClient("desktop-mode-posts");
10316 return renderPostsWindow(body, client).catch((err) => {
10317 console.error("[posts-window] render failed:", err);
10318 });
10319 };
10320 registry["desktop-mode-pages"] = (body) => {
10321 const client = createPostsWindowClient("desktop-mode-pages");
10322 return renderPostsWindow(body, client).catch((err) => {
10323 console.error("[pages-window] render failed:", err);
10324 });
10325 };
10326 registry["desktop-mode-users"] = (body) => {
10327 const client = createUsersWindowClient("desktop-mode-users");
10328 return Promise.resolve().then(() => usersRender).then((m) => m.renderUsersWindow(body, client)).catch((err) => {
10329 console.error("[users-window] render failed:", err);
10330 });
10331 };
10332 registry["desktop-mode-user-edit"] = (body) => {
10333 const profile = body.querySelector(
10334 "wpd-user-profile[data-wpd-user-profile-host]"
10335 );
10336 if (!profile) {
10337 return;
10338 }
10339 void Promise.resolve().then(() => userEditTarget).then((target) => {
10340 const pending = target.readUserEditTarget();
10341 let userId = pending.userId && pending.userId > 0 ? pending.userId : 0;
10342 if (userId <= 0) {
10343 try {
10344 userId = window.desktopModeWindowConfig?.["desktop-mode-user-edit"]?.currentUserId ?? 0;
10345 } catch {
10346 userId = 0;
10347 }
10348 }
10349 if (userId > 0) {
10350 profile.setAttribute("user-id", String(userId));
10351 }
10352 target.clearUserEditTarget();
10353 target.subscribeUserEditTarget((next) => {
10354 if (!profile.isConnected) {
10355 return;
10356 }
10357 if (next.userId && next.userId > 0 && next.userId !== userId) {
10358 userId = next.userId;
10359 profile.setAttribute("user-id", String(userId));
10360 target.clearUserEditTarget();
10361 }
10362 });
10363 });
10364 };
10365 function createUserEditClient(windowId = "desktop-mode-user-edit") {
10366 const getConfig = () => {
10367 const store = window.desktopModeWindowConfig;
10368 const cfg = store?.[windowId];
10369 if (!cfg) {
10370 throw new Error(
10371 `[${windowId}] config blob is missing — was the window opened without registration? See \`includes/user-edit-window/window.php\`.`
10372 );
10373 }
10374 return cfg;
10375 };
10376 const shellFetch = (input, init, source) => {
10377 return trackedFetch(input, init, {
10378 windowId,
10379 source: source ?? "user-edit-window/rest"
10380 });
10381 };
10382 const fetchUser = async (id) => {
10383 const cfg = getConfig();
10384 const base = cfg.usersUrl ?? joinRestUrl(cfg.restRoot, "wp/v2/users");
10385 const url = joinRestUrl(base, `${id}?context=edit`);
10386 const res = await shellFetch(
10387 url,
10388 {
10389 method: "GET",
10390 credentials: "same-origin",
10391 headers: {
10392 Accept: "application/json",
10393 "X-WP-Nonce": cfg.restNonce
10394 }
10395 },
10396 "user-edit-window/load"
10397 );
10398 if (!res.ok) {
10399 throw new Error(`[user-edit] load failed: ${res.status}`);
10400 }
10401 return await res.json();
10402 };
10403 const saveUser = async (id, patch) => {
10404 const cfg = getConfig();
10405 const base = cfg.usersUrl ?? joinRestUrl(cfg.restRoot, "wp/v2/users");
10406 const res = await shellFetch(
10407 joinRestUrl(base, `${id}?context=edit`),
10408 {
10409 method: "POST",
10410 // PUT == POST for WP REST when X-HTTP-Method-Override is unsupported.
10411 credentials: "same-origin",
10412 headers: {
10413 "Content-Type": "application/json",
10414 "X-WP-Nonce": cfg.restNonce,
10415 "X-HTTP-Method-Override": "PUT"
10416 },
10417 body: JSON.stringify(patch)
10418 },
10419 "user-edit-window/save"
10420 );
10421 if (!res.ok) {
10422 const data = await res.json().catch(() => ({}));
10423 const fieldErrors = {};
10424 const params = data.data?.params;
10425 if (params && typeof params === "object") {
10426 for (const [k, v] of Object.entries(params)) {
10427 fieldErrors[k] = String(v);
10428 }
10429 }
10430 return {
10431 ok: false,
10432 error: data.code ?? `http_${res.status}`,
10433 message: data.message,
10434 fieldErrors
10435 };
10436 }
10437 const user = await res.json();
10438 return { ok: true, user };
10439 };
10440 const fetchInsights = async (id, opts = {}) => {
10441 const cfg = getConfig();
10442 const base = cfg.insightsUrlBase ?? joinRestUrl(cfg.restRoot, "desktop-mode/v1/users/");
10443 const url = new URL(joinRestUrl(base, `${id}/insights`));
10444 if (opts.fresh) {
10445 url.searchParams.set("fresh", "1");
10446 }
10447 const res = await shellFetch(
10448 url.toString(),
10449 {
10450 method: "GET",
10451 credentials: "same-origin",
10452 headers: {
10453 Accept: "application/json",
10454 "X-WP-Nonce": cfg.restNonce
10455 }
10456 },
10457 "user-edit-window/insights"
10458 );
10459 if (!res.ok) {
10460 throw new Error(`[user-edit] insights failed: ${res.status}`);
10461 }
10462 return await res.json();
10463 };
10464 return {
10465 windowId,
10466 getConfig,
10467 fetchUser,
10468 saveUser,
10469 fetchInsights
10470 };
10471 }
10472 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}`;
10473 const _WpdCheckboxLabel = class _WpdCheckboxLabel extends Component {
10474 render() {
10475 const label = this.label || "";
10476 const checked = this.checked !== null;
10477 return html`
10478 <label>
10479 <input
10480 type="checkbox"
10481 ?checked=${checked}
10482 @change=${(e) => this._onChange(e)}
10483 />
10484 <span class="wpd-checkbox-label__text">${label}</span>
10485 </label>
10486 `;
10487 }
10488 _onChange(e) {
10489 const next = e.target.checked;
10490 if (next) {
10491 this.setAttribute("checked", "");
10492 } else {
10493 this.removeAttribute("checked");
10494 }
10495 this.emit("wpd-checkbox-change", { checked: next });
10496 }
10497 };
10498 _WpdCheckboxLabel.props = ["label", "checked"];
10499 _WpdCheckboxLabel.styles = [styles$1];
10500 _WpdCheckboxLabel.help = {
10501 title: "Checkbox label",
10502 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.",
10503 status: "stable",
10504 since: "0.9.0",
10505 props: [
10506 {
10507 name: "label",
10508 type: "string",
10509 description: "Visible label text, paired with the checkbox via a native <label>."
10510 },
10511 {
10512 name: "checked",
10513 type: "boolean attribute",
10514 description: "Reflects and controls the checked state."
10515 }
10516 ],
10517 events: [
10518 {
10519 name: "wpd-checkbox-change",
10520 description: "Fires when the user toggles the checkbox.",
10521 detail: "{ checked: boolean }"
10522 }
10523 ],
10524 cssProps: [
10525 { name: "--desktop-mode-text", description: "Label colour." }
10526 ],
10527 example: html`
10528 <wpd-checkbox-label label="Reduce motion" checked></wpd-checkbox-label>
10529 `
10530 };
10531 let WpdCheckboxLabel = _WpdCheckboxLabel;
10532 defineComponent("wpd-checkbox-label", WpdCheckboxLabel);
10533 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}`;
10534 let _cache = null;
10535 function parseCssContentToChar(raw) {
10536 let value = raw.trim();
10537 if (value === "") {
10538 return null;
10539 }
10540 if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
10541 value = value.slice(1, -1);
10542 }
10543 const escaped = value.match(/^\\([0-9a-f]{1,6})\s?$/i);
10544 if (escaped) {
10545 return String.fromCodePoint(parseInt(escaped[1], 16));
10546 }
10547 return value || null;
10548 }
10549 function buildMap() {
10550 const map = /* @__PURE__ */ new Map();
10551 if (typeof document === "undefined") {
10552 return map;
10553 }
10554 const sheets = Array.from(document.styleSheets ?? []);
10555 for (const sheet of sheets) {
10556 let rules = null;
10557 try {
10558 rules = sheet.cssRules;
10559 } catch {
10560 continue;
10561 }
10562 if (!rules) {
10563 continue;
10564 }
10565 for (const rule of Array.from(rules)) {
10566 const styleRule = rule;
10567 if (!styleRule || !styleRule.selectorText) {
10568 continue;
10569 }
10570 const match = styleRule.selectorText.match(
10571 /\.dashicons-([a-z0-9-]+)::?before/i
10572 );
10573 if (!match) {
10574 continue;
10575 }
10576 const content = styleRule.style?.content;
10577 if (!content) {
10578 continue;
10579 }
10580 const char = parseCssContentToChar(content);
10581 if (char) {
10582 map.set(match[1], char);
10583 }
10584 }
10585 }
10586 return map;
10587 }
10588 function resolveDashicon(name) {
10589 if (!_cache) {
10590 _cache = buildMap();
10591 }
10592 const slug = name.startsWith("dashicons-") ? name.slice("dashicons-".length) : name;
10593 return _cache.get(slug) ?? null;
10594 }
10595 function refreshDashiconCache() {
10596 _cache = buildMap();
10597 }
10598 let _scheduled = false;
10599 function primeOnLoad() {
10600 if (_scheduled || typeof window === "undefined") {
10601 return;
10602 }
10603 _scheduled = true;
10604 const refresh = () => {
10605 refreshDashiconCache();
10606 };
10607 if (document.readyState === "loading") {
10608 document.addEventListener("DOMContentLoaded", refresh, { once: true });
10609 }
10610 window.addEventListener("load", refresh, { once: true });
10611 }
10612 primeOnLoad();
10613 const _WpdIcon = class _WpdIcon extends Component {
10614 render() {
10615 const rawName = this.name || "";
10616 const slug = rawName.startsWith("dashicons-") ? rawName.slice("dashicons-".length) : rawName;
10617 const size = this.size;
10618 if (size && /^\d+$/.test(size)) {
10619 this.style.setProperty("--wpd-icon-size", `${size}px`);
10620 }
10621 const char = resolveDashicon(slug);
10622 if (char) {
10623 return html`<span
10624 class="wpd-icon__glyph wpd-icon__glyph--char dashicons dashicons-${slug}"
10625 aria-hidden="true"
10626 >${char}</span>`;
10627 }
10628 return html`<span
10629 class="wpd-icon__glyph dashicons dashicons-${slug}"
10630 aria-hidden="true"
10631 ></span>`;
10632 }
10633 };
10634 _WpdIcon.props = ["name", "size"];
10635 _WpdIcon.styles = [styles];
10636 _WpdIcon.help = {
10637 title: "Icon",
10638 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.',
10639 status: "stable",
10640 since: "0.10.0",
10641 props: [
10642 {
10643 name: "name",
10644 type: "string",
10645 description: "Dashicon identifier, with or without the `dashicons-` prefix."
10646 },
10647 {
10648 name: "size",
10649 type: "integer (px)",
10650 default: "16",
10651 description: "Glyph size in pixels."
10652 }
10653 ],
10654 cssProps: [
10655 { name: "--wpd-icon-size", default: "16px" }
10656 ],
10657 example: html`
10658 <wpd-cluster gap="8" align="center">
10659 <wpd-icon name="admin-post"></wpd-icon>
10660 <wpd-icon name="calculator" size="20"></wpd-icon>
10661 <wpd-icon name="dashicons-star-filled" size="32"></wpd-icon>
10662 </wpd-cluster>
10663 `
10664 };
10665 let WpdIcon = _WpdIcon;
10666 defineComponent("wpd-icon", WpdIcon);
10667 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}`;
10668 const _WpdTextField = class _WpdTextField extends Component {
10669 constructor() {
10670 super(...arguments);
10671 this._revealed = false;
10672 }
10673 connectedCallback() {
10674 super.connectedCallback();
10675 ensureAutoId(this);
10676 }
10677 render() {
10678 const label = this.label || "";
10679 const value = this.value ?? "";
10680 const placeholder = this.placeholder || "";
10681 const disabled = this.disabled !== null;
10682 const readonly = this.readonly !== null;
10683 const declaredAutocomplete = this.autocomplete;
10684 const declaredType = this.type || "text";
10685 const isPassword = declaredType === "password";
10686 let autocomplete = declaredAutocomplete || "off";
10687 if (isPassword && (!declaredAutocomplete || autocomplete === "off")) {
10688 autocomplete = "new-password";
10689 }
10690 const maxLength = this.maxlength;
10691 const minLength = this.minlength;
10692 const pattern = this.pattern || "";
10693 const name = this.name || "";
10694 const suffix = this.suffix || "";
10695 const invalid = this.invalid !== null;
10696 const reveal = this.reveal !== null;
10697 const isPasswordIntent = declaredType === "password";
10698 const isMasked = isPasswordIntent && !(reveal && this._revealed);
10699 let effectiveType;
10700 if (isPasswordIntent) {
10701 effectiveType = "text";
10702 } else if (reveal && this._revealed) {
10703 effectiveType = "text";
10704 } else {
10705 effectiveType = declaredType;
10706 }
10707 const rowClass = reveal ? "wpd-text-field__row wpd-text-field__row--has-reveal" : "wpd-text-field__row";
10708 const inputClass = isMasked ? "wpd-text-field__input wpd-text-field__input--masked" : "wpd-text-field__input";
10709 const hostId = this.id || "wpd-unnamed";
10710 const inputId = `${hostId}__input`;
10711 return html`
10712 ${label ? html`<label
10713 class="wpd-text-field__label"
10714 for=${inputId}
10715 >${label}</label>` : html``}
10716 <span class=${rowClass}>
10717 <input
10718 id=${inputId}
10719 class=${inputClass}
10720 type=${effectiveType}
10721 .value=${value}
10722 placeholder=${placeholder}
10723 ?disabled=${disabled}
10724 ?readonly=${readonly}
10725 autocomplete=${autocomplete}
10726 maxlength=${maxLength ?? ""}
10727 minlength=${minLength ?? ""}
10728 pattern=${pattern}
10729 name=${name}
10730 aria-invalid=${invalid ? "true" : "false"}
10731 aria-label=${label || ""}
10732 @input=${(e) => this._onInput(e)}
10733 @change=${(e) => this._onChange(e)}
10734 @keydown=${(e) => this._onKeyDown(e)}
10735 />
10736 ${suffix ? html`<span class="wpd-text-field__suffix">${suffix}</span>` : html``}
10737 ${reveal ? this._renderRevealButton(disabled) : html``}
10738 </span>
10739 `;
10740 }
10741 _renderRevealButton(disabled) {
10742 const label = this._revealed ? "Hide" : "Show";
10743 return html`
10744 <button
10745 type="button"
10746 class="wpd-text-field__reveal"
10747 aria-label=${label}
10748 aria-pressed=${this._revealed ? "true" : "false"}
10749 ?disabled=${disabled}
10750 tabindex="0"
10751 @click=${() => this._onToggleReveal()}
10752 >
10753 ${this._revealed ? _iconEyeOff() : _iconEye()}
10754 </button>
10755 `;
10756 }
10757 _onToggleReveal() {
10758 this._revealed = !this._revealed;
10759 this.requestUpdate();
10760 }
10761 _onInput(e) {
10762 const input = e.target;
10763 this.value = input.value;
10764 this.emit("wpd-input-change", { value: input.value });
10765 }
10766 _onChange(e) {
10767 const input = e.target;
10768 this.emit("wpd-input-commit", { value: input.value });
10769 }
10770 _onKeyDown(e) {
10771 if (e.key === "Enter" && !e.shiftKey && !e.altKey && !e.metaKey) {
10772 const input = e.target;
10773 this.emit("wpd-submit", { value: input.value });
10774 }
10775 }
10776 };
10777 _WpdTextField.props = [
10778 "label",
10779 "value",
10780 "placeholder",
10781 "disabled",
10782 "readonly",
10783 "autocomplete",
10784 "type",
10785 "maxlength",
10786 "minlength",
10787 "pattern",
10788 "name",
10789 "suffix",
10790 "invalid",
10791 "reveal"
10792 ];
10793 _WpdTextField.styles = [textFieldStyles];
10794 _WpdTextField.help = {
10795 title: "Text field",
10796 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.",
10797 status: "stable",
10798 since: "0.11.0",
10799 props: [
10800 { name: "label", type: "string", description: "Visible label above the input." },
10801 { name: "value", type: "string", description: "Current input value; reflected two-way." },
10802 { name: "placeholder", type: "string", description: "Native placeholder string." },
10803 { name: "disabled", type: "boolean attribute", description: "Disables the native input." },
10804 { name: "readonly", type: "boolean attribute", description: "Marks the input readonly." },
10805 {
10806 name: "autocomplete",
10807 type: "string",
10808 default: "off",
10809 description: "Forwarded to the native input autocomplete attribute."
10810 },
10811 {
10812 name: "type",
10813 type: "string",
10814 default: "text",
10815 description: "Native input type (text, password, email, search, tel, url)."
10816 },
10817 { name: "maxlength", type: "integer (string)", description: "Native maxlength." },
10818 { name: "minlength", type: "integer (string)", description: "Native minlength." },
10819 { name: "pattern", type: "regex string", description: "Native validation pattern." },
10820 { name: "name", type: "string", description: "Forwarded to the native input for form submission." },
10821 { name: "suffix", type: "string", description: "Text rendered inside the right edge of the input row." },
10822 {
10823 name: "invalid",
10824 type: "boolean attribute",
10825 description: "Marks the field aria-invalid and applies the error style."
10826 },
10827 {
10828 name: "reveal",
10829 type: "boolean attribute",
10830 description: 'On type="password" fields, adds an eye-icon toggle that flips the input between hidden and visible text.'
10831 }
10832 ],
10833 events: [
10834 {
10835 name: "wpd-input-change",
10836 description: "Fires on every input keystroke.",
10837 detail: "{ value: string }"
10838 },
10839 {
10840 name: "wpd-input-commit",
10841 description: "Fires on the native change event (blur / Enter).",
10842 detail: "{ value: string }"
10843 },
10844 {
10845 name: "wpd-submit",
10846 description: "Fires when the user presses Enter (without Shift/Alt/Meta).",
10847 detail: "{ value: string }"
10848 }
10849 ],
10850 cssProps: [
10851 { name: "--desktop-mode-text", description: "Text colour." },
10852 { name: "--desktop-mode-muted", description: "Label + suffix colour." },
10853 { name: "--desktop-mode-border", description: "Input outline." },
10854 { name: "--desktop-mode-window-bg", description: "Input background." }
10855 ],
10856 example: html`
10857 <wpd-stack gap="8">
10858 <wpd-text-field label="Note title" value="Untitled" placeholder="Name this note"></wpd-text-field>
10859 <wpd-text-field type="password" reveal label="API key"></wpd-text-field>
10860 </wpd-stack>
10861 `
10862 };
10863 let WpdTextField = _WpdTextField;
10864 defineComponent("wpd-text-field", WpdTextField);
10865 function _iconEye() {
10866 return html`
10867 <svg
10868 viewBox="0 0 16 16"
10869 width="14"
10870 height="14"
10871 fill="none"
10872 stroke="currentColor"
10873 stroke-width="1.5"
10874 stroke-linecap="round"
10875 stroke-linejoin="round"
10876 aria-hidden="true"
10877 focusable="false"
10878 >
10879 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
10880 <circle cx="8" cy="8" r="2" />
10881 </svg>
10882 `;
10883 }
10884 function _iconEyeOff() {
10885 return html`
10886 <svg
10887 viewBox="0 0 16 16"
10888 width="14"
10889 height="14"
10890 fill="none"
10891 stroke="currentColor"
10892 stroke-width="1.5"
10893 stroke-linecap="round"
10894 stroke-linejoin="round"
10895 aria-hidden="true"
10896 focusable="false"
10897 >
10898 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
10899 <circle cx="8" cy="8" r="2" />
10900 <line x1="2" y1="2" x2="14" y2="14" />
10901 </svg>
10902 `;
10903 }
10904 function resolveUserEditClient() {
10905 const store = window.desktopModeWindowConfig;
10906 if (store?.["desktop-mode-user-edit"]) {
10907 return createUserEditClient("desktop-mode-user-edit");
10908 }
10909 if (store?.["desktop-mode-users"]) {
10910 return createUserEditClient("desktop-mode-users");
10911 }
10912 return createUserEditClient("desktop-mode-user-edit");
10913 }
10914 function notifyToast$1(body, kind = "info") {
10915 const api = window.wp?.desktop;
10916 if (api?.showToast) {
10917 let duration;
10918 if (kind === "error") {
10919 duration = 8e3;
10920 } else if (kind === "success") {
10921 duration = 5e3;
10922 }
10923 api.showToast({ message: body, duration });
10924 return;
10925 }
10926 console.info("[user-edit-window]", body);
10927 }
10928 async function mountProfileFormAt(host, userId) {
10929 return loadAndMountProfile(host, userId);
10930 }
10931 async function mountProfileAsideAt(host, userId, fresh) {
10932 return renderInsightsAside(host, userId, fresh);
10933 }
10934 async function mountProfileActivityAt(host, userId, fresh) {
10935 return renderInsightsActivity(host, userId, fresh);
10936 }
10937 async function loadAndMountProfile(host, userId) {
10938 host.replaceChildren();
10939 const skeleton = document.createElement("div");
10940 skeleton.className = "desktop-mode-user-edit__skeleton";
10941 skeleton.style.cssText = "display:flex;align-items:center;justify-content:center;padding:48px;color:var(--desktop-mode-muted, #50575e);font-size:13px;";
10942 skeleton.textContent = __("Loading profile…");
10943 host.appendChild(skeleton);
10944 let user;
10945 try {
10946 user = await resolveUserEditClient().fetchUser(userId);
10947 } catch (err) {
10948 host.replaceChildren();
10949 const msg = document.createElement("p");
10950 msg.style.cssText = "padding:32px;color:#b32d2e;font-size:13px;text-align:center;";
10951 msg.textContent = sprintf(
10952 // translators: %s is an error message.
10953 __("Could not load profile (%s)."),
10954 String(err.message ?? err)
10955 );
10956 host.appendChild(msg);
10957 throw err;
10958 }
10959 host.replaceChildren();
10960 mountProfileForm(host, user, userId);
10961 return user;
10962 }
10963 function resolveProfileConfig() {
10964 const store = window.desktopModeWindowConfig;
10965 const userEdit = store?.["desktop-mode-user-edit"];
10966 const users = store?.["desktop-mode-users"];
10967 return {
10968 ...users ?? {},
10969 ...userEdit ?? {}
10970 };
10971 }
10972 function mountProfileForm(host, user, userId) {
10973 const cfg = resolveProfileConfig();
10974 const wrap = document.createElement("div");
10975 wrap.className = "desktop-mode-user-edit__profile";
10976 const form = document.createElement("wpd-form");
10977 form.setAttribute("submit-label", __("Save changes"));
10978 form.setAttribute("reset-label", __("Revert"));
10979 form.setAttribute("columns", "auto");
10980 const header = document.createElement("div");
10981 header.setAttribute("slot", "header");
10982 let profileHeader = buildProfileHeader(user);
10983 header.appendChild(profileHeader);
10984 form.appendChild(header);
10985 form.appendChild(textField("username", __("Username"), user.username, {
10986 readonly: true
10987 }));
10988 form.appendChild(textField("first_name", __("First name"), user.first_name));
10989 form.appendChild(textField("last_name", __("Last name"), user.last_name));
10990 form.appendChild(
10991 textField("nickname", __("Nickname"), user.nickname ?? "", {
10992 required: true,
10993 fullWidth: false
10994 })
10995 );
10996 const displaySelect = document.createElement("wpd-select");
10997 displaySelect.setAttribute("name", "name");
10998 displaySelect.setAttribute("label", __("Display name publicly as"));
10999 displaySelect.items = displayNameCandidates(user);
11000 displaySelect.value = user.name;
11001 form.appendChild(displaySelect);
11002 form.appendChild(
11003 textField("email", __("Email (required)"), user.email, {
11004 required: true,
11005 type: "email"
11006 })
11007 );
11008 form.appendChild(textField("url", __("Website"), user.url, { type: "url" }));
11009 const contactMethods = cfg.contactMethods ?? {};
11010 for (const [slug, label] of Object.entries(contactMethods)) {
11011 const value = typeof user.meta === "object" && user.meta !== null ? String(
11012 user.meta[slug] ?? ""
11013 ) : "";
11014 form.appendChild(
11015 textField(`meta.${slug}`, label, value, {
11016 dataset: { meta: slug }
11017 })
11018 );
11019 }
11020 const bio = document.createElement("wpd-textarea");
11021 bio.setAttribute("name", "description");
11022 bio.setAttribute("label", __("Biographical info"));
11023 bio.setAttribute(
11024 "placeholder",
11025 __("Share a little about yourself — visible on author archives.")
11026 );
11027 bio.setAttribute("rows", "4");
11028 bio.setAttribute("full-width", "");
11029 bio.value = user.description;
11030 bio.setAttribute("value", user.description);
11031 form.appendChild(bio);
11032 const localeSelect = document.createElement("wpd-select");
11033 localeSelect.setAttribute("name", "locale");
11034 localeSelect.setAttribute("label", __("Language"));
11035 const locales = cfg.locales ?? { "": __("Site default") };
11036 localeSelect.items = Object.entries(locales).map(([value, label]) => ({
11037 value,
11038 label
11039 }));
11040 localeSelect.value = String(user.locale ?? "");
11041 form.appendChild(localeSelect);
11042 const isSelfEdit = userId === (cfg.currentUserId ?? 0);
11043 const roleMap = (() => {
11044 const assignable = cfg.assignableRoles;
11045 if (assignable && Object.keys(assignable).length > 0) {
11046 return assignable;
11047 }
11048 return cfg.allRoles ?? {};
11049 })();
11050 if (!isSelfEdit) {
11051 const roleSelect = document.createElement("wpd-select");
11052 roleSelect.setAttribute("name", "roles[0]");
11053 roleSelect.setAttribute("label", __("Role"));
11054 roleSelect.items = Object.entries(roleMap).map(([value, label]) => ({
11055 value,
11056 label
11057 }));
11058 const currentRole = Array.isArray(user.roles) ? user.roles[0] ?? "" : "";
11059 roleSelect.value = currentRole;
11060 form.appendChild(roleSelect);
11061 }
11062 {
11063 const optsHeading = document.createElement("h3");
11064 optsHeading.setAttribute("full-width", "");
11065 optsHeading.textContent = __("Personal options");
11066 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);";
11067 form.appendChild(optsHeading);
11068 const meta = user.meta ?? {};
11069 const richEditing = String(meta.rich_editing ?? "") !== "false";
11070 const syntaxHighlighting = String(meta.syntax_highlighting ?? "") !== "false";
11071 const commentShortcuts = String(meta.comment_shortcuts ?? "false") === "true";
11072 const adminBarFront = String(meta.show_admin_bar_front ?? "true") !== "false";
11073 form.appendChild(
11074 checkboxField(
11075 "meta.rich_editing",
11076 __("Disable the visual editor when writing"),
11077 !richEditing,
11078 { trueValue: "false", falseValue: "true", fullWidth: true }
11079 )
11080 );
11081 form.appendChild(
11082 checkboxField(
11083 "meta.syntax_highlighting",
11084 __("Disable syntax highlighting when editing code"),
11085 !syntaxHighlighting,
11086 { trueValue: "false", falseValue: "true", fullWidth: true }
11087 )
11088 );
11089 form.appendChild(
11090 checkboxField(
11091 "meta.comment_shortcuts",
11092 __("Enable keyboard shortcuts for comment moderation"),
11093 commentShortcuts,
11094 { trueValue: "true", falseValue: "false", fullWidth: true }
11095 )
11096 );
11097 form.appendChild(
11098 checkboxField(
11099 "meta.show_admin_bar_front",
11100 __("Show toolbar when viewing site"),
11101 adminBarFront,
11102 { trueValue: "true", falseValue: "false", fullWidth: true }
11103 )
11104 );
11105 const colorSchemes = cfg.colorSchemes ?? {};
11106 const currentScheme = String(meta.admin_color ?? "fresh");
11107 form.appendChild(
11108 buildAdminColorPicker(colorSchemes, currentScheme, {
11109 livePreview: isSelfEdit
11110 })
11111 );
11112 }
11113 const pwdHeading = document.createElement("h3");
11114 pwdHeading.setAttribute("full-width", "");
11115 pwdHeading.textContent = __("Account management");
11116 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);";
11117 form.appendChild(pwdHeading);
11118 const pwdRow = document.createElement("div");
11119 pwdRow.setAttribute("full-width", "");
11120 pwdRow.style.cssText = "display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap;";
11121 const pwd = document.createElement("wpd-text-field");
11122 pwd.setAttribute("name", "password");
11123 pwd.setAttribute("type", "password");
11124 pwd.setAttribute("reveal", "");
11125 pwd.setAttribute("label", __("New password"));
11126 pwd.setAttribute(
11127 "placeholder",
11128 __("Leave blank to keep the current password.")
11129 );
11130 pwd.setAttribute("autocomplete", "new-password");
11131 pwd.style.flex = "1 1 280px";
11132 pwdRow.appendChild(pwd);
11133 const genBtn = document.createElement("wpd-button");
11134 genBtn.setAttribute("variant", "ghost");
11135 genBtn.setAttribute("type", "button");
11136 const genIcon = document.createElement("wpd-icon");
11137 genIcon.setAttribute("name", "randomize");
11138 genIcon.setAttribute("size", "14");
11139 genBtn.appendChild(genIcon);
11140 genBtn.appendChild(document.createTextNode(__("Generate strong")));
11141 genBtn.addEventListener("click", (e) => {
11142 e.preventDefault();
11143 const next = generateStrongPassword$1(18);
11144 pwd.value = next;
11145 pwd.setAttribute("value", next);
11146 const pwdConfirmEl = form.querySelector(
11147 'wpd-text-field[name="password_confirm"]'
11148 );
11149 if (pwdConfirmEl) {
11150 pwdConfirmEl.value = next;
11151 pwdConfirmEl.setAttribute("value", next);
11152 }
11153 void navigator.clipboard?.writeText(next).catch(() => {
11154 });
11155 notifyToast$1(__("Password generated and copied to clipboard."), "success");
11156 });
11157 pwdRow.appendChild(genBtn);
11158 form.appendChild(pwdRow);
11159 const pwdConfirm = document.createElement("wpd-text-field");
11160 pwdConfirm.setAttribute("name", "password_confirm");
11161 pwdConfirm.setAttribute("type", "password");
11162 pwdConfirm.setAttribute("reveal", "");
11163 pwdConfirm.setAttribute("label", __("Confirm new password"));
11164 pwdConfirm.setAttribute(
11165 "placeholder",
11166 __("Type the new password again.")
11167 );
11168 pwdConfirm.setAttribute("autocomplete", "new-password");
11169 pwdConfirm.setAttribute("full-width", "");
11170 form.appendChild(pwdConfirm);
11171 form.appendChild(
11172 buildSessionsRow(userId, isSelfEdit)
11173 );
11174 form.appendChild(buildAppPasswordsRow(userId));
11175 if (!isSelfEdit && cfg.isMultisite && user.meta?.is_super_admin !== void 0) {
11176 form.appendChild(
11177 checkboxField(
11178 "meta.is_super_admin",
11179 __("Grant super admin privileges for the network"),
11180 Boolean(
11181 user.meta?.is_super_admin
11182 ),
11183 { trueValue: "true", falseValue: "false", fullWidth: true }
11184 )
11185 );
11186 }
11187 let pending = false;
11188 form.addEventListener("wpd-form-submit", (e) => {
11189 const detail = e.detail;
11190 void onSubmit(detail.values);
11191 });
11192 const onSubmit = async (values) => {
11193 if (pending) {
11194 return;
11195 }
11196 pending = true;
11197 form.setBusy(true);
11198 form.clearErrors();
11199 const patch = {
11200 first_name: values.first_name,
11201 last_name: values.last_name,
11202 nickname: values.nickname,
11203 name: values.name,
11204 email: values.email,
11205 url: values.url,
11206 description: values.description,
11207 locale: values.locale ?? ""
11208 };
11209 if (typeof values.password === "string" && values.password !== "") {
11210 const confirm = String(values.password_confirm ?? "");
11211 if (confirm !== values.password) {
11212 form.setError(__("The two password fields do not match."));
11213 form.setFieldInvalid("password_confirm");
11214 pending = false;
11215 form.setBusy(false);
11216 return;
11217 }
11218 patch.password = values.password;
11219 }
11220 if (typeof values["roles[0]"] === "string" && values["roles[0]"]) {
11221 patch.roles = [values["roles[0]"]];
11222 }
11223 const meta = {};
11224 for (const [k, v] of Object.entries(values)) {
11225 if (!k.startsWith("meta.")) {
11226 continue;
11227 }
11228 let resolved = v;
11229 if (typeof v === "boolean") {
11230 const field = form.querySelector(`[name="${k}"]`);
11231 const valueAttr = field?.getAttribute("value");
11232 resolved = valueAttr ?? String(v);
11233 }
11234 meta[k.slice(5)] = resolved;
11235 }
11236 if (Object.keys(meta).length > 0) {
11237 patch.meta = meta;
11238 }
11239 const result = await resolveUserEditClient().saveUser(userId, patch);
11240 pending = false;
11241 form.setBusy(false);
11242 if (!result.ok) {
11243 const summary = result.message ?? mapErrorCode(result.error) ?? __("Save failed.");
11244 form.setError(summary);
11245 notifyToast$1(summary, "error");
11246 if (result.fieldErrors) {
11247 for (const field of Object.keys(result.fieldErrors)) {
11248 form.setFieldInvalid(field);
11249 }
11250 }
11251 console.warn("[user-edit] save failed", {
11252 code: result.error,
11253 message: result.message
11254 });
11255 return;
11256 }
11257 notifyToast$1(__("Profile saved."), "success");
11258 const broadcastApi = window.wp?.desktop;
11259 broadcastApi?.broadcast?.("desktop-mode.user.changed", {
11260 source: "user-edit-window",
11261 action: "updated",
11262 ids: [userId]
11263 });
11264 pwd.value = "";
11265 pwd.setAttribute("value", "");
11266 pwdConfirm.value = "";
11267 pwdConfirm.setAttribute("value", "");
11268 if (result.user) {
11269 Object.assign(user, result.user);
11270 const next = buildProfileHeader(user);
11271 profileHeader.replaceWith(next);
11272 profileHeader = next;
11273 const aside = host.ownerDocument?.querySelector(
11274 "[data-wpd-user-profile-aside]"
11275 );
11276 if (aside) {
11277 void mountProfileAsideAt(aside, userId, true);
11278 }
11279 }
11280 };
11281 wrap.appendChild(form);
11282 host.appendChild(wrap);
11283 }
11284 function buildProfileHeader(user) {
11285 const wrap = document.createElement("div");
11286 wrap.className = "desktop-mode-user-edit__header";
11287 wrap.style.cssText = "display:flex;align-items:center;gap:16px;margin:0 0 12px;";
11288 const avatar = document.createElement("wpd-avatar");
11289 avatar.setAttribute("size", "64");
11290 if (user.name || user.username) {
11291 avatar.setAttribute("name", user.name || user.username || "");
11292 }
11293 if (user.id > 0) {
11294 avatar.setAttribute("user-id", String(user.id));
11295 }
11296 const avatars = user.avatar_urls ?? {};
11297 const rawAvatar = avatars["96"] ?? avatars["48"] ?? "";
11298 if (rawAvatar) {
11299 applyAvatarSrc(avatar, rawAvatar);
11300 }
11301 wrap.appendChild(avatar);
11302 const text = document.createElement("div");
11303 text.style.cssText = "min-width:0;display:flex;flex-direction:column;gap:4px;";
11304 const name = document.createElement("div");
11305 name.style.cssText = "font-size:18px;font-weight:600;letter-spacing:-0.01em;";
11306 name.textContent = user.name || user.username || `#${user.id}`;
11307 text.appendChild(name);
11308 const sub = document.createElement("div");
11309 sub.style.cssText = "display:flex;align-items:center;gap:6px;font-size:12px;color:var(--desktop-mode-muted, #50575e);flex-wrap:wrap;";
11310 const handle = document.createElement("span");
11311 handle.textContent = `@${user.username}`;
11312 sub.appendChild(handle);
11313 const dot = document.createElement("span");
11314 dot.textContent = "·";
11315 dot.setAttribute("aria-hidden", "true");
11316 sub.appendChild(dot);
11317 const roleStr = Array.isArray(user.roles) ? user.roles.join(", ") : "";
11318 const roleSpan = document.createElement("span");
11319 roleSpan.textContent = roleStr || __("No role");
11320 sub.appendChild(roleSpan);
11321 text.appendChild(sub);
11322 wrap.appendChild(text);
11323 return wrap;
11324 }
11325 async function loadInsightsInto(host, userId, fresh) {
11326 host.replaceChildren();
11327 const skeleton = document.createElement("div");
11328 skeleton.style.cssText = "display:flex;align-items:center;justify-content:center;padding:32px;color:var(--desktop-mode-muted, #50575e);font-size:13px;";
11329 skeleton.textContent = __("Loading insights…");
11330 host.appendChild(skeleton);
11331 try {
11332 return await resolveUserEditClient().fetchInsights(userId, { fresh });
11333 } catch (err) {
11334 host.replaceChildren();
11335 const msg = document.createElement("p");
11336 msg.style.cssText = "padding:24px;color:#b32d2e;font-size:13px;text-align:center;";
11337 msg.textContent = sprintf(
11338 // translators: %s is an error message.
11339 __("Could not load insights (%s)."),
11340 String(err.message ?? err)
11341 );
11342 host.appendChild(msg);
11343 return null;
11344 }
11345 }
11346 async function renderInsightsAside(host, userId, fresh) {
11347 const data = await loadInsightsInto(host, userId, fresh);
11348 if (!data) {
11349 return;
11350 }
11351 host.replaceChildren();
11352 host.appendChild(buildAsideSummary(data));
11353 host.appendChild(buildAsideStatGrid(data));
11354 host.appendChild(buildContentSparkline(data));
11355 }
11356 async function renderInsightsActivity(host, userId, fresh) {
11357 const data = await loadInsightsInto(host, userId, fresh);
11358 if (!data) {
11359 return;
11360 }
11361 host.replaceChildren();
11362 const wrap = document.createElement("div");
11363 wrap.className = "desktop-mode-user-edit__activity";
11364 const heading = document.createElement("h3");
11365 heading.textContent = __("Recent activity");
11366 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);";
11367 wrap.appendChild(heading);
11368 wrap.appendChild(buildRecentLists(data));
11369 wrap.appendChild(buildSecurityPanel(data));
11370 host.appendChild(wrap);
11371 }
11372 function buildAsideSummary(data) {
11373 const card = document.createElement("div");
11374 card.style.cssText = [
11375 "display:flex",
11376 "flex-direction:column",
11377 "align-items:center",
11378 "text-align:center",
11379 "gap:6px",
11380 "padding:16px",
11381 "border:1px solid var(--desktop-mode-border, #dcdcde)",
11382 "border-radius:12px",
11383 "background:var(--wp-admin-theme-bg-elevated, #f6f7f7)"
11384 ].join(";");
11385 const avatar = document.createElement("img");
11386 avatar.src = data.avatarUrl;
11387 avatar.alt = "";
11388 avatar.style.cssText = "width:72px;height:72px;border-radius:50%;flex-shrink:0;";
11389 card.appendChild(avatar);
11390 const name = document.createElement("div");
11391 name.style.cssText = "font-size:15px;font-weight:600;letter-spacing:-0.01em;";
11392 name.textContent = data.displayName || `#${data.userId}`;
11393 card.appendChild(name);
11394 const roles = document.createElement("div");
11395 roles.style.cssText = "display:flex;flex-wrap:wrap;gap:4px;justify-content:center;";
11396 for (const role of data.roles) {
11397 const chip = document.createElement("span");
11398 chip.textContent = role;
11399 chip.style.cssText = [
11400 "display:inline-flex",
11401 "padding:2px 8px",
11402 "border-radius:10px",
11403 "background:rgba(34,113,177,0.10)",
11404 "color:#0a4b78",
11405 "font-size:11px",
11406 "font-weight:600"
11407 ].join(";");
11408 roles.appendChild(chip);
11409 }
11410 if (data.roles.length === 0) {
11411 const noRole = document.createElement("span");
11412 noRole.textContent = __("No role");
11413 noRole.style.cssText = "font-size:11px;color:var(--desktop-mode-muted, #8c8f94);";
11414 roles.appendChild(noRole);
11415 }
11416 card.appendChild(roles);
11417 const completeness = data.profileCompleteness;
11418 if (completeness && completeness.total > 0) {
11419 const cwrap = document.createElement("div");
11420 cwrap.style.cssText = "display:flex;flex-direction:column;gap:4px;width:100%;margin-top:6px;";
11421 const top = document.createElement("div");
11422 top.style.cssText = "display:flex;justify-content:space-between;align-items:baseline;font-size:11px;color:var(--desktop-mode-muted, #50575e);";
11423 const lbl = document.createElement("span");
11424 lbl.textContent = __("Profile completeness");
11425 const pct = document.createElement("span");
11426 pct.style.cssText = "font-variant-numeric:tabular-nums;font-weight:600;";
11427 pct.textContent = `${completeness.percent}%`;
11428 top.appendChild(lbl);
11429 top.appendChild(pct);
11430 cwrap.appendChild(top);
11431 const track = document.createElement("div");
11432 track.style.cssText = [
11433 "height:4px",
11434 "border-radius:999px",
11435 "background:rgba(0,0,0,0.06)",
11436 "position:relative",
11437 "overflow:hidden"
11438 ].join(";");
11439 const bar = document.createElement("div");
11440 bar.style.cssText = [
11441 "position:absolute",
11442 "inset:0",
11443 `width:${completeness.percent}%`,
11444 "background:var(--wp-admin-theme-color, #2271b1)",
11445 "transition:width 360ms ease"
11446 ].join(";");
11447 track.appendChild(bar);
11448 cwrap.appendChild(track);
11449 card.appendChild(cwrap);
11450 }
11451 return card;
11452 }
11453 function buildAsideStatGrid(data) {
11454 const grid = document.createElement("div");
11455 grid.style.cssText = [
11456 "display:grid",
11457 "grid-template-columns:1fr 1fr",
11458 "gap:8px",
11459 "margin-top:12px"
11460 ].join(";");
11461 const tile = (label, value, sub) => {
11462 const card = document.createElement("div");
11463 card.style.cssText = [
11464 "border:1px solid var(--desktop-mode-border, #dcdcde)",
11465 "border-radius:8px",
11466 "padding:8px 10px",
11467 "display:flex",
11468 "flex-direction:column",
11469 "gap:1px",
11470 "min-width:0"
11471 ].join(";");
11472 const lbl = document.createElement("div");
11473 lbl.style.cssText = "font-size:10px;text-transform:uppercase;letter-spacing:0.04em;color:var(--desktop-mode-muted, #50575e);font-weight:600;";
11474 lbl.textContent = label;
11475 const val = document.createElement("div");
11476 val.style.cssText = "font-size:18px;font-weight:600;font-variant-numeric:tabular-nums;";
11477 val.textContent = value;
11478 card.appendChild(lbl);
11479 card.appendChild(val);
11480 if (sub) {
11481 const subEl = document.createElement("div");
11482 subEl.style.cssText = "font-size:10px;color:var(--desktop-mode-muted, #8c8f94);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
11483 subEl.title = sub;
11484 subEl.textContent = sub;
11485 card.appendChild(subEl);
11486 }
11487 return card;
11488 };
11489 const stats = data.stats;
11490 grid.appendChild(
11491 tile(
11492 __("Posts"),
11493 String(stats.posts),
11494 // translators: %d is a count of pages.
11495 stats.pages > 0 ? sprintf(__("+ %d pages"), stats.pages) : void 0
11496 )
11497 );
11498 let commentsSub;
11499 if (stats.commentsReceived > 0) {
11500 commentsSub = sprintf(
11501 // translators: %d is a count of received comments.
11502 __("%d received"),
11503 stats.commentsReceived
11504 );
11505 }
11506 grid.appendChild(
11507 tile(__("Comments"), String(stats.commentsAuthored), commentsSub)
11508 );
11509 grid.appendChild(
11510 tile(
11511 __("Last login"),
11512 stats.lastLoginAt ? relativeTime$1(stats.lastLoginAt) : __("Never"),
11513 stats.lastLoginAt ? new Date(stats.lastLoginAt * 1e3).toLocaleDateString() : void 0
11514 )
11515 );
11516 let memberValue = "—";
11517 if (stats.daysSinceRegistration !== null) {
11518 memberValue = sprintf(
11519 // translators: %d is a number of days.
11520 __("%d days"),
11521 stats.daysSinceRegistration
11522 );
11523 }
11524 grid.appendChild(
11525 tile(
11526 __("Member"),
11527 memberValue,
11528 stats.registeredAt ? new Date(stats.registeredAt * 1e3).toLocaleDateString() : void 0
11529 )
11530 );
11531 return grid;
11532 }
11533 function buildContentSparkline(data) {
11534 const wrap = document.createElement("div");
11535 wrap.style.cssText = [
11536 "border:1px solid var(--desktop-mode-border, #dcdcde)",
11537 "border-radius:10px",
11538 "padding:14px 16px",
11539 "margin:0 0 22px"
11540 ].join(";");
11541 const head = document.createElement("div");
11542 head.style.cssText = "display:flex;justify-content:space-between;align-items:baseline;margin:0 0 8px;";
11543 const title = document.createElement("div");
11544 title.style.cssText = "font-size:13px;font-weight:600;";
11545 title.textContent = __("Posts published — last 12 months");
11546 head.appendChild(title);
11547 const total = data.contentByMonth.reduce((s, m) => s + m.count, 0);
11548 const sub = document.createElement("div");
11549 sub.style.cssText = "font-size:11px;color:var(--desktop-mode-muted, #50575e);";
11550 sub.textContent = sprintf(
11551 // translators: %d is a count of posts.
11552 __("%d total"),
11553 total
11554 );
11555 head.appendChild(sub);
11556 wrap.appendChild(head);
11557 if (data.contentByMonth.length === 0) {
11558 const empty = document.createElement("p");
11559 empty.style.cssText = "margin:0;color:var(--desktop-mode-muted, #50575e);font-size:12px;";
11560 empty.textContent = __("No activity in the last 12 months.");
11561 wrap.appendChild(empty);
11562 return wrap;
11563 }
11564 const max = Math.max(1, ...data.contentByMonth.map((m) => m.count));
11565 const bars = document.createElement("div");
11566 bars.style.cssText = [
11567 "display:grid",
11568 `grid-template-columns:repeat(${data.contentByMonth.length}, 1fr)`,
11569 "gap:4px",
11570 "align-items:end",
11571 "height:60px"
11572 ].join(";");
11573 for (const month of data.contentByMonth) {
11574 const col = document.createElement("div");
11575 col.style.cssText = "display:flex;flex-direction:column;align-items:center;height:100%;justify-content:flex-end;";
11576 const bar = document.createElement("div");
11577 const heightPct = Math.round(month.count / max * 100);
11578 bar.style.cssText = [
11579 "width:100%",
11580 `height:${Math.max(3, heightPct)}%`,
11581 "background:var(--wp-admin-theme-color, #2271b1)",
11582 month.count === 0 ? "opacity:0.18" : "opacity:1",
11583 "border-radius:3px 3px 0 0",
11584 "transition:height 360ms ease"
11585 ].join(";");
11586 bar.title = sprintf(
11587 // translators: %1$s is a YYYY-MM month, %2$d is post count.
11588 __("%1$s — %2$d posts"),
11589 month.month,
11590 month.count
11591 );
11592 col.appendChild(bar);
11593 wrap.appendChild(col);
11594 bars.appendChild(col);
11595 }
11596 wrap.appendChild(bars);
11597 const labels = document.createElement("div");
11598 labels.style.cssText = [
11599 "display:grid",
11600 `grid-template-columns:repeat(${data.contentByMonth.length}, 1fr)`,
11601 "gap:4px",
11602 "margin-top:4px",
11603 "font-size:10px",
11604 "color:var(--desktop-mode-muted, #8c8f94)",
11605 "text-align:center"
11606 ].join(";");
11607 for (const month of data.contentByMonth) {
11608 const span = document.createElement("span");
11609 const parts = month.month.split("-");
11610 span.textContent = parts.length === 2 ? parts[1] : month.month;
11611 labels.appendChild(span);
11612 }
11613 wrap.appendChild(labels);
11614 return wrap;
11615 }
11616 function buildRecentLists(data) {
11617 const wrap = document.createElement("div");
11618 wrap.style.cssText = "display:grid;grid-template-columns:repeat(auto-fit, minmax(280px, 1fr));gap:14px;margin:0 0 22px;";
11619 wrap.appendChild(
11620 buildRecentList(
11621 __("Recent posts"),
11622 __("No recent posts."),
11623 data.recentPosts.map((p) => ({
11624 primary: p.title,
11625 secondary: relativeFromIso(p.dateGmt),
11626 tag: p.status !== "publish" ? p.status : null,
11627 badge: p.commentCount > 0 ? sprintf(
11628 // translators: %d is a count of comments.
11629 __("%d 💬"),
11630 p.commentCount
11631 ) : null
11632 }))
11633 )
11634 );
11635 wrap.appendChild(
11636 buildRecentList(
11637 __("Recent comments"),
11638 __("No recent comments."),
11639 data.recentComments.map((c) => {
11640 const when = relativeFromIso(c.dateGmt);
11641 return {
11642 primary: c.excerpt || __("(empty comment)"),
11643 secondary: c.postTitle ? `${__("on")} "${c.postTitle}" · ${when}` : when,
11644 tag: c.approved ? null : __("pending"),
11645 badge: null
11646 };
11647 })
11648 )
11649 );
11650 return wrap;
11651 }
11652 function buildRecentList(title, emptyText, items) {
11653 const card = document.createElement("div");
11654 card.style.cssText = [
11655 "border:1px solid var(--desktop-mode-border, #dcdcde)",
11656 "border-radius:10px",
11657 "padding:14px 16px",
11658 "min-width:0"
11659 ].join(";");
11660 const head = document.createElement("div");
11661 head.style.cssText = "font-size:13px;font-weight:600;margin:0 0 10px;";
11662 head.textContent = title;
11663 card.appendChild(head);
11664 if (items.length === 0) {
11665 const empty = document.createElement("p");
11666 empty.style.cssText = "margin:0;color:var(--desktop-mode-muted, #50575e);font-size:12px;";
11667 empty.textContent = emptyText;
11668 card.appendChild(empty);
11669 return card;
11670 }
11671 const list = document.createElement("ul");
11672 list.style.cssText = "list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:8px;";
11673 for (const item of items) {
11674 const li = document.createElement("li");
11675 li.style.cssText = "min-width:0;";
11676 const top = document.createElement("div");
11677 top.style.cssText = "display:flex;align-items:baseline;gap:6px;min-width:0;";
11678 const primary = document.createElement("span");
11679 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;";
11680 primary.textContent = item.primary;
11681 primary.title = item.primary;
11682 top.appendChild(primary);
11683 if (item.tag) {
11684 const tag = document.createElement("span");
11685 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;";
11686 tag.textContent = item.tag;
11687 top.appendChild(tag);
11688 }
11689 if (item.badge) {
11690 const badge = document.createElement("span");
11691 badge.style.cssText = "font-size:11px;color:var(--desktop-mode-muted, #50575e);flex-shrink:0;";
11692 badge.textContent = item.badge;
11693 top.appendChild(badge);
11694 }
11695 li.appendChild(top);
11696 const sub = document.createElement("div");
11697 sub.style.cssText = "font-size:11px;color:var(--desktop-mode-muted, #8c8f94);";
11698 sub.textContent = item.secondary;
11699 li.appendChild(sub);
11700 list.appendChild(li);
11701 }
11702 card.appendChild(list);
11703 return card;
11704 }
11705 function buildSecurityPanel(data) {
11706 const card = document.createElement("div");
11707 card.style.cssText = [
11708 "border:1px solid var(--desktop-mode-border, #dcdcde)",
11709 "border-radius:10px",
11710 "padding:14px 16px"
11711 ].join(";");
11712 const head = document.createElement("div");
11713 head.style.cssText = "font-size:13px;font-weight:600;margin:0 0 10px;";
11714 head.textContent = __("Active sessions & app access");
11715 card.appendChild(head);
11716 const grid = document.createElement("div");
11717 grid.style.cssText = "display:grid;grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));gap:12px;";
11718 const sessionTile = document.createElement("div");
11719 sessionTile.style.cssText = "display:flex;flex-direction:column;gap:2px;font-size:12px;";
11720 const sessionLabel = document.createElement("div");
11721 sessionLabel.style.cssText = "color:var(--desktop-mode-muted, #50575e);font-size:11px;text-transform:uppercase;letter-spacing:0.04em;font-weight:600;";
11722 sessionLabel.textContent = __("Active sessions");
11723 const sessionValue = document.createElement("div");
11724 sessionValue.style.cssText = "font-size:18px;font-weight:600;";
11725 sessionValue.textContent = String(data.sessions.length);
11726 const sessionSub = document.createElement("div");
11727 sessionSub.style.cssText = "color:var(--desktop-mode-muted, #8c8f94);";
11728 const currentCount = data.sessions.filter((s) => s.current).length;
11729 sessionSub.textContent = currentCount > 0 ? __("Includes the current device.") : __("Logged in across multiple devices.");
11730 sessionTile.appendChild(sessionLabel);
11731 sessionTile.appendChild(sessionValue);
11732 sessionTile.appendChild(sessionSub);
11733 grid.appendChild(sessionTile);
11734 const appTile = document.createElement("div");
11735 appTile.style.cssText = "display:flex;flex-direction:column;gap:2px;font-size:12px;";
11736 const appLabel = document.createElement("div");
11737 appLabel.style.cssText = "color:var(--desktop-mode-muted, #50575e);font-size:11px;text-transform:uppercase;letter-spacing:0.04em;font-weight:600;";
11738 appLabel.textContent = __("Application passwords");
11739 const appValue = document.createElement("div");
11740 appValue.style.cssText = "font-size:18px;font-weight:600;";
11741 appValue.textContent = String(data.applicationPasswords.total);
11742 const appSub = document.createElement("div");
11743 appSub.style.cssText = "color:var(--desktop-mode-muted, #8c8f94);";
11744 if (data.applicationPasswords.lastUsedAt && data.applicationPasswords.lastUsedName) {
11745 appSub.textContent = sprintf(
11746 // translators: %1$s is the app password name, %2$s is a relative time.
11747 __('"%1$s" last used %2$s'),
11748 data.applicationPasswords.lastUsedName,
11749 relativeTime$1(data.applicationPasswords.lastUsedAt)
11750 );
11751 } else {
11752 appSub.textContent = data.applicationPasswords.total ? __("No recent use.") : __("No app passwords issued yet.");
11753 }
11754 appTile.appendChild(appLabel);
11755 appTile.appendChild(appValue);
11756 appTile.appendChild(appSub);
11757 grid.appendChild(appTile);
11758 card.appendChild(grid);
11759 return card;
11760 }
11761 function textField(formName, label, value, opts = {}) {
11762 const el = document.createElement("wpd-text-field");
11763 el.setAttribute("name", formName);
11764 el.setAttribute("label", label);
11765 el.setAttribute("value", value);
11766 el.value = value;
11767 if (opts.required) {
11768 el.setAttribute("required", "");
11769 }
11770 if (opts.readonly) {
11771 el.setAttribute("readonly", "");
11772 }
11773 if (opts.type) {
11774 el.setAttribute("type", opts.type);
11775 }
11776 if (opts.fullWidth !== false && opts.fullWidth) {
11777 el.setAttribute("full-width", "");
11778 }
11779 if (opts.dataset) {
11780 for (const [k, v] of Object.entries(opts.dataset)) {
11781 el.dataset[k] = v;
11782 }
11783 }
11784 return el;
11785 }
11786 function displayNameCandidates(user) {
11787 const candidates = /* @__PURE__ */ new Set();
11788 const add = (s) => {
11789 const t = s.trim();
11790 if (t !== "") {
11791 candidates.add(t);
11792 }
11793 };
11794 add(user.username);
11795 add(user.nickname ?? "");
11796 add(user.first_name);
11797 add(user.last_name);
11798 if (user.first_name || user.last_name) {
11799 add(`${user.first_name} ${user.last_name}`.trim());
11800 add(`${user.last_name} ${user.first_name}`.trim());
11801 }
11802 if (user.name) {
11803 add(user.name);
11804 }
11805 return Array.from(candidates).map((name) => ({
11806 value: name,
11807 label: name
11808 }));
11809 }
11810 function relativeFromIso(iso) {
11811 const ms = msFromIso(iso);
11812 if (!Number.isFinite(ms)) {
11813 return "—";
11814 }
11815 return relativeTime$1(Math.floor(ms / 1e3));
11816 }
11817 function relativeTime$1(ts) {
11818 if (!Number.isFinite(ts)) {
11819 return "—";
11820 }
11821 const now = Math.floor(Date.now() / 1e3);
11822 const delta = now - ts;
11823 if (delta < 60) {
11824 return __("just now");
11825 }
11826 if (delta < 3600) {
11827 return sprintf(__("%d min ago"), Math.floor(delta / 60));
11828 }
11829 if (delta < 86400) {
11830 return sprintf(__("%d h ago"), Math.floor(delta / 3600));
11831 }
11832 if (delta < 86400 * 30) {
11833 return sprintf(__("%d d ago"), Math.floor(delta / 86400));
11834 }
11835 if (delta < 86400 * 365) {
11836 return sprintf(__("%d mo ago"), Math.floor(delta / (86400 * 30)));
11837 }
11838 return sprintf(__("%d y ago"), Math.floor(delta / (86400 * 365)));
11839 }
11840 function msFromIso(iso) {
11841 if (!iso) {
11842 return NaN;
11843 }
11844 if (iso.startsWith("0000-00-00")) {
11845 return NaN;
11846 }
11847 let normalized = iso;
11848 if (normalized.includes(" ")) {
11849 normalized = normalized.replace(" ", "T");
11850 }
11851 if (!/Z$/.test(normalized) && !/[+-]\d{2}:?\d{2}$/.test(normalized)) {
11852 normalized += "Z";
11853 }
11854 const parsed = Date.parse(normalized);
11855 return Number.isFinite(parsed) ? parsed : NaN;
11856 }
11857 function generateStrongPassword$1(length) {
11858 const upper = "ABCDEFGHJKLMNPQRSTUVWXYZ";
11859 const lower = "abcdefghjkmnpqrstuvwxyz";
11860 const digits = "23456789";
11861 const symbols = "!@#$%^&*-_=+";
11862 const all = upper + lower + digits + symbols;
11863 const buf = new Uint32Array(length);
11864 crypto.getRandomValues(buf);
11865 let out = "";
11866 for (let i = 0; i < length; i += 1) {
11867 out += all[buf[i] % all.length];
11868 }
11869 return out;
11870 }
11871 function mapErrorCode(code) {
11872 switch (code) {
11873 case "rest_user_invalid_email":
11874 case "invalid_email":
11875 return __("Email address is not valid.");
11876 case "rest_user_email_exists":
11877 case "existing_user_email":
11878 return __("That email is already in use.");
11879 case "rest_user_invalid_role":
11880 return __("You are not allowed to assign that role.");
11881 default:
11882 return null;
11883 }
11884 }
11885 function applyColorSchemePreview(slug, info) {
11886 if (!info.url) {
11887 flipBodyClass(slug);
11888 flipShellScheme(slug);
11889 return;
11890 }
11891 let link = document.getElementById(
11892 "colors-css"
11893 );
11894 if (!link) {
11895 link = document.createElement("link");
11896 link.rel = "stylesheet";
11897 link.id = "colors-css";
11898 document.head.appendChild(link);
11899 }
11900 link.href = info.url;
11901 flipBodyClass(slug);
11902 flipShellScheme(slug);
11903 }
11904 function flipShellScheme(slug) {
11905 const shell = document.querySelector(".desktop-mode-shell");
11906 if (shell) {
11907 shell.setAttribute("data-desktop-mode-scheme", slug);
11908 }
11909 }
11910 function flipBodyClass(slug) {
11911 const body = document.body;
11912 const next = `admin-color-${slug}`;
11913 for (const cls of Array.from(body.classList)) {
11914 if (cls.startsWith("admin-color-") && cls !== next) {
11915 body.classList.remove(cls);
11916 }
11917 }
11918 body.classList.add(next);
11919 }
11920 function buildAdminColorPicker(schemes, current, opts = {}) {
11921 const wrap = document.createElement("div");
11922 wrap.setAttribute("full-width", "");
11923 wrap.style.cssText = "display:flex;flex-direction:column;gap:6px;";
11924 const label = document.createElement("span");
11925 label.style.cssText = "font-size:11px;text-transform:uppercase;letter-spacing:0.04em;color:var(--desktop-mode-muted, #50575e);font-weight:600;";
11926 label.textContent = __("Admin colour scheme");
11927 wrap.appendChild(label);
11928 const hidden = document.createElement("wpd-text-field");
11929 hidden.setAttribute("name", "meta.admin_color");
11930 hidden.setAttribute("value", current);
11931 hidden.value = current;
11932 hidden.style.display = "none";
11933 wrap.appendChild(hidden);
11934 const grid = document.createElement("div");
11935 grid.style.cssText = [
11936 "display:grid",
11937 "grid-template-columns:repeat(auto-fill, minmax(140px, 1fr))",
11938 "gap:8px"
11939 ].join(";");
11940 wrap.appendChild(grid);
11941 let selected = current;
11942 const updateSelected = (slug) => {
11943 selected = slug;
11944 hidden.value = slug;
11945 hidden.setAttribute("value", slug);
11946 for (const t of Array.from(grid.children)) {
11947 const tile = t;
11948 const v = tile.dataset.scheme;
11949 tile.style.borderColor = v === slug ? "var(--wp-admin-theme-color, #2271b1)" : "var(--desktop-mode-border, #dcdcde)";
11950 tile.style.boxShadow = v === slug ? "0 0 0 1px var(--wp-admin-theme-color, #2271b1) inset" : "none";
11951 tile.setAttribute("aria-checked", v === slug ? "true" : "false");
11952 }
11953 };
11954 for (const [slug, info] of Object.entries(schemes)) {
11955 const tile = document.createElement("button");
11956 tile.type = "button";
11957 tile.setAttribute("role", "radio");
11958 tile.setAttribute("aria-checked", slug === selected ? "true" : "false");
11959 tile.dataset.scheme = slug;
11960 tile.style.cssText = [
11961 "appearance:none",
11962 "border:1px solid var(--desktop-mode-border, #dcdcde)",
11963 "background:var(--wp-admin-theme-bg, #fff)",
11964 "color:inherit",
11965 "border-radius:8px",
11966 "padding:10px 10px 8px",
11967 "cursor:pointer",
11968 "display:flex",
11969 "flex-direction:column",
11970 "gap:6px",
11971 "text-align:left",
11972 "min-width:0",
11973 "transition:border-color 120ms ease, box-shadow 120ms ease"
11974 ].join(";");
11975 const swatchRow = document.createElement("span");
11976 swatchRow.style.cssText = "display:flex;height:18px;border-radius:4px;overflow:hidden;border:1px solid rgba(0,0,0,0.06);";
11977 const colors = (info.colors ?? []).slice(0, 4);
11978 if (colors.length === 0) {
11979 colors.push("#dcdcde", "#dcdcde", "#dcdcde");
11980 }
11981 for (const color of colors) {
11982 const swatch = document.createElement("span");
11983 swatch.style.cssText = `flex:1 1 auto;background:${color};`;
11984 swatchRow.appendChild(swatch);
11985 }
11986 tile.appendChild(swatchRow);
11987 const name = document.createElement("span");
11988 name.style.cssText = "font-size:12px;font-weight:500;";
11989 name.textContent = info.name;
11990 tile.appendChild(name);
11991 tile.addEventListener("click", () => {
11992 updateSelected(slug);
11993 if (opts.livePreview) {
11994 applyColorSchemePreview(slug, info);
11995 }
11996 });
11997 grid.appendChild(tile);
11998 }
11999 updateSelected(selected);
12000 return wrap;
12001 }
12002 function checkboxField(name, label, checked, opts = {}) {
12003 const trueValue = opts.trueValue ?? "true";
12004 const falseValue = opts.falseValue ?? "false";
12005 const wrap = document.createElement("span");
12006 if (opts.fullWidth) {
12007 wrap.setAttribute("full-width", "");
12008 }
12009 const cb = document.createElement("wpd-checkbox-label");
12010 cb.setAttribute("label", label);
12011 cb.setAttribute("name", name);
12012 cb.setAttribute("value", checked ? trueValue : falseValue);
12013 cb.value = checked ? trueValue : falseValue;
12014 if (checked) {
12015 cb.setAttribute("checked", "");
12016 }
12017 cb.addEventListener("wpd-checkbox-change", (e) => {
12018 const detail = e.detail;
12019 const v = detail?.checked ? trueValue : falseValue;
12020 cb.value = v;
12021 cb.setAttribute("value", v);
12022 });
12023 wrap.appendChild(cb);
12024 return wrap;
12025 }
12026 function buildSessionsRow(userId, isSelfEdit) {
12027 const wrap = document.createElement("div");
12028 wrap.setAttribute("full-width", "");
12029 wrap.style.cssText = "display:flex;align-items:center;gap:12px;flex-wrap:wrap;";
12030 const label = document.createElement("span");
12031 label.style.cssText = "font-size:13px;color:var(--desktop-mode-fg, inherit);";
12032 label.textContent = __("Active sessions");
12033 wrap.appendChild(label);
12034 const btn = document.createElement("wpd-button");
12035 btn.setAttribute("variant", "ghost");
12036 btn.setAttribute("type", "button");
12037 btn.textContent = isSelfEdit ? __("Log out everywhere else") : __("Log this user out everywhere");
12038 btn.addEventListener("click", async (e) => {
12039 e.preventDefault();
12040 try {
12041 const cfg = resolveUserEditClient().getConfig();
12042 const base = cfg.insightsUrlBase ?? joinRestUrl(cfg.restRoot, "desktop-mode/v1/users/");
12043 const res = await trackedFetch(
12044 joinRestUrl(base, `${userId}/destroy-sessions`),
12045 {
12046 method: "POST",
12047 credentials: "same-origin",
12048 headers: {
12049 "Content-Type": "application/json",
12050 "X-WP-Nonce": cfg.restNonce
12051 },
12052 body: JSON.stringify({
12053 scope: isSelfEdit ? "others" : "all"
12054 })
12055 },
12056 { source: "user-edit-window/destroy-sessions" }
12057 );
12058 if (!res.ok) {
12059 throw new Error(`http_${res.status}`);
12060 }
12061 notifyToast$1(__("Sessions destroyed."), "success");
12062 } catch (err) {
12063 notifyToast$1(
12064 sprintf(
12065 // translators: %s is an error message.
12066 __("Could not destroy sessions (%s)."),
12067 String(err.message ?? err)
12068 ),
12069 "error"
12070 );
12071 }
12072 });
12073 wrap.appendChild(btn);
12074 return wrap;
12075 }
12076 function buildAppPasswordsRow(userId) {
12077 const wrap = document.createElement("div");
12078 wrap.setAttribute("full-width", "");
12079 wrap.style.cssText = "display:flex;flex-direction:column;gap:8px;border:1px solid var(--desktop-mode-border, #dcdcde);border-radius:8px;padding:12px 14px;";
12080 const heading = document.createElement("div");
12081 heading.style.cssText = "display:flex;align-items:center;justify-content:space-between;gap:8px;";
12082 const headLabel = document.createElement("span");
12083 headLabel.textContent = __("Application passwords");
12084 headLabel.style.cssText = "font-size:13px;font-weight:600;";
12085 heading.appendChild(headLabel);
12086 wrap.appendChild(heading);
12087 const cfg = resolveUserEditClient().getConfig();
12088 const base = cfg.insightsUrlBase ?? joinRestUrl(cfg.restRoot, "desktop-mode/v1/users/");
12089 const list = document.createElement("ul");
12090 list.style.cssText = "list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px;";
12091 wrap.appendChild(list);
12092 const createRow = document.createElement("div");
12093 createRow.style.cssText = "display:flex;gap:8px;align-items:flex-end;flex-wrap:wrap;margin-top:6px;";
12094 const nameInput = document.createElement("wpd-text-field");
12095 nameInput.setAttribute("label", __("New application password name"));
12096 nameInput.setAttribute(
12097 "placeholder",
12098 __("e.g. iPhone, WP-CLI, Backup tool")
12099 );
12100 nameInput.style.flex = "1 1 220px";
12101 createRow.appendChild(nameInput);
12102 const createBtn = document.createElement("wpd-button");
12103 createBtn.setAttribute("variant", "primary");
12104 createBtn.setAttribute("type", "button");
12105 createBtn.textContent = __("Create");
12106 createRow.appendChild(createBtn);
12107 wrap.appendChild(createRow);
12108 const renderItems = (items) => {
12109 list.replaceChildren();
12110 if (items.length === 0) {
12111 const empty = document.createElement("li");
12112 empty.style.cssText = "font-size:12px;color:var(--desktop-mode-muted, #50575e);";
12113 empty.textContent = __("No application passwords issued yet.");
12114 list.appendChild(empty);
12115 return;
12116 }
12117 for (const item of items) {
12118 const row = document.createElement("li");
12119 row.style.cssText = "display:flex;align-items:center;gap:8px;font-size:12px;";
12120 const nameSpan = document.createElement("span");
12121 nameSpan.style.cssText = "flex:1 1 auto;font-weight:500;";
12122 nameSpan.textContent = item.name;
12123 row.appendChild(nameSpan);
12124 const meta = document.createElement("span");
12125 meta.style.cssText = "color:var(--desktop-mode-muted, #8c8f94);";
12126 meta.textContent = item.last_used ? sprintf(
12127 // translators: %s is a relative time.
12128 __("last used %s"),
12129 relativeTime$1(item.last_used)
12130 ) : __("never used");
12131 row.appendChild(meta);
12132 const revoke = document.createElement("wpd-button");
12133 revoke.setAttribute("variant", "ghost");
12134 revoke.setAttribute("type", "button");
12135 revoke.textContent = __("Revoke");
12136 revoke.addEventListener("click", async (e) => {
12137 e.preventDefault();
12138 try {
12139 const res = await trackedFetch(
12140 joinRestUrl(base, `${userId}/application-passwords/${item.uuid}`),
12141 {
12142 method: "DELETE",
12143 credentials: "same-origin",
12144 headers: { "X-WP-Nonce": cfg.restNonce }
12145 },
12146 { source: "user-edit-window/app-pw-revoke" }
12147 );
12148 if (!res.ok) {
12149 throw new Error(`http_${res.status}`);
12150 }
12151 row.remove();
12152 notifyToast$1(__("Application password revoked."), "success");
12153 } catch (err) {
12154 notifyToast$1(
12155 String(err.message ?? err),
12156 "error"
12157 );
12158 }
12159 });
12160 row.appendChild(revoke);
12161 list.appendChild(row);
12162 }
12163 };
12164 const refresh = async () => {
12165 try {
12166 const res = await trackedFetch(
12167 joinRestUrl(base, `${userId}/application-passwords`),
12168 {
12169 credentials: "same-origin",
12170 headers: { "X-WP-Nonce": cfg.restNonce }
12171 },
12172 { source: "user-edit-window/app-pw-list", silent: true }
12173 );
12174 if (!res.ok) {
12175 return;
12176 }
12177 const data = await res.json();
12178 renderItems(data.items ?? []);
12179 } catch {
12180 }
12181 };
12182 void refresh();
12183 createBtn.addEventListener("click", async (e) => {
12184 e.preventDefault();
12185 const name = String(nameInput.value ?? "").trim();
12186 if (!name) {
12187 notifyToast$1(__("Application password name is required."), "error");
12188 return;
12189 }
12190 try {
12191 const res = await trackedFetch(
12192 joinRestUrl(base, `${userId}/application-passwords`),
12193 {
12194 method: "POST",
12195 credentials: "same-origin",
12196 headers: {
12197 "Content-Type": "application/json",
12198 "X-WP-Nonce": cfg.restNonce
12199 },
12200 body: JSON.stringify({ name })
12201 },
12202 { source: "user-edit-window/app-pw-create" }
12203 );
12204 if (!res.ok) {
12205 throw new Error(`http_${res.status}`);
12206 }
12207 const data = await res.json();
12208 notifyToast$1(
12209 sprintf(
12210 // translators: %s is an application password.
12211 __("Created. Copy the password now: %s"),
12212 data.password
12213 ),
12214 "success"
12215 );
12216 void navigator.clipboard?.writeText(data.password).catch(() => {
12217 });
12218 nameInput.value = "";
12219 nameInput.setAttribute("value", "");
12220 void refresh();
12221 } catch (err) {
12222 notifyToast$1(
12223 String(err.message ?? err),
12224 "error"
12225 );
12226 }
12227 });
12228 return wrap;
12229 }
12230 const userEditRender = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
12231 __proto__: null,
12232 mountProfileActivityAt,
12233 mountProfileAsideAt,
12234 mountProfileFormAt
12235 }, Symbol.toStringTag, { value: "Module" }));
12236 async function showPagesIntroDialog() {
12237 return new Promise((resolve) => {
12238 const backdrop = document.createElement("div");
12239 backdrop.className = "desktop-mode-pages-intro__backdrop";
12240 backdrop.setAttribute("role", "presentation");
12241 Object.assign(backdrop.style, {
12242 position: "fixed",
12243 inset: "0",
12244 background: "color-mix(in srgb, var(--wp-admin-theme-color, #1d2327) 60%, transparent)",
12245 backdropFilter: "blur(2px)",
12246 zIndex: "100000",
12247 display: "flex",
12248 alignItems: "center",
12249 justifyContent: "center",
12250 padding: "24px"
12251 });
12252 const dialog = document.createElement("div");
12253 dialog.setAttribute("role", "dialog");
12254 dialog.setAttribute("aria-modal", "true");
12255 dialog.setAttribute("aria-labelledby", "desktop-mode-pages-intro-title");
12256 dialog.className = "desktop-mode-pages-intro";
12257 Object.assign(dialog.style, {
12258 background: "var(--wp-admin-theme-bg, #fff)",
12259 color: "var(--wp-admin-theme-fg, #1d2327)",
12260 borderRadius: "14px",
12261 boxShadow: "0 24px 60px rgba(0,0,0,.28)",
12262 maxWidth: "520px",
12263 width: "100%",
12264 maxHeight: "90vh",
12265 overflow: "auto",
12266 padding: "28px 32px 24px",
12267 fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
12268 });
12269 dialog.innerHTML = renderDialogMarkup$1();
12270 backdrop.appendChild(dialog);
12271 document.body.appendChild(backdrop);
12272 const primaryBtn = dialog.querySelector(
12273 '[data-action="confirm"]'
12274 );
12275 const settingsBtn = dialog.querySelector(
12276 '[data-action="settings"]'
12277 );
12278 primaryBtn?.focus();
12279 let resolved = false;
12280 const cleanup = (result) => {
12281 if (resolved) {
12282 return;
12283 }
12284 resolved = true;
12285 document.removeEventListener("keydown", onKey, true);
12286 backdrop.remove();
12287 resolve(result);
12288 };
12289 const onKey = (e) => {
12290 if (e.key === "Escape") {
12291 e.preventDefault();
12292 cleanup("cancel");
12293 }
12294 };
12295 document.addEventListener("keydown", onKey, true);
12296 backdrop.addEventListener("click", (e) => {
12297 if (e.target === backdrop) {
12298 cleanup("cancel");
12299 }
12300 });
12301 primaryBtn?.addEventListener("click", () => cleanup("confirm"));
12302 settingsBtn?.addEventListener("click", () => cleanup("settings"));
12303 });
12304 }
12305 function renderDialogMarkup$1() {
12306 const title = __("Welcome to the new Pages window");
12307 const lede = __(
12308 "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."
12309 );
12310 const highlights = [
12311 __("Sticky header and sticky title column so long lists stay readable as you scroll."),
12312 __('Front page and Posts page badges right on the title — no more "wait, which one is the homepage?".'),
12313 __("Page Template column so you can spot which template each page uses at a glance."),
12314 __("Slug column with one-click copy — perfect when configuring redirects or sharing canonical URLs."),
12315 __("Comments column, Parent column, View link, lock indicator, multi-select bulk actions, inline search, status segments. All in one screen, no reloads.")
12316 ];
12317 const li = (arr) => arr.map(
12318 (s) => `<li><span class="dot" aria-hidden="true"></span>${escapeHtml$1(s)}</li>`
12319 ).join("");
12320 return `
12321 <style>
12322 .desktop-mode-pages-intro h2 {
12323 margin: 0 0 8px;
12324 font-size: 22px;
12325 font-weight: 600;
12326 letter-spacing: -0.01em;
12327 }
12328 .desktop-mode-pages-intro p.lede {
12329 margin: 0 0 20px;
12330 color: var(--wp-admin-theme-fg-muted, #50575e);
12331 font-size: 14px;
12332 line-height: 1.5;
12333 }
12334 .desktop-mode-pages-intro__list {
12335 list-style: none;
12336 margin: 0 0 22px;
12337 padding: 0;
12338 font-size: 14px;
12339 line-height: 1.5;
12340 }
12341 .desktop-mode-pages-intro__list li {
12342 display: flex;
12343 align-items: flex-start;
12344 gap: 10px;
12345 padding: 6px 0;
12346 }
12347 .desktop-mode-pages-intro__list .dot {
12348 flex: 0 0 auto;
12349 width: 6px;
12350 height: 6px;
12351 margin-top: 9px;
12352 border-radius: 50%;
12353 background: var(--wp-admin-theme-color, #2271b1);
12354 }
12355 .desktop-mode-pages-intro__footer {
12356 display: flex;
12357 justify-content: flex-end;
12358 gap: 8px;
12359 margin-top: 8px;
12360 }
12361 .desktop-mode-pages-intro__footer button {
12362 appearance: none;
12363 border: 1px solid var(--wp-admin-theme-border, #dcdcde);
12364 background: var(--wp-admin-theme-bg, #fff);
12365 color: inherit;
12366 padding: 8px 14px;
12367 border-radius: 6px;
12368 font-size: 13px;
12369 cursor: pointer;
12370 }
12371 .desktop-mode-pages-intro__footer button.primary {
12372 border-color: var(--wp-admin-theme-color, #2271b1);
12373 background: var(--wp-admin-theme-color, #2271b1);
12374 color: #fff;
12375 font-weight: 500;
12376 }
12377 .desktop-mode-pages-intro__footer button:hover { filter: brightness(1.05); }
12378 .desktop-mode-pages-intro__footer button:focus-visible {
12379 outline: 2px solid var(--wp-admin-theme-color, #2271b1);
12380 outline-offset: 2px;
12381 }
12382 </style>
12383 <h2 id="desktop-mode-pages-intro-title">${escapeHtml$1(title)}</h2>
12384 <p class="lede">${escapeHtml$1(lede)}</p>
12385 <ul class="desktop-mode-pages-intro__list">${li(highlights)}</ul>
12386 <div class="desktop-mode-pages-intro__footer">
12387 <button type="button" data-action="settings">${escapeHtml$1(
12388 __("Take me to settings")
12389 )}</button>
12390 <button type="button" class="primary" data-action="confirm">${escapeHtml$1(
12391 __("Got it")
12392 )}</button>
12393 </div>
12394 `;
12395 }
12396 function escapeHtml$1(s) {
12397 const t = document.createElement("div");
12398 t.textContent = s;
12399 return t.innerHTML;
12400 }
12401 const pagesIntroDialog = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
12402 __proto__: null,
12403 showPagesIntroDialog
12404 }, Symbol.toStringTag, { value: "Module" }));
12405 const REPULSION_K = 5500;
12406 const SPRING_K = 0.05;
12407 const SPRING_LEN = 130;
12408 const MIN_RADIUS = 22;
12409 const MAX_RADIUS = 48;
12410 const POST_PER_PAGE$1 = 10;
12411 const POST_RING_RADIUS$1 = 170;
12412 async function mountCategoriesMindmap(host, client) {
12413 const api = window.wp?.desktop;
12414 if (!api || typeof api.loadModules !== "function") {
12415 host.textContent = __("Mindmap unavailable: shell modules API missing.");
12416 return () => {
12417 };
12418 }
12419 try {
12420 await api.loadModules(["pixijs"]);
12421 } catch {
12422 host.textContent = __("Mindmap unavailable.");
12423 return () => {
12424 };
12425 }
12426 const pixiMaybe = window.PIXI;
12427 if (!pixiMaybe) {
12428 host.textContent = __("Mindmap unavailable.");
12429 return () => {
12430 };
12431 }
12432 const pixi = pixiMaybe;
12433 host.replaceChildren();
12434 host.classList.add("wpd-mindmap");
12435 const toolbar = document.createElement("div");
12436 toolbar.className = "wpd-mindmap__toolbar";
12437 const addRootBtn = document.createElement("button");
12438 addRootBtn.type = "button";
12439 addRootBtn.className = "wpd-mindmap__btn wpd-mindmap__btn--primary";
12440 addRootBtn.innerHTML = '<span class="dashicons dashicons-plus" aria-hidden="true"></span>' + __("Add root category");
12441 const recenterBtn = document.createElement("button");
12442 recenterBtn.type = "button";
12443 recenterBtn.className = "wpd-mindmap__btn";
12444 recenterBtn.innerHTML = '<span class="dashicons dashicons-image-rotate" aria-hidden="true"></span>' + __("Recenter");
12445 const searchWrap = document.createElement("div");
12446 searchWrap.className = "wpd-mindmap__search";
12447 const searchInput = document.createElement("input");
12448 searchInput.type = "search";
12449 searchInput.className = "wpd-mindmap__search-input";
12450 searchInput.placeholder = __("Search categories…");
12451 searchInput.setAttribute(
12452 "aria-label",
12453 __("Search categories in the mindmap")
12454 );
12455 searchWrap.appendChild(searchInput);
12456 const searchResults = document.createElement("ul");
12457 searchResults.className = "wpd-mindmap__search-results";
12458 searchResults.hidden = true;
12459 searchWrap.appendChild(searchResults);
12460 const hint = document.createElement("span");
12461 hint.className = "wpd-mindmap__hint";
12462 hint.textContent = __(
12463 "Click a node to focus + edit · drag onto another to reparent · wheel to zoom"
12464 );
12465 toolbar.appendChild(addRootBtn);
12466 toolbar.appendChild(recenterBtn);
12467 toolbar.appendChild(searchWrap);
12468 toolbar.appendChild(hint);
12469 host.appendChild(toolbar);
12470 const layout = document.createElement("div");
12471 layout.className = "wpd-mindmap__layout";
12472 host.appendChild(layout);
12473 const stage = document.createElement("div");
12474 stage.className = "wpd-mindmap__stage";
12475 stage.classList.add("is-loading");
12476 layout.appendChild(stage);
12477 const sidebar = document.createElement("aside");
12478 sidebar.className = "wpd-mindmap__sidebar";
12479 layout.appendChild(sidebar);
12480 const app = new pixi.Application();
12481 await app.init({
12482 resizeTo: stage,
12483 backgroundAlpha: 0,
12484 antialias: true,
12485 autoDensity: true,
12486 resolution: Math.min(window.devicePixelRatio || 1, 2)
12487 });
12488 stage.appendChild(app.canvas);
12489 app.canvas.classList.add("wpd-mindmap__canvas");
12490 const world = new pixi.Container();
12491 world.x = stage.clientWidth / 2;
12492 world.y = stage.clientHeight / 2;
12493 app.stage.addChild(world);
12494 const edgeLayer = new pixi.Container();
12495 const nodeLayer = new pixi.Container();
12496 const postEdgeLayer = new pixi.Container();
12497 const postLayer = new pixi.Container();
12498 const chipLayer = new pixi.Container();
12499 const postChipLayer = new pixi.Container();
12500 world.addChild(edgeLayer);
12501 world.addChild(postEdgeLayer);
12502 world.addChild(postLayer);
12503 world.addChild(nodeLayer);
12504 world.addChild(chipLayer);
12505 world.addChild(postChipLayer);
12506 const edgeGfx = new pixi.Graphics();
12507 edgeLayer.addChild(edgeGfx);
12508 const postEdgeGfx = new pixi.Graphics();
12509 postEdgeLayer.addChild(postEdgeGfx);
12510 const CHIP_TEXT_RES2 = 4;
12511 const pager = new pixi.Container();
12512 pager.eventMode = "passive";
12513 pager.visible = false;
12514 postLayer.addChild(pager);
12515 const pagerPrev = new pixi.Graphics();
12516 const pagerNext = new pixi.Graphics();
12517 const pagerLabel = new pixi.Text({
12518 text: "1 / 1",
12519 style: {
12520 fill: 5265246,
12521 fontSize: 14,
12522 fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
12523 fontWeight: "600"
12524 },
12525 resolution: CHIP_TEXT_RES2
12526 });
12527 pagerLabel.anchor.set(0.5);
12528 pagerPrev.eventMode = "static";
12529 pagerPrev.cursor = "pointer";
12530 pagerNext.eventMode = "static";
12531 pagerNext.cursor = "pointer";
12532 pagerPrev.hitArea = new pixi.Circle(0, 0, 16);
12533 pagerNext.hitArea = new pixi.Circle(0, 0, 16);
12534 pager.addChild(pagerPrev);
12535 pager.addChild(pagerLabel);
12536 pager.addChild(pagerNext);
12537 const stopBubble = (e) => {
12538 e.stopPropagation?.();
12539 pixiInteractionAt = performance.now();
12540 };
12541 pagerPrev.on("pointerdown", stopBubble);
12542 pagerNext.on("pointerdown", stopBubble);
12543 pagerPrev.on("pointertap", (e) => {
12544 stopBubble(e);
12545 lastFocusChange = performance.now();
12546 if (focusPage <= 1) {
12547 return;
12548 }
12549 focusPage--;
12550 void loadPostsForFocus();
12551 });
12552 pagerNext.on("pointertap", (e) => {
12553 stopBubble(e);
12554 lastFocusChange = performance.now();
12555 if (focusPage >= focusTotalPages) {
12556 return;
12557 }
12558 focusPage++;
12559 void loadPostsForFocus();
12560 });
12561 const nodes = /* @__PURE__ */ new Map();
12562 const chips = /* @__PURE__ */ new Map();
12563 const postChips = /* @__PURE__ */ new Map();
12564 const postNodes = /* @__PURE__ */ new Map();
12565 let focusId = null;
12566 let focusPage = 1;
12567 let focusTotalPages = 1;
12568 let loadSeq = 0;
12569 let pixiInteractionAt = 0;
12570 let dragNode = null;
12571 let dragHover = null;
12572 let panActive = false;
12573 let panStart = null;
12574 let panMovedDist = 0;
12575 let raf = null;
12576 let lastTick = performance.now();
12577 let targetScale = world.scale.x;
12578 let targetWorldX = world.x;
12579 let targetWorldY = world.y;
12580 let nudgeAwayFrom = null;
12581 const pinnedTargetBackup = /* @__PURE__ */ new Map();
12582 let prevView = null;
12583 let draft = null;
12584 const themeHue = readAdminThemeHue$1();
12585 const clusterColor = (idx) => hslToInt$1((themeHue + idx * 47) % 360, 55, 52);
12586 let terms = [];
12587 try {
12588 const all = [];
12589 let page = 1;
12590 while (page <= 5) {
12591 const res = await client.fetchTerms("categories", { page, perPage: 100 });
12592 all.push(...res.items);
12593 if (page >= res.totalPages) {
12594 break;
12595 }
12596 page++;
12597 }
12598 terms = all;
12599 } catch (err) {
12600 showToast$1(__("Couldn’t load categories:"), err);
12601 }
12602 const showError = (title, err) => showToast$1(title, err);
12603 function isUncategorized(term) {
12604 if (term.isDefault) {
12605 return true;
12606 }
12607 return term.id === 1 || term.slug === "uncategorized" || term.name.toLowerCase() === "uncategorized";
12608 }
12609 function syncEmptyHint() {
12610 const existing = stage.querySelector(".wpd-mindmap__empty");
12611 if (terms.length <= 1) {
12612 if (!existing) {
12613 const empty = document.createElement("div");
12614 empty.className = "wpd-mindmap__empty";
12615 empty.textContent = __(
12616 'No custom categories yet. Click "Add root category" to start branching.'
12617 );
12618 stage.appendChild(empty);
12619 }
12620 } else if (existing) {
12621 existing.remove();
12622 }
12623 }
12624 function buildTree() {
12625 const childMap = /* @__PURE__ */ new Map();
12626 for (const t of terms) {
12627 const list = childMap.get(t.parent) ?? [];
12628 list.push(t);
12629 childMap.set(t.parent, list);
12630 }
12631 const allRoots = childMap.get(0) ?? [];
12632 const roots = allRoots.filter((r) => !isUncategorized(r));
12633 const uncategorized = allRoots.find(isUncategorized);
12634 const place = (term, depth, rootIdx, angle, angleSpan) => {
12635 const rootRingByCount = roots.length > 1 ? 110 + roots.length * 28 : 0;
12636 const rootRing = uncategorized ? Math.max(rootRingByCount, 140) : rootRingByCount;
12637 const baseRadius = depth === 0 ? rootRing : rootRing + 160 + (depth - 1) * 150;
12638 const tx = baseRadius * Math.cos(angle);
12639 const ty = baseRadius * Math.sin(angle);
12640 const radius = nodeRadius(term.count, terms);
12641 const color = depth === 0 ? clusterColor(rootIdx) : nodes.get(term.parent)?.color ?? clusterColor(rootIdx);
12642 let node = nodes.get(term.id);
12643 if (!node) {
12644 const gfx = new pixi.Graphics();
12645 gfx.eventMode = "static";
12646 gfx.cursor = "pointer";
12647 node = {
12648 id: term.id,
12649 parent: term.parent,
12650 name: term.name,
12651 description: term.description,
12652 count: term.count,
12653 x: tx,
12654 y: ty,
12655 tx,
12656 ty,
12657 radius,
12658 depth,
12659 color,
12660 gfx,
12661 pinned: depth === 0
12662 };
12663 nodeLayer.addChild(gfx);
12664 gfx.on("pointerdown", (e) => onNodePointerDown(e, node));
12665 nodes.set(term.id, node);
12666 } else {
12667 node.parent = term.parent;
12668 node.name = term.name;
12669 node.description = term.description;
12670 node.count = term.count;
12671 node.depth = depth;
12672 node.color = color;
12673 node.radius = radius;
12674 node.tx = tx;
12675 node.ty = ty;
12676 node.pinned = depth === 0;
12677 }
12678 drawNodeDisc(node, false);
12679 const kids = childMap.get(term.id) ?? [];
12680 if (kids.length > 0) {
12681 const sub = angleSpan / kids.length;
12682 kids.forEach((child, i) => {
12683 place(
12684 child,
12685 depth + 1,
12686 rootIdx,
12687 angle - angleSpan / 2 + sub * (i + 0.5),
12688 sub * 0.85
12689 );
12690 });
12691 }
12692 };
12693 const liveIds = new Set(terms.map((t) => t.id));
12694 for (const [id, node] of nodes) {
12695 if (!liveIds.has(id)) {
12696 nodeLayer.removeChild(node.gfx);
12697 node.gfx.destroy();
12698 nodes.delete(id);
12699 destroyChip(id);
12700 }
12701 }
12702 const rootCount = Math.max(1, roots.length);
12703 roots.forEach((root, idx) => {
12704 const angle = 2 * Math.PI / rootCount * idx;
12705 place(root, 0, idx, angle, 2 * Math.PI / rootCount);
12706 });
12707 if (uncategorized) {
12708 placeIsolated(uncategorized);
12709 }
12710 syncEmptyHint();
12711 }
12712 function placeIsolated(term) {
12713 const tx = 0;
12714 const ty = 0;
12715 const radius = nodeRadius(term.count, terms);
12716 const color = 9211796;
12717 let node = nodes.get(term.id);
12718 if (!node) {
12719 const gfx = new pixi.Graphics();
12720 gfx.eventMode = "static";
12721 gfx.cursor = "pointer";
12722 node = {
12723 id: term.id,
12724 parent: 0,
12725 name: term.name,
12726 description: term.description,
12727 count: term.count,
12728 x: tx,
12729 y: ty,
12730 tx,
12731 ty,
12732 radius,
12733 depth: 0,
12734 color,
12735 gfx,
12736 pinned: true
12737 };
12738 nodeLayer.addChild(gfx);
12739 gfx.on("pointerdown", (e) => onNodePointerDown(e, node));
12740 nodes.set(term.id, node);
12741 } else {
12742 node.parent = 0;
12743 node.name = term.name;
12744 node.description = term.description;
12745 node.count = term.count;
12746 node.depth = 0;
12747 node.color = color;
12748 node.radius = radius;
12749 node.tx = tx;
12750 node.ty = ty;
12751 node.pinned = true;
12752 }
12753 drawNodeDisc(node, false);
12754 }
12755 function drawCurvedEdge(g, x1, y1, x2, y2, color, opts = {}) {
12756 const dx = x2 - x1;
12757 const cp1x = x1 + dx * 0.5;
12758 const cp1y = y1;
12759 const cp2x = x2 - dx * 0.5;
12760 const cp2y = y2;
12761 const alpha = opts.alpha ?? 0.5;
12762 const width = opts.width ?? 1.5;
12763 if (!opts.dashed) {
12764 g.moveTo(x1, y1);
12765 g.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x2, y2);
12766 g.stroke({ color, width, alpha });
12767 return;
12768 }
12769 const sampleAt = (t) => {
12770 const omt = 1 - t;
12771 const px = omt * omt * omt * x1 + 3 * omt * omt * t * cp1x + 3 * omt * t * t * cp2x + t * t * t * x2;
12772 const py = omt * omt * omt * y1 + 3 * omt * omt * t * cp1y + 3 * omt * t * t * cp2y + t * t * t * y2;
12773 return { x: px, y: py };
12774 };
12775 const STEPS = 32;
12776 const phase = opts.dashPhase ?? 0;
12777 const stride = Math.max(1, opts.dashStride ?? 1);
12778 let lastX = x1;
12779 let lastY = y1;
12780 for (let i = 1; i <= STEPS; i++) {
12781 const p = sampleAt(i / STEPS);
12782 const groupIdx = Math.floor((i - 1 + phase) / stride);
12783 const visible = groupIdx % 2 === 0;
12784 if (visible) {
12785 g.moveTo(lastX, lastY);
12786 g.lineTo(p.x, p.y);
12787 g.stroke({ color, width, alpha });
12788 }
12789 lastX = p.x;
12790 lastY = p.y;
12791 }
12792 }
12793 function drawNodeDisc(node, highlighted) {
12794 const g = node.gfx;
12795 g.clear();
12796 const r = node.radius;
12797 if (!highlighted) {
12798 g.circle(0, 5, r);
12799 g.fill({ color: 0, alpha: 0.18 });
12800 }
12801 if (highlighted) {
12802 g.circle(0, 0, r + 10);
12803 g.fill({ color: node.color, alpha: 0.22 });
12804 }
12805 g.circle(0, 0, r);
12806 g.fill(shadeColor(node.color, -0.18));
12807 g.circle(0, -r * 0.06, r * 0.94);
12808 g.fill(node.color);
12809 g.circle(-r * 0.32, -r * 0.42, r * 0.3);
12810 g.fill({ color: 16777215, alpha: 0.32 });
12811 g.circle(0, 0, r);
12812 g.stroke({
12813 color: 16777215,
12814 width: highlighted ? 3 : 2,
12815 alignment: 0
12816 });
12817 g.x = node.x;
12818 g.y = node.y;
12819 g.zIndex = 10;
12820 g.hitArea = new pixi.Circle(0, 0, r + 4);
12821 }
12822 function drawDropTarget(hover, sourceColor) {
12823 drawNodeDisc(hover, false);
12824 const g = hover.gfx;
12825 const t = performance.now();
12826 const pulse = Math.sin(t / 280) * 0.5 + 0.5;
12827 const ringR = hover.radius + 6 + pulse * 5;
12828 g.circle(0, 0, ringR);
12829 g.stroke({
12830 color: sourceColor,
12831 width: 3,
12832 alpha: 0.6 + pulse * 0.35
12833 });
12834 g.circle(0, 0, hover.radius * 0.42);
12835 g.fill({ color: sourceColor, alpha: 0.85 });
12836 g.hitArea = new pixi.Circle(0, 0, hover.radius + 12);
12837 }
12838 function drawEdges() {
12839 edgeGfx.clear();
12840 for (const node of nodes.values()) {
12841 if (!node.parent) {
12842 continue;
12843 }
12844 const parent = nodes.get(node.parent);
12845 if (!parent) {
12846 continue;
12847 }
12848 const isOldLink = dragNode !== null && node === dragNode;
12849 const isFocusEdge = focusId !== null && (node.id === focusId || node.parent === focusId);
12850 const dimMul = focusId !== null && !isFocusEdge ? 0.35 : 1;
12851 drawCurvedEdge(
12852 edgeGfx,
12853 parent.x,
12854 parent.y,
12855 node.x,
12856 node.y,
12857 parent.color,
12858 isOldLink ? { dashed: true, alpha: 0.28 * dimMul } : { alpha: 0.5 * dimMul }
12859 );
12860 }
12861 if (dragNode && dragHover) {
12862 const x1 = dragNode.x;
12863 const y1 = dragNode.y;
12864 const x2 = dragHover.x;
12865 const y2 = dragHover.y;
12866 const targetColor = dragHover.color;
12867 drawCurvedEdge(edgeGfx, x1, y1, x2, y2, targetColor, {
12868 alpha: 0.22,
12869 width: 9
12870 });
12871 const dashPhase = Math.floor(performance.now() / 70);
12872 drawCurvedEdge(edgeGfx, x1, y1, x2, y2, targetColor, {
12873 alpha: 0.95,
12874 width: 2.5,
12875 dashed: true,
12876 dashStride: 2,
12877 dashPhase
12878 });
12879 const pt = performance.now() % 1300 / 1300;
12880 const omt = 1 - pt;
12881 const dx = x2 - x1;
12882 const cp1x = x1 + dx * 0.5;
12883 const cp1y = y1;
12884 const cp2x = x2 - dx * 0.5;
12885 const cp2y = y2;
12886 const px = omt * omt * omt * x1 + 3 * omt * omt * pt * cp1x + 3 * omt * pt * pt * cp2x + pt * pt * pt * x2;
12887 const py = omt * omt * omt * y1 + 3 * omt * omt * pt * cp1y + 3 * omt * pt * pt * cp2y + pt * pt * pt * y2;
12888 edgeGfx.circle(px, py, 5);
12889 edgeGfx.fill({ color: 16777215, alpha: 0.95 });
12890 edgeGfx.stroke({ color: targetColor, width: 2, alpha: 1 });
12891 }
12892 postEdgeGfx.clear();
12893 if (focusId !== null) {
12894 const center = nodes.get(focusId);
12895 if (center) {
12896 for (const post of postNodes.values()) {
12897 postEdgeGfx.moveTo(center.x, center.y);
12898 postEdgeGfx.lineTo(post.x, post.y);
12899 postEdgeGfx.stroke({
12900 color: center.color,
12901 width: 1,
12902 alpha: 0.35
12903 });
12904 }
12905 }
12906 }
12907 }
12908 const FONT_FAMILY2 = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
12909 const CHIP_NAME_MAX_CHARS2 = 18;
12910 const POST_TITLE_MAX_CHARS2 = 22;
12911 function truncateChipName2(name) {
12912 return name.length > CHIP_NAME_MAX_CHARS2 ? name.slice(0, CHIP_NAME_MAX_CHARS2 - 1) + "…" : name;
12913 }
12914 function ensureChip(node) {
12915 const existing = chips.get(node.id);
12916 if (existing) {
12917 return existing;
12918 }
12919 const container = new pixi.Container();
12920 container.eventMode = "static";
12921 container.cursor = "pointer";
12922 const bg = new pixi.Graphics();
12923 container.addChild(bg);
12924 const nameText = new pixi.Text({
12925 text: truncateChipName2(node.name),
12926 style: {
12927 fill: 1909543,
12928 fontSize: 14,
12929 fontFamily: FONT_FAMILY2,
12930 fontWeight: "600"
12931 },
12932 resolution: CHIP_TEXT_RES2
12933 });
12934 container.addChild(nameText);
12935 const countBg = new pixi.Graphics();
12936 container.addChild(countBg);
12937 const countText = new pixi.Text({
12938 text: String(node.count),
12939 style: {
12940 fill: 16777215,
12941 fontSize: 12,
12942 fontFamily: FONT_FAMILY2,
12943 fontWeight: "700"
12944 },
12945 resolution: CHIP_TEXT_RES2
12946 });
12947 container.addChild(countText);
12948 const chip = {
12949 container,
12950 bg,
12951 nameText,
12952 countBg,
12953 countText,
12954 width: 0,
12955 height: 0,
12956 cachedName: "",
12957 cachedCount: -1,
12958 cachedFocused: false,
12959 cachedHover: false,
12960 cachedColor: -1
12961 };
12962 chips.set(node.id, chip);
12963 chipLayer.addChild(container);
12964 container.on("pointerdown", (e) => {
12965 e.stopPropagation?.();
12966 pixiInteractionAt = performance.now();
12967 });
12968 container.on("pointertap", () => {
12969 void focusNode(node.id);
12970 });
12971 container.on("pointerover", () => {
12972 chip.cachedHover = true;
12973 layoutChip(chip, node);
12974 });
12975 container.on("pointerout", () => {
12976 chip.cachedHover = false;
12977 layoutChip(chip, node);
12978 });
12979 return chip;
12980 }
12981 function layoutChip(chip, node) {
12982 const focused = focusId === node.id;
12983 const displayName = truncateChipName2(node.name);
12984 const countStr = String(node.count);
12985 if (chip.nameText.text !== displayName) {
12986 chip.nameText.text = displayName;
12987 }
12988 if (chip.countText.text !== countStr) {
12989 chip.countText.text = countStr;
12990 }
12991 chip.cachedName = displayName;
12992 chip.cachedCount = node.count;
12993 chip.cachedFocused = focused;
12994 chip.cachedColor = node.color;
12995 const padX = 9;
12996 const padY = 3;
12997 const gap = 5;
12998 const countPadX = 5;
12999 const countPadY = 2;
13000 const minBadgeW = 18;
13001 const nameW = chip.nameText.width;
13002 const nameH = chip.nameText.height;
13003 const countW = chip.countText.width;
13004 const countH = chip.countText.height;
13005 const badgeW = Math.max(minBadgeW, countW + countPadX * 2);
13006 const badgeH = countH + countPadY * 2;
13007 const totalW = padX + nameW + gap + badgeW + padX;
13008 const totalH = Math.max(nameH, badgeH) + padY * 2;
13009 chip.width = totalW;
13010 chip.height = totalH;
13011 const left = -totalW / 2;
13012 chip.bg.clear();
13013 chip.bg.roundRect(left, 0, totalW, totalH, totalH / 2);
13014 if (focused) {
13015 chip.bg.fill(node.color);
13016 } else if (chip.cachedHover) {
13017 chip.bg.fill({ color: 16777215, alpha: 0.96 });
13018 chip.bg.stroke({
13019 color: node.color,
13020 width: 1.5,
13021 alpha: 1
13022 });
13023 } else {
13024 chip.bg.fill({ color: 16777215, alpha: 0.88 });
13025 chip.bg.stroke({
13026 color: 0,
13027 width: 1,
13028 alpha: 0.06
13029 });
13030 }
13031 chip.nameText.x = left + padX;
13032 chip.nameText.y = (totalH - nameH) / 2;
13033 chip.nameText.style.fill = focused ? 16777215 : 1909543;
13034 const badgeX = left + padX + nameW + gap;
13035 const badgeY = (totalH - badgeH) / 2;
13036 chip.countBg.clear();
13037 chip.countBg.roundRect(
13038 badgeX,
13039 badgeY,
13040 badgeW,
13041 badgeH,
13042 badgeH / 2
13043 );
13044 chip.countBg.fill(
13045 focused ? { color: 16777215, alpha: 0.25 } : node.color
13046 );
13047 chip.countText.x = badgeX + (badgeW - countW) / 2;
13048 chip.countText.y = badgeY + (badgeH - countH) / 2;
13049 }
13050 function destroyChip(id) {
13051 const chip = chips.get(id);
13052 if (!chip) {
13053 return;
13054 }
13055 chipLayer.removeChild(chip.container);
13056 chip.container.destroy({ children: true });
13057 chips.delete(id);
13058 }
13059 function syncChipPositions() {
13060 const activeIds = new Set(nodes.keys());
13061 for (const id of [...chips.keys()]) {
13062 if (!activeIds.has(id)) {
13063 destroyChip(id);
13064 }
13065 }
13066 const chipCounterScale = 1 / Math.max(0.01, world.scale.x);
13067 const anyFocus = focusId !== null;
13068 for (const node of nodes.values()) {
13069 const chip = ensureChip(node);
13070 chip.container.x = node.x;
13071 chip.container.y = node.y + node.radius + 6;
13072 chip.container.scale.set(chipCounterScale);
13073 const focused = focusId === node.id;
13074 const targetAlpha = !anyFocus || focused ? 1 : 0.4;
13075 if (Math.abs(chip.container.alpha - targetAlpha) > 5e-3) {
13076 chip.container.alpha += (targetAlpha - chip.container.alpha) * 0.18;
13077 } else {
13078 chip.container.alpha = targetAlpha;
13079 }
13080 if (Math.abs(node.gfx.alpha - targetAlpha) > 5e-3) {
13081 node.gfx.alpha += (targetAlpha - node.gfx.alpha) * 0.18;
13082 } else {
13083 node.gfx.alpha = targetAlpha;
13084 }
13085 const displayName = truncateChipName2(node.name);
13086 if (chip.cachedName !== displayName || chip.cachedCount !== node.count || chip.cachedFocused !== focused || chip.cachedColor !== node.color) {
13087 layoutChip(chip, node);
13088 }
13089 }
13090 for (const post of postNodes.values()) {
13091 const chip = postChips.get(post.id);
13092 if (!chip) {
13093 continue;
13094 }
13095 chip.container.x = post.x;
13096 chip.container.y = post.y;
13097 chip.container.scale.set(chipCounterScale);
13098 if (chip.container.alpha < 1) {
13099 chip.container.alpha = Math.min(
13100 1,
13101 chip.container.alpha + 0.18
13102 );
13103 }
13104 }
13105 }
13106 function physicsStep(dt) {
13107 const list = Array.from(nodes.values());
13108 for (const a of list) {
13109 if (a.pinned) {
13110 a.x += (a.tx - a.x) * 0.12;
13111 a.y += (a.ty - a.y) * 0.12;
13112 a.gfx.x = a.x;
13113 a.gfx.y = a.y;
13114 continue;
13115 }
13116 let fx = 0;
13117 let fy = 0;
13118 for (const b of list) {
13119 if (a === b) {
13120 continue;
13121 }
13122 const dx = a.x - b.x;
13123 const dy = a.y - b.y;
13124 const d2 = dx * dx + dy * dy + 1;
13125 const f = REPULSION_K / d2;
13126 const d = Math.sqrt(d2);
13127 fx += dx / d * f;
13128 fy += dy / d * f;
13129 }
13130 const parent = nodes.get(a.parent);
13131 if (parent) {
13132 const dx = parent.x - a.x;
13133 const dy = parent.y - a.y;
13134 const d = Math.sqrt(dx * dx + dy * dy) || 1;
13135 const stretch = d - SPRING_LEN;
13136 fx += dx / d * stretch * SPRING_K;
13137 fy += dy / d * stretch * SPRING_K;
13138 } else {
13139 fx += -a.x * 8e-4;
13140 fy += -a.y * 8e-4;
13141 }
13142 if (nudgeAwayFrom && a.id !== focusId) {
13143 const ndx = a.x - nudgeAwayFrom.x;
13144 const ndy = a.y - nudgeAwayFrom.y;
13145 const nd = Math.sqrt(ndx * ndx + ndy * ndy) || 1;
13146 const limit = nudgeAwayFrom.radius + a.radius;
13147 if (nd < limit) {
13148 const pushK = 18;
13149 fx += ndx / nd * pushK * (limit - nd);
13150 fy += ndy / nd * pushK * (limit - nd);
13151 }
13152 }
13153 if (a !== dragNode) {
13154 a.x += fx * dt * 1e-3 + (a.tx - a.x) * 0.02;
13155 a.y += fy * dt * 1e-3 + (a.ty - a.y) * 0.02;
13156 }
13157 a.gfx.x = a.x;
13158 a.gfx.y = a.y;
13159 }
13160 }
13161 function preSettlePhysics(iterations) {
13162 for (let i = 0; i < iterations; i++) {
13163 physicsStep(16);
13164 }
13165 for (const n of nodes.values()) {
13166 n.tx = n.x;
13167 n.ty = n.y;
13168 }
13169 }
13170 function tick() {
13171 const now = performance.now();
13172 const dt = Math.min(50, now - lastTick);
13173 lastTick = now;
13174 const ZOOM_EASE = 0.22;
13175 const ds = targetScale - world.scale.x;
13176 const dwx = targetWorldX - world.x;
13177 const dwy = targetWorldY - world.y;
13178 if (Math.abs(ds) > 5e-4 || Math.abs(dwx) > 0.5 || Math.abs(dwy) > 0.5) {
13179 world.scale.set(world.scale.x + ds * ZOOM_EASE);
13180 world.x += dwx * ZOOM_EASE;
13181 world.y += dwy * ZOOM_EASE;
13182 }
13183 physicsStep(dt);
13184 for (const p of postNodes.values()) {
13185 p.x += (p.tx - p.x) * 0.18;
13186 p.y += (p.ty - p.y) * 0.18;
13187 p.gfx.x = p.x;
13188 p.gfx.y = p.y;
13189 }
13190 drawEdges();
13191 if (dragNode && dragHover) {
13192 drawDropTarget(dragHover, dragNode.color);
13193 }
13194 syncChipPositions();
13195 raf = requestAnimationFrame(tick);
13196 }
13197 let dragStartPos = null;
13198 let dragOffset = { x: 0, y: 0 };
13199 function onNodePointerDown(e, node) {
13200 const ev = e;
13201 ev.stopPropagation?.();
13202 pixiInteractionAt = performance.now();
13203 dragNode = node;
13204 node.pinned = true;
13205 node.tx = node.x;
13206 node.ty = node.y;
13207 dragStartPos = { x: ev.global.x, y: ev.global.y };
13208 const local = stageToWorld({ x: ev.global.x, y: ev.global.y });
13209 dragOffset = { x: node.x - local.x, y: node.y - local.y };
13210 }
13211 function stageToWorld(global) {
13212 return {
13213 x: (global.x - world.x) / world.scale.x,
13214 y: (global.y - world.y) / world.scale.y
13215 };
13216 }
13217 function onStagePointerDown(e) {
13218 const ev = e;
13219 panActive = true;
13220 panStart = { x: ev.global.x, y: ev.global.y };
13221 panMovedDist = 0;
13222 }
13223 function onStagePointerMove(e) {
13224 const ev = e;
13225 if (dragNode) {
13226 const cursorWorld = stageToWorld(ev.global);
13227 const nx = cursorWorld.x + dragOffset.x;
13228 const ny = cursorWorld.y + dragOffset.y;
13229 dragNode.x = nx;
13230 dragNode.y = ny;
13231 dragNode.tx = nx;
13232 dragNode.ty = ny;
13233 dragNode.gfx.x = nx;
13234 dragNode.gfx.y = ny;
13235 let hover = null;
13236 for (const c of nodes.values()) {
13237 if (c === dragNode) {
13238 continue;
13239 }
13240 const dx = c.x - cursorWorld.x;
13241 const dy = c.y - cursorWorld.y;
13242 if (dx * dx + dy * dy < c.radius * c.radius) {
13243 hover = c;
13244 break;
13245 }
13246 }
13247 if (hover !== dragHover) {
13248 if (dragHover) {
13249 drawNodeDisc(dragHover, focusId === dragHover.id);
13250 }
13251 dragHover = hover;
13252 if (hover && dragNode) {
13253 drawDropTarget(hover, dragNode.color);
13254 }
13255 }
13256 return;
13257 }
13258 if (panActive && panStart) {
13259 const dx = ev.global.x - panStart.x;
13260 const dy = ev.global.y - panStart.y;
13261 world.x += dx;
13262 world.y += dy;
13263 targetWorldX += dx;
13264 targetWorldY += dy;
13265 panMovedDist += Math.sqrt(dx * dx + dy * dy);
13266 panStart = { x: ev.global.x, y: ev.global.y };
13267 }
13268 }
13269 async function onStagePointerUp(e) {
13270 if (dragNode) {
13271 const node = dragNode;
13272 const target = dragHover;
13273 const startPos = dragStartPos;
13274 dragNode = null;
13275 dragHover = null;
13276 dragStartPos = null;
13277 node.pinned = node.depth === 0;
13278 let movement = Infinity;
13279 const ev = e;
13280 if (startPos && ev && ev.global) {
13281 const dx = ev.global.x - startPos.x;
13282 const dy = ev.global.y - startPos.y;
13283 movement = Math.sqrt(dx * dx + dy * dy);
13284 }
13285 if (!target && movement < 2) {
13286 focusNode(node.id);
13287 panActive = false;
13288 panStart = null;
13289 return;
13290 }
13291 if (target && target.id !== node.parent && !isAncestor(node.id, target.id)) {
13292 try {
13293 await client.updateTerm("categories", node.id, {
13294 parent: target.id
13295 });
13296 node.parent = target.id;
13297 terms = terms.map(
13298 (t) => t.id === node.id ? { ...t, parent: target.id } : t
13299 );
13300 buildTree();
13301 } catch (err) {
13302 showError(__("Reparent failed:"), err);
13303 }
13304 } else {
13305 drawNodeDisc(node, focusId === node.id);
13306 if (target) {
13307 drawNodeDisc(target, focusId === target.id);
13308 }
13309 }
13310 }
13311 panActive = false;
13312 panStart = null;
13313 }
13314 app.stage.eventMode = "static";
13315 app.stage.hitArea = new pixi.Rectangle(
13316 0,
13317 0,
13318 stage.clientWidth,
13319 stage.clientHeight
13320 );
13321 app.stage.on("pointerdown", onStagePointerDown);
13322 app.stage.on("pointermove", onStagePointerMove);
13323 app.stage.on("pointerup", (e) => void onStagePointerUp(e));
13324 app.stage.on("pointerupoutside", (e) => void onStagePointerUp(e));
13325 function onWheel(e) {
13326 e.preventDefault();
13327 const SENSITIVITY = 8e-4;
13328 const factor = Math.exp(-e.deltaY * SENSITIVITY);
13329 const prev = targetScale;
13330 const next = Math.max(0.3, Math.min(2.5, prev * factor));
13331 if (Math.abs(next - prev) < 5e-4) {
13332 return;
13333 }
13334 const r = stage.getBoundingClientRect();
13335 const sx = e.clientX - r.left;
13336 const sy = e.clientY - r.top;
13337 const wx = (sx - targetWorldX) / prev;
13338 const wy = (sy - targetWorldY) / prev;
13339 targetScale = next;
13340 targetWorldX = sx - wx * next;
13341 targetWorldY = sy - wy * next;
13342 }
13343 stage.addEventListener("wheel", onWheel, { passive: false });
13344 let firstFitDone = false;
13345 let settledW = 0;
13346 let settledH = 0;
13347 const SETTLE_THRESHOLD_PX = 24;
13348 const SETTLE_DEBOUNCE_MS = 80;
13349 let settleTimer = null;
13350 function onResize() {
13351 const r = stage.getBoundingClientRect();
13352 app.renderer.resize(r.width, r.height);
13353 app.stage.hitArea = new pixi.Rectangle(0, 0, r.width, r.height);
13354 if (!firstFitDone && r.width > 0 && r.height > 0) {
13355 firstFitDone = true;
13356 settledW = r.width;
13357 settledH = r.height;
13358 fitToView();
13359 stage.classList.remove("is-loading");
13360 }
13361 if (settleTimer !== null) {
13362 window.clearTimeout(settleTimer);
13363 }
13364 settleTimer = window.setTimeout(() => {
13365 settleTimer = null;
13366 const cur = stage.getBoundingClientRect();
13367 const dw = Math.abs(cur.width - settledW);
13368 const dh = Math.abs(cur.height - settledH);
13369 if (dw >= SETTLE_THRESHOLD_PX || dh >= SETTLE_THRESHOLD_PX) {
13370 settledW = cur.width;
13371 settledH = cur.height;
13372 recenterCamera();
13373 }
13374 }, SETTLE_DEBOUNCE_MS);
13375 app.render();
13376 }
13377 const ro = new ResizeObserver(onResize);
13378 ro.observe(stage);
13379 function isAncestor(ancestor, descendant) {
13380 let cur = nodes.get(descendant);
13381 let safety = 32;
13382 while (cur && safety-- > 0) {
13383 if (cur.id === ancestor) {
13384 return true;
13385 }
13386 if (!cur.parent) {
13387 return false;
13388 }
13389 cur = nodes.get(cur.parent);
13390 }
13391 return false;
13392 }
13393 let lastFocusChange = 0;
13394 const SPOTLIGHT_RADIUS2 = POST_RING_RADIUS$1 + 130;
13395 async function focusNode(id) {
13396 if (focusId === id) {
13397 closeFocus();
13398 return;
13399 }
13400 const wasFocused = focusId !== null;
13401 focusId = id;
13402 focusPage = 1;
13403 lastFocusChange = performance.now();
13404 const focused = nodes.get(id);
13405 if (focused) {
13406 if (!wasFocused) {
13407 prevView = {
13408 scale: targetScale,
13409 x: targetWorldX,
13410 y: targetWorldY
13411 };
13412 }
13413 const r = stage.getBoundingClientRect();
13414 if (r.width > 0 && r.height > 0) {
13415 const half = POST_RING_RADIUS$1 + 70;
13416 const sx = r.width * 0.85 / (2 * half);
13417 const sy = r.height * 0.85 / (2 * half);
13418 const newScale = Math.max(
13419 0.5,
13420 Math.min(1.6, Math.min(sx, sy))
13421 );
13422 targetScale = newScale;
13423 targetWorldX = r.width / 2 - focused.x * newScale;
13424 targetWorldY = r.height / 2 - focused.y * newScale;
13425 }
13426 nudgeAwayFrom = {
13427 x: focused.x,
13428 y: focused.y,
13429 radius: SPOTLIGHT_RADIUS2
13430 };
13431 pinnedTargetBackup.clear();
13432 for (const n of nodes.values()) {
13433 if (n.id === id || !n.pinned) {
13434 continue;
13435 }
13436 const dx = n.x - focused.x;
13437 const dy = n.y - focused.y;
13438 const d = Math.sqrt(dx * dx + dy * dy) || 1;
13439 if (d >= SPOTLIGHT_RADIUS2 + n.radius) {
13440 continue;
13441 }
13442 pinnedTargetBackup.set(n.id, { tx: n.tx, ty: n.ty });
13443 const push = SPOTLIGHT_RADIUS2 + n.radius + 20;
13444 n.tx = focused.x + dx / d * push;
13445 n.ty = focused.y + dy / d * push;
13446 }
13447 }
13448 for (const n of nodes.values()) {
13449 drawNodeDisc(n, focusId === n.id);
13450 }
13451 paintSidebar();
13452 await loadPostsForFocus();
13453 }
13454 function closeFocus() {
13455 focusId = null;
13456 lastFocusChange = performance.now();
13457 loadSeq++;
13458 nudgeAwayFrom = null;
13459 for (const [id, t] of pinnedTargetBackup) {
13460 const n = nodes.get(id);
13461 if (n) {
13462 n.tx = t.tx;
13463 n.ty = t.ty;
13464 }
13465 }
13466 pinnedTargetBackup.clear();
13467 if (prevView) {
13468 targetScale = prevView.scale;
13469 targetWorldX = prevView.x;
13470 targetWorldY = prevView.y;
13471 prevView = null;
13472 }
13473 paintSidebar();
13474 clearPosts();
13475 for (const n of nodes.values()) {
13476 drawNodeDisc(n, false);
13477 }
13478 }
13479 function clearPosts() {
13480 for (const post of postNodes.values()) {
13481 postLayer.removeChild(post.gfx);
13482 post.gfx.destroy();
13483 }
13484 postNodes.clear();
13485 for (const chip of postChips.values()) {
13486 postChipLayer.removeChild(chip.container);
13487 chip.container.destroy({ children: true });
13488 }
13489 postChips.clear();
13490 postEdgeGfx.clear();
13491 pager.visible = false;
13492 }
13493 function ensurePostChip(post) {
13494 const existing = postChips.get(post.id);
13495 if (existing) {
13496 return existing;
13497 }
13498 const container = new pixi.Container();
13499 container.eventMode = "static";
13500 container.cursor = "pointer";
13501 container.alpha = 0;
13502 const bg = new pixi.Graphics();
13503 container.addChild(bg);
13504 const dot = new pixi.Graphics();
13505 container.addChild(dot);
13506 const titleText = new pixi.Text({
13507 text: post.title,
13508 style: {
13509 fill: 1909543,
13510 // Matches category chip fontSize so the two read at
13511 // the same weight when both are deployed. Base size
13512 // is the on-screen size since the post chip's
13513 // container counter-scales with `1/world.scale.x`
13514 // in `syncChipPositions`.
13515 fontSize: 14,
13516 fontFamily: FONT_FAMILY2,
13517 fontWeight: "500"
13518 },
13519 resolution: CHIP_TEXT_RES2
13520 });
13521 container.addChild(titleText);
13522 const chip = {
13523 container,
13524 bg,
13525 dot,
13526 titleText,
13527 width: 0,
13528 height: 0,
13529 cachedTitle: "",
13530 cachedHover: false
13531 };
13532 postChips.set(post.id, chip);
13533 postChipLayer.addChild(container);
13534 container.on("pointerdown", (e) => {
13535 e.stopPropagation?.();
13536 pixiInteractionAt = performance.now();
13537 });
13538 container.on("pointertap", () => {
13539 openInPostsTab(post.id, post.editUrl, post.title);
13540 closeFocus();
13541 });
13542 container.on("pointerover", () => {
13543 chip.cachedHover = true;
13544 layoutPostChip(chip, post);
13545 });
13546 container.on("pointerout", () => {
13547 chip.cachedHover = false;
13548 layoutPostChip(chip, post);
13549 });
13550 layoutPostChip(chip, post);
13551 return chip;
13552 }
13553 function layoutPostChip(chip, post) {
13554 const displayTitle = post.title.length > POST_TITLE_MAX_CHARS2 ? post.title.slice(0, POST_TITLE_MAX_CHARS2 - 1) + "…" : post.title;
13555 if (chip.titleText.text !== displayTitle) {
13556 chip.titleText.text = displayTitle;
13557 }
13558 chip.cachedTitle = displayTitle;
13559 const padX = 9;
13560 const padY = 3;
13561 const dotR = 4;
13562 const gap = 6;
13563 const titleW = chip.titleText.width;
13564 const titleH = chip.titleText.height;
13565 const totalW = padX + dotR * 2 + gap + titleW + padX;
13566 const totalH = Math.max(titleH, dotR * 2) + padY * 2;
13567 chip.width = totalW;
13568 chip.height = totalH;
13569 const left = -totalW / 2;
13570 const top = -totalH / 2;
13571 chip.bg.clear();
13572 chip.bg.roundRect(left, top, totalW, totalH, totalH / 2);
13573 if (chip.cachedHover) {
13574 chip.bg.fill({ color: 16777215, alpha: 1 });
13575 chip.bg.stroke({
13576 color: post.tone,
13577 width: 1.5,
13578 alpha: 1
13579 });
13580 } else {
13581 chip.bg.fill({ color: 16777215, alpha: 0.95 });
13582 chip.bg.stroke({
13583 color: 0,
13584 width: 1,
13585 alpha: 0.12
13586 });
13587 }
13588 chip.dot.clear();
13589 chip.dot.circle(left + padX + dotR, 0, dotR);
13590 chip.dot.fill({ color: post.tone, alpha: 0.85 });
13591 chip.dot.stroke({ color: 16777215, width: 1 });
13592 chip.titleText.x = left + padX + dotR * 2 + gap;
13593 chip.titleText.y = -titleH / 2;
13594 }
13595 const POSTS_CACHE_TTL_MS = 6e4;
13596 const postsCache = /* @__PURE__ */ new Map();
13597 function applyPostsResult(entry, focusedNodeId) {
13598 focusTotalPages = entry.totalPages;
13599 if (Number.isFinite(entry.realTotal)) {
13600 const node = nodes.get(focusedNodeId);
13601 if (node && node.count !== entry.realTotal) {
13602 node.count = entry.realTotal;
13603 terms = terms.map(
13604 (t) => t.id === node.id ? { ...t, count: entry.realTotal } : t
13605 );
13606 layoutChip(ensureChip(node), node);
13607 }
13608 }
13609 renderPosts(entry.items);
13610 }
13611 async function loadPostsForFocus() {
13612 if (focusId === null) {
13613 return;
13614 }
13615 const mySeq = ++loadSeq;
13616 const myFocusId = focusId;
13617 const cacheKey2 = `${focusId}:${focusPage}`;
13618 const cached = postsCache.get(cacheKey2);
13619 if (cached && performance.now() - cached.fetchedAt < POSTS_CACHE_TTL_MS) {
13620 applyPostsResult(cached, myFocusId);
13621 return;
13622 }
13623 const cfg = client.getConfig();
13624 const url = new URL(cfg.postsUrl);
13625 url.searchParams.set("categories", String(focusId));
13626 url.searchParams.set("per_page", String(POST_PER_PAGE$1));
13627 url.searchParams.set("page", String(focusPage));
13628 url.searchParams.set("status", "any");
13629 url.searchParams.set("_fields", "id,title,status");
13630 try {
13631 const response = await fetchShellJson$1(client, url.toString());
13632 if (mySeq !== loadSeq || focusId !== myFocusId) {
13633 return;
13634 }
13635 const raw = response.json ?? [];
13636 const totalPages = Math.max(
13637 1,
13638 parseInt(response.headers.get("X-WP-TotalPages") ?? "1", 10) || 1
13639 );
13640 const realTotalParsed = parseInt(response.headers.get("X-WP-Total") ?? "", 10);
13641 const realTotal = Number.isFinite(realTotalParsed) ? realTotalParsed : -1;
13642 const items = raw.map((p) => ({
13643 id: p.id,
13644 title: stripTags$1(p.title?.rendered || `#${p.id}`),
13645 editUrl: `${cfg.editPostUrlBase}?post=${p.id}&action=edit`
13646 }));
13647 const entry = {
13648 items,
13649 totalPages,
13650 realTotal,
13651 fetchedAt: performance.now()
13652 };
13653 postsCache.set(cacheKey2, entry);
13654 applyPostsResult(entry, myFocusId);
13655 } catch (err) {
13656 showError(__("Couldn’t load posts:"), err);
13657 }
13658 }
13659 function renderPosts(items) {
13660 clearPosts();
13661 if (focusId === null) {
13662 return;
13663 }
13664 const center = nodes.get(focusId);
13665 if (!center) {
13666 return;
13667 }
13668 const count = items.length;
13669 const ringR = POST_RING_RADIUS$1 + Math.max(0, count - 8) * 6;
13670 items.forEach((item, idx) => {
13671 const angle = 2 * Math.PI / Math.max(1, count) * idx - Math.PI / 2;
13672 const tx = center.x + Math.cos(angle) * ringR;
13673 const ty = center.y + Math.sin(angle) * ringR;
13674 const tone = center.color;
13675 const gfx = new pixi.Graphics();
13676 postLayer.addChild(gfx);
13677 const post = {
13678 id: item.id,
13679 title: item.title,
13680 editUrl: item.editUrl,
13681 angle,
13682 r: ringR,
13683 x: center.x,
13684 y: center.y,
13685 tx,
13686 ty,
13687 gfx,
13688 tone
13689 };
13690 postNodes.set(item.id, post);
13691 ensurePostChip(post);
13692 });
13693 repaintPager();
13694 }
13695 function repaintPager() {
13696 if (focusId === null || focusTotalPages <= 1) {
13697 pager.visible = false;
13698 return;
13699 }
13700 pager.visible = true;
13701 const center = nodes.get(focusId);
13702 if (!center) {
13703 pager.visible = false;
13704 return;
13705 }
13706 const prevDisabled = focusPage <= 1;
13707 const nextDisabled = focusPage >= focusTotalPages;
13708 drawPagerButton(pagerPrev, "◀", prevDisabled);
13709 drawPagerButton(pagerNext, "▶", nextDisabled);
13710 pagerPrev.cursor = prevDisabled ? "default" : "pointer";
13711 pagerNext.cursor = nextDisabled ? "default" : "pointer";
13712 pagerLabel.text = `${focusPage} / ${focusTotalPages}`;
13713 pagerPrev.x = -38;
13714 pagerPrev.y = 0;
13715 pagerNext.x = 38;
13716 pagerNext.y = 0;
13717 pagerLabel.x = 0;
13718 pagerLabel.y = 0;
13719 pager.x = center.x;
13720 pager.y = center.y + POST_RING_RADIUS$1 + 60;
13721 }
13722 function drawPagerButton(gfx, glyph, disabled) {
13723 gfx.clear();
13724 gfx.circle(0, 0, 14);
13725 gfx.fill({
13726 color: disabled ? 15921906 : 16777215,
13727 alpha: disabled ? 0.7 : 1
13728 });
13729 gfx.stroke({
13730 color: 0,
13731 width: 1,
13732 alpha: 0.12
13733 });
13734 const children = gfx.children;
13735 const label = children?.[0] ?? null;
13736 if (!label) {
13737 const t = new pixi.Text({
13738 text: glyph,
13739 style: {
13740 fill: disabled ? 11580344 : 5265246,
13741 fontSize: 16,
13742 fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
13743 fontWeight: "600"
13744 },
13745 resolution: CHIP_TEXT_RES2
13746 });
13747 t.anchor.set(0.5);
13748 gfx.addChild(t);
13749 } else {
13750 label.text = glyph;
13751 label.style.fill = disabled ? 11580344 : 5265246;
13752 }
13753 }
13754 function openInPostsTab(_id, editUrl, title) {
13755 const wm = api?.windowManager;
13756 const derive = api?.deriveWindowId;
13757 const postsWin = wm && typeof wm.getById === "function" ? wm.getById("desktop-mode-posts") : void 0;
13758 if (postsWin && typeof postsWin.isFullscreen === "function" && typeof postsWin.toggleFullscreen === "function" && postsWin.isFullscreen()) {
13759 postsWin.toggleFullscreen();
13760 }
13761 if (wm && typeof derive === "function") {
13762 const id = derive(editUrl);
13763 wm.open({
13764 id,
13765 baseId: id,
13766 url: editUrl,
13767 title: title ?? editUrl,
13768 icon: "dashicons-admin-post"
13769 });
13770 return;
13771 }
13772 try {
13773 window.open(editUrl, "_blank");
13774 } catch {
13775 window.location.assign(editUrl);
13776 }
13777 }
13778 function paintDraftSidebar(d) {
13779 const parentNode = d.parent !== 0 ? nodes.get(d.parent) : null;
13780 const header = document.createElement("div");
13781 header.className = "wpd-mindmap__sidebar-header";
13782 const dot = document.createElement("span");
13783 dot.className = "wpd-mindmap__sidebar-dot";
13784 const color = parentNode ? parentNode.color : clusterColor(terms.length);
13785 dot.style.background = `#${color.toString(16).padStart(6, "0")}`;
13786 const label = document.createElement("code");
13787 label.className = "wpd-mindmap__sidebar-slug";
13788 label.textContent = parentNode ? sprintf(
13789 /* translators: %s: parent category name. */
13790 __("New child of %s"),
13791 parentNode.name
13792 ) : __("New root category");
13793 header.appendChild(dot);
13794 header.appendChild(label);
13795 sidebar.appendChild(header);
13796 const nameLabel = document.createElement("label");
13797 nameLabel.className = "wpd-mindmap__sidebar-label";
13798 nameLabel.textContent = __("Name");
13799 sidebar.appendChild(nameLabel);
13800 const nameInput = document.createElement("input");
13801 nameInput.type = "text";
13802 nameInput.className = "wpd-mindmap__editor-name";
13803 nameInput.placeholder = __("e.g. Recipes");
13804 sidebar.appendChild(nameInput);
13805 requestAnimationFrame(() => nameInput.focus());
13806 const slugLabel = document.createElement("label");
13807 slugLabel.className = "wpd-mindmap__sidebar-label";
13808 slugLabel.textContent = __("Slug");
13809 sidebar.appendChild(slugLabel);
13810 const slugInput = document.createElement("input");
13811 slugInput.type = "text";
13812 slugInput.className = "wpd-mindmap__editor-name";
13813 slugInput.placeholder = __("auto-from-name");
13814 slugInput.spellcheck = false;
13815 slugInput.autocapitalize = "off";
13816 slugInput.addEventListener("input", () => {
13817 const v = slugInput.value;
13818 const norm = v.toLowerCase().replace(/[^a-z0-9-]+/g, "-");
13819 if (v !== norm) {
13820 const sel = slugInput.selectionStart ?? norm.length;
13821 slugInput.value = norm;
13822 slugInput.setSelectionRange(sel, sel);
13823 }
13824 });
13825 sidebar.appendChild(slugInput);
13826 const descLabel = document.createElement("label");
13827 descLabel.className = "wpd-mindmap__sidebar-label";
13828 descLabel.textContent = __("Description");
13829 sidebar.appendChild(descLabel);
13830 const descInput = document.createElement("textarea");
13831 descInput.className = "wpd-mindmap__editor-desc";
13832 descInput.placeholder = __("Description (optional)");
13833 descInput.rows = 4;
13834 sidebar.appendChild(descInput);
13835 const actions = document.createElement("div");
13836 actions.className = "wpd-mindmap__editor-actions";
13837 const createBtn = document.createElement("button");
13838 createBtn.type = "button";
13839 createBtn.className = "wpd-mindmap__btn wpd-mindmap__btn--primary";
13840 createBtn.textContent = __("Create");
13841 const cancelBtn = document.createElement("button");
13842 cancelBtn.type = "button";
13843 cancelBtn.className = "wpd-mindmap__btn wpd-mindmap__btn--danger";
13844 cancelBtn.textContent = __("Cancel");
13845 const handleCreate = async () => {
13846 const name = nameInput.value.trim();
13847 if (!name) {
13848 nameInput.focus();
13849 return;
13850 }
13851 createBtn.disabled = true;
13852 try {
13853 const created = await client.createCategory(name, d.parent, {
13854 slug: slugInput.value.trim() || void 0,
13855 description: descInput.value || void 0
13856 });
13857 const next = {
13858 id: created.id,
13859 name: created.name,
13860 slug: created.slug || "",
13861 parent: created.parent,
13862 count: 0,
13863 description: created.description || "",
13864 isDefault: false
13865 };
13866 if (!terms.some((t) => t.id === next.id)) {
13867 terms = terms.concat(next);
13868 }
13869 draft = null;
13870 buildTree();
13871 focusId = created.id;
13872 paintSidebar();
13873 await loadPostsForFocus();
13874 } catch (err) {
13875 createBtn.disabled = false;
13876 showError(__("Couldn’t create:"), err);
13877 }
13878 };
13879 createBtn.addEventListener("click", () => {
13880 void handleCreate();
13881 });
13882 cancelBtn.addEventListener("click", () => {
13883 draft = null;
13884 paintSidebar();
13885 });
13886 nameInput.addEventListener("keydown", (e) => {
13887 if (e.key === "Enter") {
13888 e.preventDefault();
13889 void handleCreate();
13890 } else if (e.key === "Escape") {
13891 draft = null;
13892 paintSidebar();
13893 }
13894 });
13895 actions.appendChild(createBtn);
13896 actions.appendChild(cancelBtn);
13897 sidebar.appendChild(actions);
13898 }
13899 function paintSidebar() {
13900 sidebar.replaceChildren();
13901 if (draft !== null) {
13902 paintDraftSidebar(draft);
13903 return;
13904 }
13905 if (focusId === null) {
13906 const empty = document.createElement("div");
13907 empty.className = "wpd-mindmap__sidebar-empty";
13908 const icon = document.createElement("span");
13909 icon.className = "dashicons dashicons-admin-tools";
13910 icon.setAttribute("aria-hidden", "true");
13911 empty.appendChild(icon);
13912 const title = document.createElement("h3");
13913 title.textContent = __("No category selected");
13914 empty.appendChild(title);
13915 const help = document.createElement("p");
13916 help.textContent = __(
13917 "Click a node on the mindmap to edit its name, description, and posts."
13918 );
13919 empty.appendChild(help);
13920 sidebar.appendChild(empty);
13921 return;
13922 }
13923 const node = nodes.get(focusId);
13924 if (!node) {
13925 focusId = null;
13926 paintSidebar();
13927 return;
13928 }
13929 const id = node.id;
13930 const header = document.createElement("div");
13931 header.className = "wpd-mindmap__sidebar-header";
13932 const dot = document.createElement("span");
13933 dot.className = "wpd-mindmap__sidebar-dot";
13934 dot.style.background = `#${node.color.toString(16).padStart(6, "0")}`;
13935 const term = terms.find((t) => t.id === id);
13936 const idLabel = document.createElement("code");
13937 idLabel.className = "wpd-mindmap__sidebar-slug";
13938 idLabel.textContent = `#${id}`;
13939 header.appendChild(dot);
13940 header.appendChild(idLabel);
13941 sidebar.appendChild(header);
13942 const nameLabel = document.createElement("label");
13943 nameLabel.className = "wpd-mindmap__sidebar-label";
13944 nameLabel.textContent = __("Name");
13945 sidebar.appendChild(nameLabel);
13946 const nameInput = document.createElement("input");
13947 nameInput.type = "text";
13948 nameInput.className = "wpd-mindmap__editor-name";
13949 nameInput.value = node.name;
13950 nameInput.placeholder = __("Name");
13951 sidebar.appendChild(nameInput);
13952 const slugLabel = document.createElement("label");
13953 slugLabel.className = "wpd-mindmap__sidebar-label";
13954 slugLabel.textContent = __("Slug");
13955 sidebar.appendChild(slugLabel);
13956 const slugInput = document.createElement("input");
13957 slugInput.type = "text";
13958 slugInput.className = "wpd-mindmap__editor-name";
13959 slugInput.value = term?.slug || "";
13960 slugInput.placeholder = __("auto-from-name");
13961 slugInput.spellcheck = false;
13962 slugInput.autocapitalize = "off";
13963 slugInput.addEventListener("input", () => {
13964 const v = slugInput.value;
13965 const norm = v.toLowerCase().replace(/[^a-z0-9-]+/g, "-");
13966 if (v !== norm) {
13967 const sel = slugInput.selectionStart ?? norm.length;
13968 slugInput.value = norm;
13969 slugInput.setSelectionRange(sel, sel);
13970 }
13971 });
13972 sidebar.appendChild(slugInput);
13973 const descLabel = document.createElement("label");
13974 descLabel.className = "wpd-mindmap__sidebar-label";
13975 descLabel.textContent = __("Description");
13976 sidebar.appendChild(descLabel);
13977 const descInput = document.createElement("textarea");
13978 descInput.className = "wpd-mindmap__editor-desc";
13979 descInput.value = node.description || "";
13980 descInput.placeholder = __("Description (optional)");
13981 descInput.rows = 4;
13982 sidebar.appendChild(descInput);
13983 const meta = document.createElement("p");
13984 meta.className = "wpd-mindmap__sidebar-meta";
13985 meta.textContent = sprintf(
13986 /* translators: %d: post count. */
13987 __("%d posts in this category."),
13988 node.count
13989 );
13990 sidebar.appendChild(meta);
13991 const actions = document.createElement("div");
13992 actions.className = "wpd-mindmap__editor-actions";
13993 const addChildBtn = document.createElement("button");
13994 addChildBtn.type = "button";
13995 addChildBtn.className = "wpd-mindmap__btn wpd-mindmap__btn--secondary";
13996 addChildBtn.textContent = __("+ Child");
13997 addChildBtn.addEventListener("click", () => {
13998 startDraft(id);
13999 });
14000 const makeRootBtn = node.parent && node.parent !== 0 ? document.createElement("button") : null;
14001 if (makeRootBtn) {
14002 makeRootBtn.type = "button";
14003 makeRootBtn.className = "wpd-mindmap__btn wpd-mindmap__btn--secondary";
14004 makeRootBtn.textContent = __("Make root");
14005 makeRootBtn.title = __(
14006 "Promote this category to a top-level root (no parent)."
14007 );
14008 makeRootBtn.addEventListener("click", async () => {
14009 try {
14010 await client.updateTerm("categories", node.id, { parent: 0 });
14011 node.parent = 0;
14012 terms = terms.map(
14013 (t) => t.id === node.id ? { ...t, parent: 0 } : t
14014 );
14015 buildTree();
14016 paintSidebar();
14017 } catch (err) {
14018 showError(__("Couldn’t reparent:"), err);
14019 }
14020 });
14021 }
14022 const saveBtn = document.createElement("button");
14023 saveBtn.type = "button";
14024 saveBtn.className = "wpd-mindmap__btn wpd-mindmap__btn--primary";
14025 saveBtn.textContent = __("Save");
14026 saveBtn.addEventListener("click", async () => {
14027 const name = nameInput.value.trim();
14028 if (!name) {
14029 return;
14030 }
14031 const description = descInput.value;
14032 const slugRaw = slugInput.value.trim();
14033 const currentSlug = term?.slug ?? "";
14034 if (name === node.name && description === (node.description || "") && slugRaw === currentSlug) {
14035 return;
14036 }
14037 const patch = { name, description };
14038 if (slugRaw !== currentSlug) {
14039 patch.slug = slugRaw;
14040 }
14041 try {
14042 const updated = await client.updateTerm(
14043 "categories",
14044 node.id,
14045 patch
14046 );
14047 node.name = updated.name;
14048 node.description = updated.description;
14049 terms = terms.map(
14050 (t) => t.id === node.id ? {
14051 ...t,
14052 name: updated.name,
14053 description: updated.description,
14054 slug: updated.slug ?? t.slug
14055 } : t
14056 );
14057 layoutChip(ensureChip(node), node);
14058 paintSidebar();
14059 } catch (err) {
14060 showError(__("Couldn’t save:"), err);
14061 }
14062 });
14063 const delBtn = document.createElement("button");
14064 delBtn.type = "button";
14065 delBtn.className = "wpd-mindmap__btn wpd-mindmap__btn--danger";
14066 delBtn.textContent = __("Delete");
14067 let armResetTimer = null;
14068 const armDelete = () => {
14069 delBtn.textContent = __("Click again to delete");
14070 delBtn.classList.add("is-armed");
14071 if (armResetTimer !== null) {
14072 window.clearTimeout(armResetTimer);
14073 }
14074 armResetTimer = window.setTimeout(() => {
14075 delBtn.textContent = __("Delete");
14076 delBtn.classList.remove("is-armed");
14077 armResetTimer = null;
14078 }, 2500);
14079 };
14080 delBtn.addEventListener("click", async () => {
14081 if (!delBtn.classList.contains("is-armed")) {
14082 armDelete();
14083 return;
14084 }
14085 if (armResetTimer !== null) {
14086 window.clearTimeout(armResetTimer);
14087 armResetTimer = null;
14088 }
14089 try {
14090 await client.deleteTerm("categories", node.id);
14091 terms = terms.filter((t) => t.id !== node.id);
14092 focusId = null;
14093 clearPosts();
14094 buildTree();
14095 paintSidebar();
14096 } catch (err) {
14097 showError(__("Couldn’t delete:"), err);
14098 }
14099 });
14100 actions.appendChild(addChildBtn);
14101 if (makeRootBtn) {
14102 actions.appendChild(makeRootBtn);
14103 }
14104 actions.appendChild(saveBtn);
14105 actions.appendChild(delBtn);
14106 sidebar.appendChild(actions);
14107 }
14108 function startDraft(parent) {
14109 if (parent !== 0 && !nodes.get(parent)) {
14110 return;
14111 }
14112 draft = { parent };
14113 paintSidebar();
14114 }
14115 addRootBtn.addEventListener("click", () => {
14116 startDraft(0);
14117 });
14118 function fitToView(opts = {}) {
14119 const padding = opts.padding ?? 90;
14120 const animate = opts.animate ?? false;
14121 const r = stage.getBoundingClientRect();
14122 if (nodes.size === 0 || r.width === 0 || r.height === 0) {
14123 const cx2 = r.width / 2;
14124 const cy2 = r.height / 2;
14125 targetScale = 1;
14126 targetWorldX = cx2;
14127 targetWorldY = cy2;
14128 if (!animate) {
14129 world.x = cx2;
14130 world.y = cy2;
14131 world.scale.set(1);
14132 }
14133 return;
14134 }
14135 let minX = Infinity;
14136 let minY = Infinity;
14137 let maxX = -Infinity;
14138 let maxY = -Infinity;
14139 const LABEL_OVERHANG = 30;
14140 for (const n of nodes.values()) {
14141 const rad = n.radius;
14142 minX = Math.min(minX, n.tx - rad);
14143 minY = Math.min(minY, n.ty - rad);
14144 maxX = Math.max(maxX, n.tx + rad);
14145 maxY = Math.max(maxY, n.ty + rad + LABEL_OVERHANG);
14146 }
14147 const w = Math.max(1, maxX - minX);
14148 const h = Math.max(1, maxY - minY);
14149 const sx = (r.width - padding * 2) / w;
14150 const sy = (r.height - padding * 2) / h;
14151 const scale = Math.max(0.2, Math.min(1.5, Math.min(sx, sy)));
14152 const cx = (minX + maxX) / 2;
14153 const cy = (minY + maxY) / 2;
14154 const newWorldX = r.width / 2 - cx * scale;
14155 const newWorldY = r.height / 2 - cy * scale;
14156 targetScale = scale;
14157 targetWorldX = newWorldX;
14158 targetWorldY = newWorldY;
14159 if (!animate) {
14160 world.scale.set(scale);
14161 world.x = newWorldX;
14162 world.y = newWorldY;
14163 }
14164 }
14165 function recenterCamera() {
14166 if (focusId !== null) {
14167 const focused = nodes.get(focusId);
14168 const r = stage.getBoundingClientRect();
14169 if (focused && r.width > 0 && r.height > 0) {
14170 const half = POST_RING_RADIUS$1 + 70;
14171 const sx = r.width * 0.85 / (2 * half);
14172 const sy = r.height * 0.85 / (2 * half);
14173 const newScale = Math.max(
14174 0.5,
14175 Math.min(1.6, Math.min(sx, sy))
14176 );
14177 targetScale = newScale;
14178 targetWorldX = r.width / 2 - focused.x * newScale;
14179 targetWorldY = r.height / 2 - focused.y * newScale;
14180 return;
14181 }
14182 }
14183 fitToView({ animate: true });
14184 }
14185 recenterBtn.addEventListener("click", () => recenterCamera());
14186 app.canvas.addEventListener("click", (e) => {
14187 const now = performance.now();
14188 if (now - lastFocusChange < 250 || now - pixiInteractionAt < 250) {
14189 return;
14190 }
14191 if (panMovedDist > 4) {
14192 return;
14193 }
14194 const target = e.target;
14195 if (target === app.canvas && !dragNode && focusId !== null) {
14196 closeFocus();
14197 }
14198 });
14199 async function refreshCountsViaBulk() {
14200 if (terms.length === 0) {
14201 return;
14202 }
14203 const cfg = client.getConfig();
14204 const url = new URL(
14205 joinRestUrl(cfg.restRoot, "desktop-mode/v1/term-counts")
14206 );
14207 url.searchParams.set("taxonomy", "category");
14208 url.searchParams.set(
14209 "ids",
14210 terms.map((t) => t.id).join(",")
14211 );
14212 try {
14213 const response = await fetchShellJson$1(client, url.toString());
14214 const map = response.json;
14215 let dirty = false;
14216 terms = terms.map((t) => {
14217 const fresh = map[String(t.id)];
14218 if (typeof fresh === "number" && fresh !== t.count) {
14219 dirty = true;
14220 const node = nodes.get(t.id);
14221 if (node) {
14222 node.count = fresh;
14223 layoutChip(ensureChip(node), node);
14224 }
14225 return { ...t, count: fresh };
14226 }
14227 return t;
14228 });
14229 if (dirty) {
14230 buildTree();
14231 fitToView({ animate: true });
14232 }
14233 } catch {
14234 }
14235 }
14236 buildTree();
14237 paintSidebar();
14238 preSettlePhysics(80);
14239 raf = requestAnimationFrame(tick);
14240 void refreshCountsViaBulk();
14241 let currentMatches = [];
14242 let selectedIndex = 0;
14243 const repaintHighlight = () => {
14244 const items = searchResults.querySelectorAll(
14245 ".wpd-mindmap__search-result"
14246 );
14247 items.forEach((el, i) => {
14248 const active = i === selectedIndex;
14249 el.classList.toggle("is-active", active);
14250 if (active) {
14251 el.scrollIntoView({ block: "nearest" });
14252 }
14253 });
14254 };
14255 const selectMatch = (n) => {
14256 searchInput.value = "";
14257 searchResults.hidden = true;
14258 searchResults.replaceChildren();
14259 currentMatches = [];
14260 selectedIndex = 0;
14261 void focusNode(n.id);
14262 };
14263 const renderSearchResults = () => {
14264 const q = searchInput.value.trim().toLowerCase();
14265 if (q.length === 0) {
14266 searchResults.hidden = true;
14267 searchResults.replaceChildren();
14268 currentMatches = [];
14269 selectedIndex = 0;
14270 return;
14271 }
14272 currentMatches = Array.from(nodes.values()).filter((n) => n.name.toLowerCase().includes(q)).sort((a, b) => b.count - a.count).slice(0, 10);
14273 selectedIndex = 0;
14274 searchResults.replaceChildren();
14275 currentMatches.forEach((n, i) => {
14276 const li = document.createElement("li");
14277 const btn = document.createElement("button");
14278 btn.type = "button";
14279 btn.className = "wpd-mindmap__search-result";
14280 if (i === 0) {
14281 btn.classList.add("is-active");
14282 }
14283 const nameEl = document.createElement("span");
14284 nameEl.className = "wpd-mindmap__search-title";
14285 nameEl.textContent = n.name || `#${n.id}`;
14286 const countEl = document.createElement("span");
14287 countEl.className = "wpd-mindmap__search-meta";
14288 countEl.textContent = sprintf(
14289 /* translators: %d: number of posts assigned to a category. */
14290 __("%d posts"),
14291 n.count
14292 );
14293 btn.appendChild(nameEl);
14294 btn.appendChild(countEl);
14295 btn.addEventListener("mousedown", (ev) => {
14296 ev.preventDefault();
14297 selectMatch(n);
14298 });
14299 btn.addEventListener("mouseenter", () => {
14300 selectedIndex = i;
14301 repaintHighlight();
14302 });
14303 li.appendChild(btn);
14304 searchResults.appendChild(li);
14305 });
14306 searchResults.hidden = currentMatches.length === 0;
14307 };
14308 searchInput.addEventListener("input", renderSearchResults);
14309 searchInput.addEventListener("focus", renderSearchResults);
14310 searchInput.addEventListener("keydown", (ev) => {
14311 if (ev.key === "ArrowDown") {
14312 if (currentMatches.length === 0) {
14313 return;
14314 }
14315 ev.preventDefault();
14316 selectedIndex = Math.min(
14317 selectedIndex + 1,
14318 currentMatches.length - 1
14319 );
14320 repaintHighlight();
14321 } else if (ev.key === "ArrowUp") {
14322 if (currentMatches.length === 0) {
14323 return;
14324 }
14325 ev.preventDefault();
14326 selectedIndex = Math.max(selectedIndex - 1, 0);
14327 repaintHighlight();
14328 } else if (ev.key === "Enter") {
14329 if (currentMatches.length === 0) {
14330 return;
14331 }
14332 ev.preventDefault();
14333 selectMatch(currentMatches[selectedIndex]);
14334 } else if (ev.key === "Escape") {
14335 searchInput.value = "";
14336 searchResults.hidden = true;
14337 searchResults.replaceChildren();
14338 currentMatches = [];
14339 selectedIndex = 0;
14340 }
14341 });
14342 searchInput.addEventListener("blur", () => {
14343 setTimeout(() => {
14344 searchResults.hidden = true;
14345 }, 120);
14346 });
14347 const onDocClickSearch = (ev) => {
14348 if (!searchWrap.contains(ev.target)) {
14349 searchResults.hidden = true;
14350 }
14351 };
14352 document.addEventListener("click", onDocClickSearch);
14353 return () => {
14354 if (raf !== null) {
14355 cancelAnimationFrame(raf);
14356 raf = null;
14357 }
14358 if (settleTimer !== null) {
14359 window.clearTimeout(settleTimer);
14360 settleTimer = null;
14361 }
14362 ro.disconnect();
14363 stage.removeEventListener("wheel", onWheel);
14364 document.removeEventListener("click", onDocClickSearch);
14365 try {
14366 app.ticker?.stop();
14367 } catch {
14368 }
14369 try {
14370 app.canvas?.remove();
14371 } catch {
14372 }
14373 host.replaceChildren();
14374 host.classList.remove("wpd-mindmap");
14375 };
14376 }
14377 function nodeRadius(count, all) {
14378 const max = Math.max(1, ...all.map((t) => t.count));
14379 const ratio = Math.sqrt(count / max);
14380 return MIN_RADIUS + (MAX_RADIUS - MIN_RADIUS) * ratio;
14381 }
14382 function readAdminThemeHue$1() {
14383 try {
14384 const value = getComputedStyle(document.documentElement).getPropertyValue("--wp-admin-theme-color").trim();
14385 if (!value) {
14386 return 210;
14387 }
14388 const c = document.createElement("span");
14389 c.style.color = value;
14390 document.body.appendChild(c);
14391 const rgb = getComputedStyle(c).color;
14392 c.remove();
14393 const m = rgb.match(/\d+/g);
14394 if (!m || m.length < 3) {
14395 return 210;
14396 }
14397 return rgbToHue$1(
14398 parseInt(m[0], 10),
14399 parseInt(m[1], 10),
14400 parseInt(m[2], 10)
14401 );
14402 } catch {
14403 return 210;
14404 }
14405 }
14406 function rgbToHue$1(r, g, b) {
14407 const rn = r / 255;
14408 const gn = g / 255;
14409 const bn = b / 255;
14410 const max = Math.max(rn, gn, bn);
14411 const min = Math.min(rn, gn, bn);
14412 const d = max - min;
14413 if (d === 0) {
14414 return 210;
14415 }
14416 let h;
14417 switch (max) {
14418 case rn:
14419 h = (gn - bn) / d + (gn < bn ? 6 : 0);
14420 break;
14421 case gn:
14422 h = (bn - rn) / d + 2;
14423 break;
14424 default:
14425 h = (rn - gn) / d + 4;
14426 break;
14427 }
14428 return Math.round(h * 60);
14429 }
14430 function hslToInt$1(h, s, l) {
14431 const sn = s / 100;
14432 const ln = l / 100;
14433 const c = (1 - Math.abs(2 * ln - 1)) * sn;
14434 const hp = h / 60;
14435 const x = c * (1 - Math.abs(hp % 2 - 1));
14436 let r = 0;
14437 let g = 0;
14438 let b = 0;
14439 if (hp < 1) {
14440 r = c;
14441 g = x;
14442 } else if (hp < 2) {
14443 r = x;
14444 g = c;
14445 } else if (hp < 3) {
14446 g = c;
14447 b = x;
14448 } else if (hp < 4) {
14449 g = x;
14450 b = c;
14451 } else if (hp < 5) {
14452 r = x;
14453 b = c;
14454 } else {
14455 r = c;
14456 b = x;
14457 }
14458 const m = ln - c / 2;
14459 const ri = Math.round((r + m) * 255);
14460 const gi = Math.round((g + m) * 255);
14461 const bi = Math.round((b + m) * 255);
14462 return ri * 65536 + gi * 256 + bi;
14463 }
14464 function shadeColor(color, delta) {
14465 const r = Math.floor(color / 65536) % 256;
14466 const g = Math.floor(color / 256) % 256;
14467 const b = color % 256;
14468 const adj = (ch) => {
14469 return Math.round(ch * (1 + delta));
14470 };
14471 return adj(r) * 65536 + adj(g) * 256 + adj(b);
14472 }
14473 function stripTags$1(html2) {
14474 const tmp = document.createElement("div");
14475 tmp.innerHTML = html2;
14476 return tmp.textContent || tmp.innerText || "";
14477 }
14478 function showToast$1(title, err) {
14479 const reason = err instanceof Error ? err.message : String(err);
14480 const api = window.wp?.desktop;
14481 if (api && typeof api.showToast === "function") {
14482 api.showToast({
14483 message: `${title} ${reason}`.trim(),
14484 duration: 6e3
14485 });
14486 return;
14487 }
14488 console.error(title, err);
14489 }
14490 async function fetchShellJson$1(client, url) {
14491 const cfg = client.getConfig();
14492 const init = {
14493 method: "GET",
14494 credentials: "same-origin",
14495 headers: {
14496 "X-WP-Nonce": cfg.restNonce,
14497 Accept: "application/json"
14498 }
14499 };
14500 const response = await trackedFetch(url, init, {
14501 windowId: "desktop-mode-posts"
14502 });
14503 if (!response.ok) {
14504 throw new Error(`${response.status} ${response.statusText}`);
14505 }
14506 const json = await response.json();
14507 return { json, headers: response.headers };
14508 }
14509 const categoriesMindmap = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
14510 __proto__: null,
14511 mountCategoriesMindmap
14512 }, Symbol.toStringTag, { value: "Module" }));
14513 const POST_PER_PAGE = 10;
14514 const POST_RING_RADIUS = 170;
14515 const MIN_FONT_SIZE = 11;
14516 const MAX_FONT_SIZE = 28;
14517 const FONT_FAMILY = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
14518 const CHIP_TEXT_RES = 3;
14519 const CHIP_NAME_MAX_CHARS = 22;
14520 const POST_TITLE_MAX_CHARS = 22;
14521 const CHIP_PAD_X = 11;
14522 const CHIP_PAD_Y = 6;
14523 const CHIP_GAP_HASH = 4;
14524 const CHIP_GAP_COUNT = 8;
14525 const SPIRAL_PADDING = 14;
14526 const SPOTLIGHT_RADIUS = POST_RING_RADIUS + 130;
14527 async function mountTagsCloud(host, client) {
14528 const api = window.wp?.desktop;
14529 if (!api || typeof api.loadModules !== "function") {
14530 host.textContent = __("Tag cloud unavailable: shell modules API missing.");
14531 return () => {
14532 };
14533 }
14534 try {
14535 await api.loadModules(["pixijs"]);
14536 } catch {
14537 host.textContent = __("Tag cloud unavailable.");
14538 return () => {
14539 };
14540 }
14541 const pixiMaybe = window.PIXI;
14542 if (!pixiMaybe) {
14543 host.textContent = __("Tag cloud unavailable.");
14544 return () => {
14545 };
14546 }
14547 const pixi = pixiMaybe;
14548 host.replaceChildren();
14549 host.classList.add("wpd-tagcloud");
14550 const toolbar = document.createElement("div");
14551 toolbar.className = "wpd-tagcloud__toolbar";
14552 const addTagBtn = document.createElement("button");
14553 addTagBtn.type = "button";
14554 addTagBtn.className = "wpd-tagcloud__btn wpd-tagcloud__btn--primary";
14555 addTagBtn.innerHTML = '<span class="dashicons dashicons-plus" aria-hidden="true"></span>' + __("Add tag");
14556 const recenterBtn = document.createElement("button");
14557 recenterBtn.type = "button";
14558 recenterBtn.className = "wpd-tagcloud__btn";
14559 recenterBtn.innerHTML = '<span class="dashicons dashicons-image-rotate" aria-hidden="true"></span>' + __("Recenter");
14560 const reflowBtn = document.createElement("button");
14561 reflowBtn.type = "button";
14562 reflowBtn.className = "wpd-tagcloud__btn";
14563 reflowBtn.innerHTML = '<span class="dashicons dashicons-grid-view" aria-hidden="true"></span>' + __("Reflow");
14564 reflowBtn.title = __(
14565 "Recompute the chip layout from scratch — discards manual repositioning."
14566 );
14567 const searchWrap = document.createElement("div");
14568 searchWrap.className = "wpd-tagcloud__search";
14569 const searchInput = document.createElement("input");
14570 searchInput.type = "search";
14571 searchInput.className = "wpd-tagcloud__search-input";
14572 searchInput.placeholder = __("Search tags…");
14573 searchInput.setAttribute(
14574 "aria-label",
14575 __("Search tags in the cloud")
14576 );
14577 searchWrap.appendChild(searchInput);
14578 const searchResults = document.createElement("ul");
14579 searchResults.className = "wpd-tagcloud__search-results";
14580 searchResults.hidden = true;
14581 searchWrap.appendChild(searchResults);
14582 const hint = document.createElement("span");
14583 hint.className = "wpd-tagcloud__hint";
14584 hint.textContent = __(
14585 "Click a tag to focus + edit · drag to reposition · wheel to zoom"
14586 );
14587 toolbar.appendChild(addTagBtn);
14588 toolbar.appendChild(recenterBtn);
14589 toolbar.appendChild(reflowBtn);
14590 toolbar.appendChild(searchWrap);
14591 toolbar.appendChild(hint);
14592 host.appendChild(toolbar);
14593 const layout = document.createElement("div");
14594 layout.className = "wpd-tagcloud__layout";
14595 host.appendChild(layout);
14596 const stage = document.createElement("div");
14597 stage.className = "wpd-tagcloud__stage";
14598 stage.classList.add("is-loading");
14599 layout.appendChild(stage);
14600 const sidebar = document.createElement("aside");
14601 sidebar.className = "wpd-tagcloud__sidebar";
14602 layout.appendChild(sidebar);
14603 const app = new pixi.Application();
14604 await app.init({
14605 resizeTo: stage,
14606 backgroundAlpha: 0,
14607 antialias: true,
14608 autoDensity: true,
14609 resolution: Math.min(window.devicePixelRatio || 1, 2)
14610 });
14611 stage.appendChild(app.canvas);
14612 app.canvas.classList.add("wpd-tagcloud__canvas");
14613 const world = new pixi.Container();
14614 world.x = stage.clientWidth / 2;
14615 world.y = stage.clientHeight / 2;
14616 app.stage.addChild(world);
14617 const chipLayer = new pixi.Container();
14618 const postEdgeLayer = new pixi.Container();
14619 const postLayer = new pixi.Container();
14620 const postChipLayer = new pixi.Container();
14621 world.addChild(postEdgeLayer);
14622 world.addChild(chipLayer);
14623 world.addChild(postLayer);
14624 world.addChild(postChipLayer);
14625 const postEdgeGfx = new pixi.Graphics();
14626 postEdgeLayer.addChild(postEdgeGfx);
14627 const pager = new pixi.Container();
14628 pager.eventMode = "passive";
14629 pager.visible = false;
14630 postLayer.addChild(pager);
14631 const pagerPrev = new pixi.Graphics();
14632 const pagerNext = new pixi.Graphics();
14633 const pagerLabel = new pixi.Text({
14634 text: "1 / 1",
14635 style: {
14636 fill: 5265246,
14637 fontSize: 12,
14638 fontFamily: FONT_FAMILY,
14639 fontWeight: "600"
14640 }
14641 });
14642 pagerLabel.anchor.set(0.5);
14643 pagerPrev.eventMode = "static";
14644 pagerPrev.cursor = "pointer";
14645 pagerNext.eventMode = "static";
14646 pagerNext.cursor = "pointer";
14647 pagerPrev.hitArea = new pixi.Circle(0, 0, 16);
14648 pagerNext.hitArea = new pixi.Circle(0, 0, 16);
14649 pager.addChild(pagerPrev);
14650 pager.addChild(pagerLabel);
14651 pager.addChild(pagerNext);
14652 const stopBubble = (e) => {
14653 e.stopPropagation?.();
14654 pixiInteractionAt = performance.now();
14655 };
14656 pagerPrev.on("pointerdown", stopBubble);
14657 pagerNext.on("pointerdown", stopBubble);
14658 pagerPrev.on("pointertap", (e) => {
14659 stopBubble(e);
14660 lastFocusChange = performance.now();
14661 if (focusPage <= 1) {
14662 return;
14663 }
14664 focusPage--;
14665 void loadPostsForFocus();
14666 });
14667 pagerNext.on("pointertap", (e) => {
14668 stopBubble(e);
14669 lastFocusChange = performance.now();
14670 if (focusPage >= focusTotalPages) {
14671 return;
14672 }
14673 focusPage++;
14674 void loadPostsForFocus();
14675 });
14676 const tags = /* @__PURE__ */ new Map();
14677 const postChips = /* @__PURE__ */ new Map();
14678 const postNodes = /* @__PURE__ */ new Map();
14679 let focusId = null;
14680 let focusPage = 1;
14681 let focusTotalPages = 1;
14682 let loadSeq = 0;
14683 let pixiInteractionAt = 0;
14684 let dragChip = null;
14685 let dragOffset = { x: 0, y: 0 };
14686 let dragStart = null;
14687 let panActive = false;
14688 let panStart = null;
14689 let panMovedDist = 0;
14690 let raf = null;
14691 let lastTick = performance.now();
14692 let targetScale = world.scale.x;
14693 let targetWorldX = world.x;
14694 let targetWorldY = world.y;
14695 let nudgeAwayFrom = null;
14696 let prevView = null;
14697 let lastFocusChange = 0;
14698 let draft = null;
14699 let terms = [];
14700 const positionsKey = computePositionsKey();
14701 const persistedPositions = readPersistedPositions(positionsKey);
14702 let cooccurrenceMap = /* @__PURE__ */ new Map();
14703 const themeHue = readAdminThemeHue();
14704 try {
14705 const all = [];
14706 let page = 1;
14707 while (page <= 5) {
14708 const res = await client.fetchTerms("tags", { page, perPage: 100 });
14709 all.push(...res.items);
14710 if (page >= res.totalPages) {
14711 break;
14712 }
14713 page++;
14714 }
14715 terms = all;
14716 } catch (err) {
14717 showToast(__("Couldn’t load tags:"), err);
14718 }
14719 const showError = (title, err) => showToast(title, err);
14720 function buildCloud() {
14721 const liveIds = new Set(terms.map((t) => t.id));
14722 for (const [id, box] of tags) {
14723 if (!liveIds.has(id)) {
14724 chipLayer.removeChild(box.chip.container);
14725 box.chip.container.destroy({ children: true });
14726 tags.delete(id);
14727 }
14728 }
14729 const maxCount = Math.max(1, ...terms.map((t) => t.count));
14730 const fresh = [];
14731 for (const term of terms) {
14732 const fontSize = fontSizeFor(term.count, maxCount);
14733 const hue = tagHue(term.slug || term.name, themeHue);
14734 const rotation = tagRotation(term.slug || term.name);
14735 const existing = tags.get(term.id);
14736 if (existing) {
14737 existing.name = term.name;
14738 existing.slug = term.slug;
14739 existing.description = term.description;
14740 existing.count = term.count;
14741 existing.fontSize = fontSize;
14742 existing.hue = hue;
14743 existing.rotation = rotation;
14744 layoutChip(existing);
14745 } else {
14746 const chip = createTagChip(pixi, chipLayer, term, fontSize, hue);
14747 const persisted = persistedPositions.get(term.id);
14748 const box = {
14749 id: term.id,
14750 name: term.name,
14751 slug: term.slug,
14752 description: term.description,
14753 count: term.count,
14754 fontSize,
14755 hue,
14756 rotation,
14757 x: persisted ? persisted.x : 0,
14758 y: persisted ? persisted.y : 0,
14759 tx: persisted ? persisted.x : 0,
14760 ty: persisted ? persisted.y : 0,
14761 width: 0,
14762 height: 0,
14763 chip
14764 };
14765 tags.set(term.id, box);
14766 layoutChip(box);
14767 wireChipPointer(box);
14768 if (!persisted) {
14769 fresh.push(box);
14770 }
14771 }
14772 }
14773 const placed = [];
14774 const placedById = /* @__PURE__ */ new Map();
14775 for (const box of tags.values()) {
14776 if (!fresh.includes(box)) {
14777 placed.push({
14778 x: box.tx - box.width / 2,
14779 y: box.ty - box.height / 2,
14780 w: box.width,
14781 h: box.height
14782 });
14783 placedById.set(box.id, { x: box.tx, y: box.ty });
14784 }
14785 }
14786 fresh.sort((a, b) => b.count - a.count);
14787 packBoxesWithClusters(fresh, placed, placedById, cooccurrenceMap);
14788 for (const box of fresh) {
14789 box.x = box.tx;
14790 box.y = box.ty;
14791 }
14792 }
14793 function wireChipPointer(box) {
14794 const c = box.chip.container;
14795 c.on("pointerdown", (e) => {
14796 const ev = e;
14797 ev.stopPropagation?.();
14798 pixiInteractionAt = performance.now();
14799 dragChip = box;
14800 dragStart = { x: ev.global.x, y: ev.global.y };
14801 const local = stageToWorld({ x: ev.global.x, y: ev.global.y });
14802 dragOffset = { x: box.x - local.x, y: box.y - local.y };
14803 });
14804 c.on("pointerover", () => {
14805 box.chip.cachedHover = true;
14806 paintChip(box);
14807 });
14808 c.on("pointerout", () => {
14809 box.chip.cachedHover = false;
14810 paintChip(box);
14811 });
14812 }
14813 function layoutChip(box) {
14814 const chip = box.chip;
14815 const displayName = truncateChipName(box.name);
14816 const countStr = String(box.count);
14817 if (chip.nameText.text !== displayName) {
14818 chip.nameText.text = displayName;
14819 }
14820 if (chip.countText.text !== countStr) {
14821 chip.countText.text = countStr;
14822 }
14823 chip.nameText.style.fontSize = box.fontSize;
14824 chip.hashText.style.fontSize = box.fontSize;
14825 chip.countText.style.fontSize = Math.max(
14826 10,
14827 Math.round(box.fontSize * 0.55)
14828 );
14829 chip.cachedName = displayName;
14830 chip.cachedCount = box.count;
14831 chip.cachedHue = box.hue;
14832 const hashW = chip.hashText.width;
14833 const nameW = chip.nameText.width;
14834 const nameH = chip.nameText.height;
14835 const countW = chip.countText.width;
14836 const countH = chip.countText.height;
14837 const countBadgeW = Math.max(18, countW + 10);
14838 const countBadgeH = Math.max(14, countH + 4);
14839 const totalW = CHIP_PAD_X + hashW + CHIP_GAP_HASH + nameW + CHIP_GAP_COUNT + countBadgeW + CHIP_PAD_X;
14840 const totalH = Math.max(nameH, countBadgeH) + CHIP_PAD_Y * 2;
14841 box.width = totalW;
14842 box.height = totalH;
14843 paintChip(box);
14844 }
14845 function paintChip(box) {
14846 const chip = box.chip;
14847 const focused = focusId === box.id;
14848 chip.cachedFocused = focused;
14849 const totalW = box.width;
14850 const totalH = box.height;
14851 const left = -totalW / 2;
14852 const top = -totalH / 2;
14853 const radius = totalH / 2;
14854 let fillBg;
14855 if (focused) {
14856 fillBg = hslToInt(box.hue, 70, 48);
14857 } else if (chip.cachedHover) {
14858 fillBg = hslToInt(box.hue, 70, 92);
14859 } else {
14860 fillBg = hslToInt(box.hue, 60, 95);
14861 }
14862 const borderColor = focused ? hslToInt(box.hue, 70, 38) : hslToInt(box.hue, 50, 70);
14863 const textColor = focused ? 16777215 : 1909543;
14864 const hashColor = focused ? 16777215 : hslToInt(box.hue, 65, 42);
14865 const countBg = focused ? hslToInt(box.hue, 80, 30) : hslToInt(box.hue, 70, 50);
14866 chip.shadow.clear();
14867 chip.shadow.roundRect(
14868 left - 1,
14869 top + 3,
14870 totalW + 2,
14871 totalH + 2,
14872 radius + 1
14873 );
14874 let shadowAlpha = 0.1;
14875 if (focused) {
14876 shadowAlpha = 0.18;
14877 } else if (chip.cachedHover) {
14878 shadowAlpha = 0.16;
14879 }
14880 chip.shadow.fill({
14881 color: 0,
14882 alpha: shadowAlpha
14883 });
14884 chip.bg.clear();
14885 chip.bg.roundRect(left, top, totalW, totalH, radius);
14886 chip.bg.fill(fillBg);
14887 chip.bg.stroke({
14888 color: borderColor,
14889 width: focused ? 2 : 1.25,
14890 alpha: focused ? 1 : 0.85
14891 });
14892 const hashW = chip.hashText.width;
14893 const nameW = chip.nameText.width;
14894 const nameH = chip.nameText.height;
14895 const countW = chip.countText.width;
14896 const countH = chip.countText.height;
14897 const countBadgeW = Math.max(18, countW + 10);
14898 const countBadgeH = Math.max(14, countH + 4);
14899 chip.hashText.x = left + CHIP_PAD_X;
14900 chip.hashText.y = (totalH - nameH) / 2 + top;
14901 chip.hashText.style.fill = hashColor;
14902 chip.nameText.x = left + CHIP_PAD_X + hashW + CHIP_GAP_HASH;
14903 chip.nameText.y = (totalH - nameH) / 2 + top;
14904 chip.nameText.style.fill = textColor;
14905 const badgeX = left + CHIP_PAD_X + hashW + CHIP_GAP_HASH + nameW + CHIP_GAP_COUNT;
14906 const badgeY = (totalH - countBadgeH) / 2 + top;
14907 chip.bg.roundRect(
14908 badgeX,
14909 badgeY,
14910 countBadgeW,
14911 countBadgeH,
14912 countBadgeH / 2
14913 );
14914 chip.bg.fill(countBg);
14915 chip.countText.x = badgeX + (countBadgeW - countW) / 2;
14916 chip.countText.y = badgeY + (countBadgeH - countH) / 2;
14917 chip.countText.style.fill = 16777215;
14918 }
14919 function findSpiralSlot(w, h, placed, anchorX = 0, anchorY = 0) {
14920 if (placed.length === 0) {
14921 return { x: anchorX, y: anchorY };
14922 }
14923 const padding = SPIRAL_PADDING;
14924 {
14925 const aabb = {
14926 x: anchorX - w / 2 - padding,
14927 y: anchorY - h / 2 - padding,
14928 w: w + padding * 2,
14929 h: h + padding * 2
14930 };
14931 let overlap = false;
14932 for (const p of placed) {
14933 if (aabbIntersect(aabb, p)) {
14934 overlap = true;
14935 break;
14936 }
14937 }
14938 if (!overlap) {
14939 return { x: anchorX, y: anchorY };
14940 }
14941 }
14942 let theta = 0;
14943 const maxIter = 1e4;
14944 for (let i = 0; i < maxIter; i++) {
14945 theta += 0.18;
14946 const r = theta * 5;
14947 const cx = anchorX + r * Math.cos(theta);
14948 const cy = anchorY + r * Math.sin(theta) * 0.7;
14949 const aabb = {
14950 x: cx - w / 2 - padding,
14951 y: cy - h / 2 - padding,
14952 w: w + padding * 2,
14953 h: h + padding * 2
14954 };
14955 let overlap = false;
14956 for (const p of placed) {
14957 if (aabbIntersect(aabb, p)) {
14958 overlap = true;
14959 break;
14960 }
14961 }
14962 if (!overlap) {
14963 return { x: cx, y: cy };
14964 }
14965 }
14966 return {
14967 x: anchorX,
14968 y: anchorY + (placed.length + 1) * (h + padding)
14969 };
14970 }
14971 function packBoxesWithClusters(boxesInOrder, placed, placedById, cooccurrence) {
14972 let clusterCounter = 0;
14973 const allocateClusterAnchor = () => {
14974 const idx = clusterCounter++;
14975 if (idx === 0) {
14976 return { x: 0, y: 0 };
14977 }
14978 const theta = idx * 2.4;
14979 const radius = 120 + idx * 70;
14980 return {
14981 x: radius * Math.cos(theta),
14982 y: radius * Math.sin(theta) * 0.8
14983 };
14984 };
14985 for (const box of boxesInOrder) {
14986 let anchorX = 0;
14987 let anchorY = 0;
14988 let usedCentroid = false;
14989 const neighbors = cooccurrence.get(box.id);
14990 if (neighbors && neighbors.length > 0) {
14991 let sumX = 0;
14992 let sumY = 0;
14993 let sumW = 0;
14994 for (const n of neighbors) {
14995 const pos = placedById.get(n.id);
14996 if (!pos) {
14997 continue;
14998 }
14999 sumX += pos.x * n.shared;
15000 sumY += pos.y * n.shared;
15001 sumW += n.shared;
15002 }
15003 if (sumW > 0) {
15004 anchorX = sumX / sumW;
15005 anchorY = sumY / sumW;
15006 usedCentroid = true;
15007 }
15008 }
15009 if (!usedCentroid) {
15010 const anchor = allocateClusterAnchor();
15011 anchorX = anchor.x;
15012 anchorY = anchor.y;
15013 }
15014 const slot = findSpiralSlot(
15015 box.width,
15016 box.height,
15017 placed,
15018 anchorX,
15019 anchorY
15020 );
15021 box.tx = slot.x;
15022 box.ty = slot.y;
15023 placedById.set(box.id, { x: slot.x, y: slot.y });
15024 placed.push({
15025 x: slot.x - box.width / 2,
15026 y: slot.y - box.height / 2,
15027 w: box.width,
15028 h: box.height
15029 });
15030 }
15031 }
15032 function syncChipPositions() {
15033 const chipCounterScale = 1 / Math.max(0.01, world.scale.x);
15034 const anyFocus = focusId !== null;
15035 for (const box of tags.values()) {
15036 const c = box.chip.container;
15037 c.x = box.x;
15038 c.y = box.y;
15039 const counter = Math.max(1, chipCounterScale);
15040 c.scale.set(counter);
15041 c.rotation = box.rotation;
15042 const focused = focusId === box.id;
15043 const targetAlpha = !anyFocus || focused ? 1 : 0.32;
15044 if (Math.abs(c.alpha - targetAlpha) > 5e-3) {
15045 c.alpha += (targetAlpha - c.alpha) * 0.18;
15046 } else {
15047 c.alpha = targetAlpha;
15048 }
15049 }
15050 for (const post of postNodes.values()) {
15051 const chip = postChips.get(post.id);
15052 if (!chip) {
15053 continue;
15054 }
15055 chip.container.x = post.x;
15056 chip.container.y = post.y;
15057 chip.container.scale.set(chipCounterScale);
15058 if (chip.container.alpha < 1) {
15059 chip.container.alpha = Math.min(
15060 1,
15061 chip.container.alpha + 0.18
15062 );
15063 }
15064 }
15065 }
15066 function tick() {
15067 const now = performance.now();
15068 const dt = Math.min(50, now - lastTick);
15069 lastTick = now;
15070 const ZOOM_EASE = 0.22;
15071 const ds = targetScale - world.scale.x;
15072 const dwx = targetWorldX - world.x;
15073 const dwy = targetWorldY - world.y;
15074 if (Math.abs(ds) > 5e-4 || Math.abs(dwx) > 0.5 || Math.abs(dwy) > 0.5) {
15075 world.scale.set(world.scale.x + ds * ZOOM_EASE);
15076 world.x += dwx * ZOOM_EASE;
15077 world.y += dwy * ZOOM_EASE;
15078 }
15079 for (const box of tags.values()) {
15080 if (box === dragChip) {
15081 continue;
15082 }
15083 let tx = box.tx;
15084 let ty = box.ty;
15085 if (nudgeAwayFrom && box.id !== focusId) {
15086 const dx = box.tx - nudgeAwayFrom.x;
15087 const dy = box.ty - nudgeAwayFrom.y;
15088 const d = Math.sqrt(dx * dx + dy * dy) || 1;
15089 const limit = nudgeAwayFrom.radius + Math.max(box.width, box.height) / 2;
15090 if (d < limit) {
15091 const push = limit + 12;
15092 tx = nudgeAwayFrom.x + dx / d * push;
15093 ty = nudgeAwayFrom.y + dy / d * push;
15094 }
15095 }
15096 const ease = 1 - Math.exp(-dt * 0.012);
15097 box.x += (tx - box.x) * ease;
15098 box.y += (ty - box.y) * ease;
15099 }
15100 for (const p of postNodes.values()) {
15101 p.x += (p.tx - p.x) * 0.18;
15102 p.y += (p.ty - p.y) * 0.18;
15103 p.gfx.x = p.x;
15104 p.gfx.y = p.y;
15105 }
15106 drawPostEdges();
15107 syncChipPositions();
15108 raf = requestAnimationFrame(tick);
15109 }
15110 function drawPostEdges() {
15111 postEdgeGfx.clear();
15112 if (focusId === null) {
15113 return;
15114 }
15115 const center = tags.get(focusId);
15116 if (!center) {
15117 return;
15118 }
15119 for (const post of postNodes.values()) {
15120 postEdgeGfx.moveTo(center.x, center.y);
15121 postEdgeGfx.lineTo(post.x, post.y);
15122 postEdgeGfx.stroke({
15123 color: hslToInt(center.hue, 60, 50),
15124 width: 1,
15125 alpha: 0.35
15126 });
15127 }
15128 }
15129 function stageToWorld(global) {
15130 return {
15131 x: (global.x - world.x) / world.scale.x,
15132 y: (global.y - world.y) / world.scale.y
15133 };
15134 }
15135 function onStagePointerDown(e) {
15136 const ev = e;
15137 panActive = true;
15138 panStart = { x: ev.global.x, y: ev.global.y };
15139 panMovedDist = 0;
15140 }
15141 function onStagePointerMove(e) {
15142 const ev = e;
15143 if (dragChip) {
15144 const cursorWorld = stageToWorld(ev.global);
15145 const nx = cursorWorld.x + dragOffset.x;
15146 const ny = cursorWorld.y + dragOffset.y;
15147 dragChip.x = nx;
15148 dragChip.y = ny;
15149 dragChip.tx = nx;
15150 dragChip.ty = ny;
15151 return;
15152 }
15153 if (panActive && panStart) {
15154 const dx = ev.global.x - panStart.x;
15155 const dy = ev.global.y - panStart.y;
15156 world.x += dx;
15157 world.y += dy;
15158 targetWorldX += dx;
15159 targetWorldY += dy;
15160 panMovedDist += Math.sqrt(dx * dx + dy * dy);
15161 panStart = { x: ev.global.x, y: ev.global.y };
15162 }
15163 }
15164 function onStagePointerUp(e) {
15165 if (dragChip) {
15166 const box = dragChip;
15167 const startPos = dragStart;
15168 dragChip = null;
15169 dragStart = null;
15170 let movement = Infinity;
15171 const ev = e;
15172 if (startPos && ev && ev.global) {
15173 const dx = ev.global.x - startPos.x;
15174 const dy = ev.global.y - startPos.y;
15175 movement = Math.sqrt(dx * dx + dy * dy);
15176 }
15177 if (movement < 3) {
15178 void focusTag(box.id);
15179 } else {
15180 persistedPositions.set(box.id, { x: box.tx, y: box.ty });
15181 writePersistedPositions(positionsKey, persistedPositions);
15182 }
15183 }
15184 panActive = false;
15185 panStart = null;
15186 }
15187 app.stage.eventMode = "static";
15188 app.stage.hitArea = new pixi.Rectangle(
15189 0,
15190 0,
15191 stage.clientWidth,
15192 stage.clientHeight
15193 );
15194 app.stage.on("pointerdown", onStagePointerDown);
15195 app.stage.on("pointermove", onStagePointerMove);
15196 app.stage.on("pointerup", (e) => onStagePointerUp(e));
15197 app.stage.on("pointerupoutside", (e) => onStagePointerUp(e));
15198 function onWheel(e) {
15199 e.preventDefault();
15200 const SENSITIVITY = 8e-4;
15201 const factor = Math.exp(-e.deltaY * SENSITIVITY);
15202 const prev = targetScale;
15203 const next = Math.max(0.3, Math.min(2.5, prev * factor));
15204 if (Math.abs(next - prev) < 5e-4) {
15205 return;
15206 }
15207 const r = stage.getBoundingClientRect();
15208 const sx = e.clientX - r.left;
15209 const sy = e.clientY - r.top;
15210 const wx = (sx - targetWorldX) / prev;
15211 const wy = (sy - targetWorldY) / prev;
15212 targetScale = next;
15213 targetWorldX = sx - wx * next;
15214 targetWorldY = sy - wy * next;
15215 }
15216 stage.addEventListener("wheel", onWheel, { passive: false });
15217 let firstFitDone = false;
15218 let settledW = 0;
15219 let settledH = 0;
15220 const SETTLE_THRESHOLD_PX = 24;
15221 const SETTLE_DEBOUNCE_MS = 80;
15222 let settleTimer = null;
15223 function onResize() {
15224 const r = stage.getBoundingClientRect();
15225 app.renderer.resize(r.width, r.height);
15226 app.stage.hitArea = new pixi.Rectangle(0, 0, r.width, r.height);
15227 if (!firstFitDone && r.width > 0 && r.height > 0) {
15228 firstFitDone = true;
15229 settledW = r.width;
15230 settledH = r.height;
15231 fitToView();
15232 stage.classList.remove("is-loading");
15233 }
15234 if (settleTimer !== null) {
15235 window.clearTimeout(settleTimer);
15236 }
15237 settleTimer = window.setTimeout(() => {
15238 settleTimer = null;
15239 const cur = stage.getBoundingClientRect();
15240 const dw = Math.abs(cur.width - settledW);
15241 const dh = Math.abs(cur.height - settledH);
15242 if (dw >= SETTLE_THRESHOLD_PX || dh >= SETTLE_THRESHOLD_PX) {
15243 settledW = cur.width;
15244 settledH = cur.height;
15245 recenterCamera();
15246 }
15247 }, SETTLE_DEBOUNCE_MS);
15248 app.render();
15249 }
15250 const ro = new ResizeObserver(onResize);
15251 ro.observe(stage);
15252 async function focusTag(id) {
15253 if (focusId === id) {
15254 closeFocus();
15255 return;
15256 }
15257 const wasFocused = focusId !== null;
15258 focusId = id;
15259 focusPage = 1;
15260 lastFocusChange = performance.now();
15261 const focused = tags.get(id);
15262 if (focused) {
15263 if (!wasFocused) {
15264 prevView = {
15265 scale: targetScale,
15266 x: targetWorldX,
15267 y: targetWorldY
15268 };
15269 }
15270 const r = stage.getBoundingClientRect();
15271 if (r.width > 0 && r.height > 0) {
15272 const half = POST_RING_RADIUS + 70;
15273 const sx = r.width * 0.85 / (2 * half);
15274 const sy = r.height * 0.85 / (2 * half);
15275 const newScale = Math.max(
15276 0.5,
15277 Math.min(1.6, Math.min(sx, sy))
15278 );
15279 targetScale = newScale;
15280 targetWorldX = r.width / 2 - focused.x * newScale;
15281 targetWorldY = r.height / 2 - focused.y * newScale;
15282 }
15283 nudgeAwayFrom = {
15284 x: focused.x,
15285 y: focused.y,
15286 radius: SPOTLIGHT_RADIUS
15287 };
15288 }
15289 for (const box of tags.values()) {
15290 paintChip(box);
15291 }
15292 paintSidebar();
15293 await loadPostsForFocus();
15294 }
15295 function closeFocus() {
15296 focusId = null;
15297 lastFocusChange = performance.now();
15298 loadSeq++;
15299 nudgeAwayFrom = null;
15300 if (prevView) {
15301 targetScale = prevView.scale;
15302 targetWorldX = prevView.x;
15303 targetWorldY = prevView.y;
15304 prevView = null;
15305 }
15306 paintSidebar();
15307 clearPosts();
15308 for (const box of tags.values()) {
15309 paintChip(box);
15310 }
15311 }
15312 function clearPosts() {
15313 for (const post of postNodes.values()) {
15314 postLayer.removeChild(post.gfx);
15315 post.gfx.destroy();
15316 }
15317 postNodes.clear();
15318 for (const chip of postChips.values()) {
15319 postChipLayer.removeChild(chip.container);
15320 chip.container.destroy({ children: true });
15321 }
15322 postChips.clear();
15323 postEdgeGfx.clear();
15324 pager.visible = false;
15325 }
15326 function ensurePostChip(post) {
15327 const existing = postChips.get(post.id);
15328 if (existing) {
15329 return existing;
15330 }
15331 const container = new pixi.Container();
15332 container.eventMode = "static";
15333 container.cursor = "pointer";
15334 container.alpha = 0;
15335 const bg = new pixi.Graphics();
15336 container.addChild(bg);
15337 const dot = new pixi.Graphics();
15338 container.addChild(dot);
15339 const titleText = new pixi.Text({
15340 text: post.title,
15341 style: {
15342 fill: 1909543,
15343 fontSize: 12,
15344 fontFamily: FONT_FAMILY,
15345 fontWeight: "500"
15346 },
15347 resolution: CHIP_TEXT_RES
15348 });
15349 container.addChild(titleText);
15350 const chip = {
15351 container,
15352 bg,
15353 dot,
15354 titleText,
15355 width: 0,
15356 height: 0,
15357 cachedTitle: "",
15358 cachedHover: false
15359 };
15360 postChips.set(post.id, chip);
15361 postChipLayer.addChild(container);
15362 container.on("pointerdown", (e) => {
15363 e.stopPropagation?.();
15364 pixiInteractionAt = performance.now();
15365 });
15366 container.on("pointertap", () => {
15367 openInPostsTab(post.id, post.editUrl, post.title);
15368 closeFocus();
15369 });
15370 container.on("pointerover", () => {
15371 chip.cachedHover = true;
15372 layoutPostChip(chip, post);
15373 });
15374 container.on("pointerout", () => {
15375 chip.cachedHover = false;
15376 layoutPostChip(chip, post);
15377 });
15378 layoutPostChip(chip, post);
15379 return chip;
15380 }
15381 function layoutPostChip(chip, post) {
15382 const displayTitle = post.title.length > POST_TITLE_MAX_CHARS ? post.title.slice(0, POST_TITLE_MAX_CHARS - 1) + "…" : post.title;
15383 if (chip.titleText.text !== displayTitle) {
15384 chip.titleText.text = displayTitle;
15385 }
15386 chip.cachedTitle = displayTitle;
15387 const padX = 9;
15388 const padY = 3;
15389 const dotR = 4;
15390 const gap = 6;
15391 const titleW = chip.titleText.width;
15392 const titleH = chip.titleText.height;
15393 const totalW = padX + dotR * 2 + gap + titleW + padX;
15394 const totalH = Math.max(titleH, dotR * 2) + padY * 2;
15395 chip.width = totalW;
15396 chip.height = totalH;
15397 const left = -totalW / 2;
15398 const top = -totalH / 2;
15399 chip.bg.clear();
15400 chip.bg.roundRect(left, top, totalW, totalH, totalH / 2);
15401 if (chip.cachedHover) {
15402 chip.bg.fill({ color: 16777215, alpha: 1 });
15403 chip.bg.stroke({
15404 color: post.tone,
15405 width: 1.5,
15406 alpha: 1
15407 });
15408 } else {
15409 chip.bg.fill({ color: 16777215, alpha: 0.95 });
15410 chip.bg.stroke({
15411 color: 0,
15412 width: 1,
15413 alpha: 0.12
15414 });
15415 }
15416 chip.dot.clear();
15417 chip.dot.circle(left + padX + dotR, 0, dotR);
15418 chip.dot.fill({ color: post.tone, alpha: 0.85 });
15419 chip.dot.stroke({ color: 16777215, width: 1 });
15420 chip.titleText.x = left + padX + dotR * 2 + gap;
15421 chip.titleText.y = -titleH / 2;
15422 }
15423 const POSTS_CACHE_TTL_MS = 6e4;
15424 const postsCache = /* @__PURE__ */ new Map();
15425 function applyPostsResult(entry, focusedTagId) {
15426 focusTotalPages = entry.totalPages;
15427 if (Number.isFinite(entry.realTotal)) {
15428 const box = tags.get(focusedTagId);
15429 if (box && box.count !== entry.realTotal) {
15430 box.count = entry.realTotal;
15431 terms = terms.map(
15432 (t) => t.id === box.id ? { ...t, count: entry.realTotal } : t
15433 );
15434 layoutChip(box);
15435 }
15436 }
15437 renderPosts(entry.items);
15438 }
15439 async function loadPostsForFocus() {
15440 if (focusId === null) {
15441 return;
15442 }
15443 const mySeq = ++loadSeq;
15444 const myFocusId = focusId;
15445 const cacheKey2 = `${focusId}:${focusPage}`;
15446 const cached = postsCache.get(cacheKey2);
15447 if (cached && performance.now() - cached.fetchedAt < POSTS_CACHE_TTL_MS) {
15448 applyPostsResult(cached, myFocusId);
15449 return;
15450 }
15451 const cfg = client.getConfig();
15452 const url = new URL(cfg.postsUrl);
15453 url.searchParams.set("tags", String(focusId));
15454 url.searchParams.set("per_page", String(POST_PER_PAGE));
15455 url.searchParams.set("page", String(focusPage));
15456 url.searchParams.set("status", "any");
15457 url.searchParams.set("_fields", "id,title,status");
15458 try {
15459 const response = await fetchShellJson(client, url.toString());
15460 if (mySeq !== loadSeq || focusId !== myFocusId) {
15461 return;
15462 }
15463 const raw = response.json ?? [];
15464 const totalPages = Math.max(
15465 1,
15466 parseInt(response.headers.get("X-WP-TotalPages") ?? "1", 10) || 1
15467 );
15468 const realTotalParsed = parseInt(response.headers.get("X-WP-Total") ?? "", 10);
15469 const realTotal = Number.isFinite(realTotalParsed) ? realTotalParsed : -1;
15470 const items = raw.map((p) => ({
15471 id: p.id,
15472 title: stripTags(p.title?.rendered || `#${p.id}`),
15473 editUrl: `${cfg.editPostUrlBase}?post=${p.id}&action=edit`
15474 }));
15475 const entry = {
15476 items,
15477 totalPages,
15478 realTotal,
15479 fetchedAt: performance.now()
15480 };
15481 postsCache.set(cacheKey2, entry);
15482 applyPostsResult(entry, myFocusId);
15483 } catch (err) {
15484 showError(__("Couldn’t load posts:"), err);
15485 }
15486 }
15487 function renderPosts(items) {
15488 clearPosts();
15489 if (focusId === null) {
15490 return;
15491 }
15492 const center = tags.get(focusId);
15493 if (!center) {
15494 return;
15495 }
15496 const count = items.length;
15497 const ringR = POST_RING_RADIUS + Math.max(0, count - 8) * 6;
15498 const tone = hslToInt(center.hue, 70, 48);
15499 items.forEach((item, idx) => {
15500 const angle = 2 * Math.PI / Math.max(1, count) * idx - Math.PI / 2;
15501 const tx = center.x + Math.cos(angle) * ringR;
15502 const ty = center.y + Math.sin(angle) * ringR;
15503 const gfx = new pixi.Graphics();
15504 postLayer.addChild(gfx);
15505 const post = {
15506 id: item.id,
15507 title: item.title,
15508 editUrl: item.editUrl,
15509 angle,
15510 r: ringR,
15511 x: center.x,
15512 y: center.y,
15513 tx,
15514 ty,
15515 gfx,
15516 tone
15517 };
15518 postNodes.set(item.id, post);
15519 ensurePostChip(post);
15520 });
15521 repaintPager();
15522 }
15523 function repaintPager() {
15524 if (focusId === null || focusTotalPages <= 1) {
15525 pager.visible = false;
15526 return;
15527 }
15528 pager.visible = true;
15529 const center = tags.get(focusId);
15530 if (!center) {
15531 pager.visible = false;
15532 return;
15533 }
15534 const prevDisabled = focusPage <= 1;
15535 const nextDisabled = focusPage >= focusTotalPages;
15536 drawPagerButton(pagerPrev, "◀", prevDisabled);
15537 drawPagerButton(pagerNext, "▶", nextDisabled);
15538 pagerPrev.cursor = prevDisabled ? "default" : "pointer";
15539 pagerNext.cursor = nextDisabled ? "default" : "pointer";
15540 pagerLabel.text = `${focusPage} / ${focusTotalPages}`;
15541 pagerPrev.x = -38;
15542 pagerPrev.y = 0;
15543 pagerNext.x = 38;
15544 pagerNext.y = 0;
15545 pagerLabel.x = 0;
15546 pagerLabel.y = 0;
15547 pager.x = center.x;
15548 pager.y = center.y + POST_RING_RADIUS + 60;
15549 }
15550 function drawPagerButton(gfx, glyph, disabled) {
15551 gfx.clear();
15552 gfx.circle(0, 0, 14);
15553 gfx.fill({
15554 color: disabled ? 15921906 : 16777215,
15555 alpha: disabled ? 0.7 : 1
15556 });
15557 gfx.stroke({
15558 color: 0,
15559 width: 1,
15560 alpha: 0.12
15561 });
15562 const children = gfx.children;
15563 const label = children?.[0] ?? null;
15564 if (!label) {
15565 const t = new pixi.Text({
15566 text: glyph,
15567 style: {
15568 fill: disabled ? 11580344 : 5265246,
15569 fontSize: 14,
15570 fontFamily: FONT_FAMILY,
15571 fontWeight: "600"
15572 }
15573 });
15574 t.anchor.set(0.5);
15575 gfx.addChild(t);
15576 } else {
15577 label.text = glyph;
15578 label.style.fill = disabled ? 11580344 : 5265246;
15579 }
15580 }
15581 function openInPostsTab(_id, editUrl, title) {
15582 const wm = api?.windowManager;
15583 const derive = api?.deriveWindowId;
15584 const postsWin = wm && typeof wm.getById === "function" ? wm.getById("desktop-mode-posts") : void 0;
15585 if (postsWin && typeof postsWin.isFullscreen === "function" && typeof postsWin.toggleFullscreen === "function" && postsWin.isFullscreen()) {
15586 postsWin.toggleFullscreen();
15587 }
15588 if (wm && typeof derive === "function") {
15589 const id = derive(editUrl);
15590 wm.open({
15591 id,
15592 baseId: id,
15593 url: editUrl,
15594 title: title ?? editUrl,
15595 icon: "dashicons-admin-post"
15596 });
15597 return;
15598 }
15599 try {
15600 window.open(editUrl, "_blank");
15601 } catch {
15602 window.location.assign(editUrl);
15603 }
15604 }
15605 function paintDraftSidebar() {
15606 const header = document.createElement("div");
15607 header.className = "wpd-tagcloud__sidebar-header";
15608 const dot = document.createElement("span");
15609 dot.className = "wpd-tagcloud__sidebar-dot";
15610 dot.style.background = `hsl( ${themeHue}deg 60% 55% )`;
15611 const label = document.createElement("code");
15612 label.className = "wpd-tagcloud__sidebar-slug";
15613 label.textContent = __("New tag");
15614 header.appendChild(dot);
15615 header.appendChild(label);
15616 sidebar.appendChild(header);
15617 const nameLabel = document.createElement("label");
15618 nameLabel.className = "wpd-tagcloud__sidebar-label";
15619 nameLabel.textContent = __("Name");
15620 sidebar.appendChild(nameLabel);
15621 const nameInput = document.createElement("input");
15622 nameInput.type = "text";
15623 nameInput.className = "wpd-tagcloud__editor-name";
15624 nameInput.placeholder = __("e.g. featured");
15625 sidebar.appendChild(nameInput);
15626 requestAnimationFrame(() => nameInput.focus());
15627 const descLabel = document.createElement("label");
15628 descLabel.className = "wpd-tagcloud__sidebar-label";
15629 descLabel.textContent = __("Description");
15630 sidebar.appendChild(descLabel);
15631 const descInput = document.createElement("textarea");
15632 descInput.className = "wpd-tagcloud__editor-desc";
15633 descInput.placeholder = __("Description (optional)");
15634 descInput.rows = 4;
15635 sidebar.appendChild(descInput);
15636 const actions = document.createElement("div");
15637 actions.className = "wpd-tagcloud__editor-actions";
15638 const createBtn = document.createElement("button");
15639 createBtn.type = "button";
15640 createBtn.className = "wpd-tagcloud__btn wpd-tagcloud__btn--primary";
15641 createBtn.textContent = __("Create");
15642 const cancelBtn = document.createElement("button");
15643 cancelBtn.type = "button";
15644 cancelBtn.className = "wpd-tagcloud__btn wpd-tagcloud__btn--danger";
15645 cancelBtn.textContent = __("Cancel");
15646 const handleCreate = async () => {
15647 const name = nameInput.value.trim();
15648 if (!name) {
15649 nameInput.focus();
15650 return;
15651 }
15652 createBtn.disabled = true;
15653 try {
15654 const created = await client.createTag(name);
15655 const next = {
15656 id: created.id,
15657 name: created.name,
15658 slug: created.slug || "",
15659 parent: 0,
15660 count: 0,
15661 description: created.description || "",
15662 isDefault: false
15663 };
15664 if (!terms.some((t) => t.id === next.id)) {
15665 terms = terms.concat(next);
15666 }
15667 const desc = descInput.value.trim();
15668 if (desc) {
15669 try {
15670 const updated = await client.updateTerm(
15671 "tags",
15672 created.id,
15673 { description: desc }
15674 );
15675 terms = terms.map(
15676 (t) => t.id === updated.id ? {
15677 ...t,
15678 description: updated.description ?? desc
15679 } : t
15680 );
15681 } catch {
15682 showError(
15683 __("Tag created but description failed:"),
15684 null
15685 );
15686 }
15687 }
15688 draft = null;
15689 buildCloud();
15690 focusId = created.id;
15691 paintSidebar();
15692 await loadPostsForFocus();
15693 } catch (err) {
15694 createBtn.disabled = false;
15695 showError(__("Couldn’t create:"), err);
15696 }
15697 };
15698 createBtn.addEventListener("click", () => {
15699 void handleCreate();
15700 });
15701 cancelBtn.addEventListener("click", () => {
15702 draft = null;
15703 paintSidebar();
15704 });
15705 nameInput.addEventListener("keydown", (e) => {
15706 if (e.key === "Enter") {
15707 e.preventDefault();
15708 void handleCreate();
15709 } else if (e.key === "Escape") {
15710 draft = null;
15711 paintSidebar();
15712 }
15713 });
15714 actions.appendChild(createBtn);
15715 actions.appendChild(cancelBtn);
15716 sidebar.appendChild(actions);
15717 }
15718 function paintSidebar() {
15719 sidebar.replaceChildren();
15720 if (draft !== null) {
15721 paintDraftSidebar();
15722 return;
15723 }
15724 if (focusId === null) {
15725 const empty = document.createElement("div");
15726 empty.className = "wpd-tagcloud__sidebar-empty";
15727 const icon = document.createElement("span");
15728 icon.className = "dashicons dashicons-tag";
15729 icon.setAttribute("aria-hidden", "true");
15730 empty.appendChild(icon);
15731 const title = document.createElement("h3");
15732 title.className = "wpd-tagcloud__sidebar-empty-title";
15733 title.textContent = __("No tag selected");
15734 empty.appendChild(title);
15735 const help = document.createElement("p");
15736 help.className = "wpd-tagcloud__sidebar-empty-hint";
15737 help.textContent = __(
15738 "Click a tag on the cloud to edit it, or click + Add tag to create a new one."
15739 );
15740 empty.appendChild(help);
15741 sidebar.appendChild(empty);
15742 return;
15743 }
15744 const box = tags.get(focusId);
15745 if (!box) {
15746 focusId = null;
15747 paintSidebar();
15748 return;
15749 }
15750 const id = box.id;
15751 const header = document.createElement("div");
15752 header.className = "wpd-tagcloud__sidebar-header";
15753 const dot = document.createElement("span");
15754 dot.className = "wpd-tagcloud__sidebar-dot";
15755 dot.style.background = `hsl( ${box.hue}deg 60% 55% )`;
15756 const term = terms.find((t) => t.id === id);
15757 const idLabel = document.createElement("code");
15758 idLabel.className = "wpd-tagcloud__sidebar-slug";
15759 idLabel.textContent = `#${id}`;
15760 header.appendChild(dot);
15761 header.appendChild(idLabel);
15762 sidebar.appendChild(header);
15763 const nameLabel = document.createElement("label");
15764 nameLabel.className = "wpd-tagcloud__sidebar-label";
15765 nameLabel.textContent = __("Name");
15766 sidebar.appendChild(nameLabel);
15767 const nameInput = document.createElement("input");
15768 nameInput.type = "text";
15769 nameInput.className = "wpd-tagcloud__editor-name";
15770 nameInput.value = box.name;
15771 nameInput.placeholder = __("Name");
15772 sidebar.appendChild(nameInput);
15773 const slugLabel = document.createElement("label");
15774 slugLabel.className = "wpd-tagcloud__sidebar-label";
15775 slugLabel.textContent = __("Slug");
15776 sidebar.appendChild(slugLabel);
15777 const slugInput = document.createElement("input");
15778 slugInput.type = "text";
15779 slugInput.className = "wpd-tagcloud__editor-name";
15780 slugInput.value = term?.slug || "";
15781 slugInput.placeholder = __("auto-from-name");
15782 slugInput.spellcheck = false;
15783 slugInput.autocapitalize = "off";
15784 slugInput.addEventListener("input", () => {
15785 const v = slugInput.value;
15786 const norm = v.toLowerCase().replace(/[^a-z0-9-]+/g, "-");
15787 if (v !== norm) {
15788 const sel = slugInput.selectionStart ?? norm.length;
15789 slugInput.value = norm;
15790 slugInput.setSelectionRange(sel, sel);
15791 }
15792 });
15793 sidebar.appendChild(slugInput);
15794 const descLabel = document.createElement("label");
15795 descLabel.className = "wpd-tagcloud__sidebar-label";
15796 descLabel.textContent = __("Description");
15797 sidebar.appendChild(descLabel);
15798 const descInput = document.createElement("textarea");
15799 descInput.className = "wpd-tagcloud__editor-desc";
15800 descInput.value = box.description || "";
15801 descInput.placeholder = __("Description (optional)");
15802 descInput.rows = 4;
15803 sidebar.appendChild(descInput);
15804 const meta = document.createElement("p");
15805 meta.className = "wpd-tagcloud__sidebar-meta";
15806 meta.textContent = sprintf(
15807 /* translators: %d: post count. */
15808 __("%d posts tagged with this."),
15809 box.count
15810 );
15811 sidebar.appendChild(meta);
15812 const actions = document.createElement("div");
15813 actions.className = "wpd-tagcloud__editor-actions";
15814 const saveBtn = document.createElement("button");
15815 saveBtn.type = "button";
15816 saveBtn.className = "wpd-tagcloud__btn wpd-tagcloud__btn--primary";
15817 saveBtn.textContent = __("Save");
15818 saveBtn.addEventListener("click", async () => {
15819 const name = nameInput.value.trim();
15820 if (!name) {
15821 return;
15822 }
15823 const description = descInput.value;
15824 const slugRaw = slugInput.value.trim();
15825 const currentSlug = term?.slug ?? "";
15826 if (name === box.name && description === (box.description || "") && slugRaw === currentSlug) {
15827 return;
15828 }
15829 const patch = { name, description };
15830 if (slugRaw !== currentSlug) {
15831 patch.slug = slugRaw;
15832 }
15833 try {
15834 const updated = await client.updateTerm("tags", box.id, patch);
15835 box.name = updated.name;
15836 box.description = updated.description;
15837 box.slug = updated.slug ?? box.slug;
15838 box.hue = tagHue(box.slug || box.name, themeHue);
15839 box.rotation = tagRotation(box.slug || box.name);
15840 terms = terms.map(
15841 (t) => t.id === box.id ? {
15842 ...t,
15843 name: updated.name,
15844 description: updated.description,
15845 slug: updated.slug ?? t.slug
15846 } : t
15847 );
15848 layoutChip(box);
15849 paintSidebar();
15850 } catch (err) {
15851 showError(__("Couldn’t save:"), err);
15852 }
15853 });
15854 const delBtn = document.createElement("button");
15855 delBtn.type = "button";
15856 delBtn.className = "wpd-tagcloud__btn wpd-tagcloud__btn--danger";
15857 delBtn.textContent = __("Delete");
15858 let armResetTimer = null;
15859 const armDelete = () => {
15860 delBtn.textContent = __("Click again to delete");
15861 delBtn.classList.add("is-armed");
15862 if (armResetTimer !== null) {
15863 window.clearTimeout(armResetTimer);
15864 }
15865 armResetTimer = window.setTimeout(() => {
15866 delBtn.textContent = __("Delete");
15867 delBtn.classList.remove("is-armed");
15868 armResetTimer = null;
15869 }, 2500);
15870 };
15871 delBtn.addEventListener("click", async () => {
15872 if (!delBtn.classList.contains("is-armed")) {
15873 armDelete();
15874 return;
15875 }
15876 if (armResetTimer !== null) {
15877 window.clearTimeout(armResetTimer);
15878 armResetTimer = null;
15879 }
15880 try {
15881 await client.deleteTerm("tags", box.id);
15882 terms = terms.filter((t) => t.id !== box.id);
15883 persistedPositions.delete(box.id);
15884 writePersistedPositions(positionsKey, persistedPositions);
15885 focusId = null;
15886 clearPosts();
15887 buildCloud();
15888 paintSidebar();
15889 } catch (err) {
15890 showError(__("Couldn’t delete:"), err);
15891 }
15892 });
15893 actions.appendChild(saveBtn);
15894 actions.appendChild(delBtn);
15895 sidebar.appendChild(actions);
15896 }
15897 function startDraft() {
15898 draft = true;
15899 paintSidebar();
15900 }
15901 addTagBtn.addEventListener("click", () => {
15902 startDraft();
15903 });
15904 function fitToView(opts = {}) {
15905 const padding = opts.padding ?? 90;
15906 const animate = opts.animate ?? false;
15907 const r = stage.getBoundingClientRect();
15908 if (tags.size === 0 || r.width === 0 || r.height === 0) {
15909 const cx2 = r.width / 2;
15910 const cy2 = r.height / 2;
15911 targetScale = 1;
15912 targetWorldX = cx2;
15913 targetWorldY = cy2;
15914 if (!animate) {
15915 world.x = cx2;
15916 world.y = cy2;
15917 world.scale.set(1);
15918 }
15919 return;
15920 }
15921 let minX = Infinity;
15922 let minY = Infinity;
15923 let maxX = -Infinity;
15924 let maxY = -Infinity;
15925 for (const box of tags.values()) {
15926 minX = Math.min(minX, box.tx - box.width / 2);
15927 minY = Math.min(minY, box.ty - box.height / 2);
15928 maxX = Math.max(maxX, box.tx + box.width / 2);
15929 maxY = Math.max(maxY, box.ty + box.height / 2);
15930 }
15931 const w = Math.max(1, maxX - minX);
15932 const h = Math.max(1, maxY - minY);
15933 const sx = (r.width - padding * 2) / w;
15934 const sy = (r.height - padding * 2) / h;
15935 const scale = Math.max(0.2, Math.min(1.5, Math.min(sx, sy)));
15936 const cx = (minX + maxX) / 2;
15937 const cy = (minY + maxY) / 2;
15938 const newWorldX = r.width / 2 - cx * scale;
15939 const newWorldY = r.height / 2 - cy * scale;
15940 targetScale = scale;
15941 targetWorldX = newWorldX;
15942 targetWorldY = newWorldY;
15943 if (!animate) {
15944 world.scale.set(scale);
15945 world.x = newWorldX;
15946 world.y = newWorldY;
15947 }
15948 }
15949 function recenterCamera() {
15950 if (focusId !== null) {
15951 const focused = tags.get(focusId);
15952 const r = stage.getBoundingClientRect();
15953 if (focused && r.width > 0 && r.height > 0) {
15954 const half = POST_RING_RADIUS + 70;
15955 const sx = r.width * 0.85 / (2 * half);
15956 const sy = r.height * 0.85 / (2 * half);
15957 const newScale = Math.max(
15958 0.5,
15959 Math.min(1.6, Math.min(sx, sy))
15960 );
15961 targetScale = newScale;
15962 targetWorldX = r.width / 2 - focused.x * newScale;
15963 targetWorldY = r.height / 2 - focused.y * newScale;
15964 return;
15965 }
15966 }
15967 fitToView({ animate: true });
15968 }
15969 recenterBtn.addEventListener("click", () => recenterCamera());
15970 reflowBtn.addEventListener("click", () => {
15971 persistedPositions.clear();
15972 writePersistedPositions(positionsKey, persistedPositions);
15973 for (const box of tags.values()) {
15974 box.tx = 0;
15975 box.ty = 0;
15976 }
15977 const allBoxes = Array.from(tags.values());
15978 allBoxes.sort((a, b) => b.count - a.count);
15979 packBoxesWithClusters(
15980 allBoxes,
15981 [],
15982 /* @__PURE__ */ new Map(),
15983 cooccurrenceMap
15984 );
15985 fitToView({ animate: true });
15986 void refreshCooccurrence();
15987 });
15988 app.canvas.addEventListener("click", (e) => {
15989 const now = performance.now();
15990 if (now - lastFocusChange < 250 || now - pixiInteractionAt < 250) {
15991 return;
15992 }
15993 if (panMovedDist > 4) {
15994 return;
15995 }
15996 const target = e.target;
15997 if (target === app.canvas && !dragChip && focusId !== null) {
15998 closeFocus();
15999 }
16000 });
16001 async function refreshCountsViaBulk() {
16002 if (terms.length === 0) {
16003 return;
16004 }
16005 const cfg = client.getConfig();
16006 const url = new URL(
16007 joinRestUrl(cfg.restRoot, "desktop-mode/v1/term-counts")
16008 );
16009 url.searchParams.set("taxonomy", "post_tag");
16010 url.searchParams.set(
16011 "ids",
16012 terms.map((t) => t.id).join(",")
16013 );
16014 try {
16015 const response = await fetchShellJson(client, url.toString());
16016 const map = response.json;
16017 let dirty = false;
16018 terms = terms.map((t) => {
16019 const fresh = map[String(t.id)];
16020 if (typeof fresh === "number" && fresh !== t.count) {
16021 dirty = true;
16022 const box = tags.get(t.id);
16023 if (box) {
16024 box.count = fresh;
16025 }
16026 return { ...t, count: fresh };
16027 }
16028 return t;
16029 });
16030 if (dirty) {
16031 const maxCount = Math.max(
16032 1,
16033 ...terms.map((t) => t.count)
16034 );
16035 for (const t of terms) {
16036 const box = tags.get(t.id);
16037 if (!box) {
16038 continue;
16039 }
16040 box.count = t.count;
16041 box.fontSize = fontSizeFor(t.count, maxCount);
16042 layoutChip(box);
16043 }
16044 if (focusId !== null) {
16045 paintSidebar();
16046 }
16047 }
16048 } catch {
16049 }
16050 }
16051 function relayoutWithCooccurrence() {
16052 const placed = [];
16053 const placedById = /* @__PURE__ */ new Map();
16054 const toRepack = [];
16055 for (const box of tags.values()) {
16056 if (persistedPositions.has(box.id)) {
16057 placed.push({
16058 x: box.tx - box.width / 2,
16059 y: box.ty - box.height / 2,
16060 w: box.width,
16061 h: box.height
16062 });
16063 placedById.set(box.id, { x: box.tx, y: box.ty });
16064 } else {
16065 toRepack.push(box);
16066 }
16067 }
16068 toRepack.sort((a, b) => b.count - a.count);
16069 packBoxesWithClusters(toRepack, placed, placedById, cooccurrenceMap);
16070 }
16071 async function refreshCooccurrence() {
16072 try {
16073 const fetched = await client.fetchTagCooccurrence("tags", 8);
16074 cooccurrenceMap = fetched;
16075 if (cooccurrenceMap.size > 0) {
16076 relayoutWithCooccurrence();
16077 }
16078 } catch {
16079 }
16080 }
16081 buildCloud();
16082 paintSidebar();
16083 raf = requestAnimationFrame(tick);
16084 void refreshCountsViaBulk();
16085 void refreshCooccurrence();
16086 if (terms.length === 0) {
16087 const empty = document.createElement("div");
16088 empty.className = "wpd-tagcloud__empty";
16089 empty.textContent = __(
16090 'No tags yet. Click "Add tag" to start building the cloud.'
16091 );
16092 stage.appendChild(empty);
16093 }
16094 let currentMatches = [];
16095 let selectedIndex = 0;
16096 const repaintHighlight = () => {
16097 const items = searchResults.querySelectorAll(
16098 ".wpd-tagcloud__search-result"
16099 );
16100 items.forEach((el, i) => {
16101 const active = i === selectedIndex;
16102 el.classList.toggle("is-active", active);
16103 if (active) {
16104 el.scrollIntoView({ block: "nearest" });
16105 }
16106 });
16107 };
16108 const selectMatch = (t) => {
16109 searchInput.value = "";
16110 searchResults.hidden = true;
16111 searchResults.replaceChildren();
16112 currentMatches = [];
16113 selectedIndex = 0;
16114 void focusTag(t.id);
16115 };
16116 const renderSearchResults = () => {
16117 const q = searchInput.value.trim().toLowerCase();
16118 if (q.length === 0) {
16119 searchResults.hidden = true;
16120 searchResults.replaceChildren();
16121 currentMatches = [];
16122 selectedIndex = 0;
16123 return;
16124 }
16125 currentMatches = Array.from(tags.values()).filter(
16126 (t) => t.name.toLowerCase().includes(q) || t.slug.toLowerCase().includes(q)
16127 ).sort((a, b) => b.count - a.count).slice(0, 10);
16128 selectedIndex = 0;
16129 searchResults.replaceChildren();
16130 currentMatches.forEach((t, i) => {
16131 const li = document.createElement("li");
16132 const btn = document.createElement("button");
16133 btn.type = "button";
16134 btn.className = "wpd-tagcloud__search-result";
16135 if (i === 0) {
16136 btn.classList.add("is-active");
16137 }
16138 const nameEl = document.createElement("span");
16139 nameEl.className = "wpd-tagcloud__search-title";
16140 nameEl.textContent = t.name || `#${t.id}`;
16141 const countEl = document.createElement("span");
16142 countEl.className = "wpd-tagcloud__search-meta";
16143 countEl.textContent = sprintf(
16144 /* translators: %d: number of posts assigned to a tag. */
16145 __("%d posts"),
16146 t.count
16147 );
16148 btn.appendChild(nameEl);
16149 btn.appendChild(countEl);
16150 btn.addEventListener("mousedown", (ev) => {
16151 ev.preventDefault();
16152 selectMatch(t);
16153 });
16154 btn.addEventListener("mouseenter", () => {
16155 selectedIndex = i;
16156 repaintHighlight();
16157 });
16158 li.appendChild(btn);
16159 searchResults.appendChild(li);
16160 });
16161 searchResults.hidden = currentMatches.length === 0;
16162 };
16163 searchInput.addEventListener("input", renderSearchResults);
16164 searchInput.addEventListener("focus", renderSearchResults);
16165 searchInput.addEventListener("keydown", (ev) => {
16166 if (ev.key === "ArrowDown") {
16167 if (currentMatches.length === 0) {
16168 return;
16169 }
16170 ev.preventDefault();
16171 selectedIndex = Math.min(
16172 selectedIndex + 1,
16173 currentMatches.length - 1
16174 );
16175 repaintHighlight();
16176 } else if (ev.key === "ArrowUp") {
16177 if (currentMatches.length === 0) {
16178 return;
16179 }
16180 ev.preventDefault();
16181 selectedIndex = Math.max(selectedIndex - 1, 0);
16182 repaintHighlight();
16183 } else if (ev.key === "Enter") {
16184 if (currentMatches.length === 0) {
16185 return;
16186 }
16187 ev.preventDefault();
16188 selectMatch(currentMatches[selectedIndex]);
16189 } else if (ev.key === "Escape") {
16190 searchInput.value = "";
16191 searchResults.hidden = true;
16192 searchResults.replaceChildren();
16193 currentMatches = [];
16194 selectedIndex = 0;
16195 }
16196 });
16197 searchInput.addEventListener("blur", () => {
16198 setTimeout(() => {
16199 searchResults.hidden = true;
16200 }, 120);
16201 });
16202 const onDocClickSearch = (ev) => {
16203 if (!searchWrap.contains(ev.target)) {
16204 searchResults.hidden = true;
16205 }
16206 };
16207 document.addEventListener("click", onDocClickSearch);
16208 return () => {
16209 if (raf !== null) {
16210 cancelAnimationFrame(raf);
16211 raf = null;
16212 }
16213 if (settleTimer !== null) {
16214 window.clearTimeout(settleTimer);
16215 settleTimer = null;
16216 }
16217 ro.disconnect();
16218 stage.removeEventListener("wheel", onWheel);
16219 document.removeEventListener("click", onDocClickSearch);
16220 try {
16221 app.ticker?.stop();
16222 } catch {
16223 }
16224 try {
16225 app.canvas?.remove();
16226 } catch {
16227 }
16228 host.replaceChildren();
16229 host.classList.remove("wpd-tagcloud");
16230 };
16231 }
16232 function fontSizeFor(count, max) {
16233 const ratio = Math.sqrt(count / Math.max(1, max));
16234 return Math.round(
16235 MIN_FONT_SIZE + (MAX_FONT_SIZE - MIN_FONT_SIZE) * ratio
16236 );
16237 }
16238 function truncateChipName(name) {
16239 return name.length > CHIP_NAME_MAX_CHARS ? name.slice(0, CHIP_NAME_MAX_CHARS - 1) + "…" : name;
16240 }
16241 function aabbIntersect(a, b) {
16242 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;
16243 }
16244 function slugHash(slug) {
16245 let h = 0;
16246 for (let i = 0; i < slug.length; i++) {
16247 h = (h * 31 + slug.charCodeAt(i)) % 2147483647;
16248 }
16249 return h;
16250 }
16251 function tagHue(slug, baseHue) {
16252 const h = slugHash(slug);
16253 return ((baseHue + h % 256 * 1.4) % 360 + 360) % 360;
16254 }
16255 function tagRotation(slug) {
16256 const h = slugHash(slug);
16257 const sign = h % 2 === 0 ? -1 : 1;
16258 const mag = Math.floor(h / 2) % 4 * 0.011;
16259 return sign * mag;
16260 }
16261 function readAdminThemeHue() {
16262 try {
16263 const value = getComputedStyle(document.documentElement).getPropertyValue("--wp-admin-theme-color").trim();
16264 if (!value) {
16265 return 210;
16266 }
16267 const c = document.createElement("span");
16268 c.style.color = value;
16269 document.body.appendChild(c);
16270 const rgb = getComputedStyle(c).color;
16271 c.remove();
16272 const m = rgb.match(/\d+/g);
16273 if (!m || m.length < 3) {
16274 return 210;
16275 }
16276 return rgbToHue(
16277 parseInt(m[0], 10),
16278 parseInt(m[1], 10),
16279 parseInt(m[2], 10)
16280 );
16281 } catch {
16282 return 210;
16283 }
16284 }
16285 function rgbToHue(r, g, b) {
16286 const rn = r / 255;
16287 const gn = g / 255;
16288 const bn = b / 255;
16289 const max = Math.max(rn, gn, bn);
16290 const min = Math.min(rn, gn, bn);
16291 const d = max - min;
16292 if (d === 0) {
16293 return 210;
16294 }
16295 let h;
16296 switch (max) {
16297 case rn:
16298 h = (gn - bn) / d + (gn < bn ? 6 : 0);
16299 break;
16300 case gn:
16301 h = (bn - rn) / d + 2;
16302 break;
16303 default:
16304 h = (rn - gn) / d + 4;
16305 break;
16306 }
16307 return Math.round(h * 60);
16308 }
16309 function hslToInt(h, s, l) {
16310 const sn = s / 100;
16311 const ln = l / 100;
16312 const c = (1 - Math.abs(2 * ln - 1)) * sn;
16313 const hp = h / 60;
16314 const x = c * (1 - Math.abs(hp % 2 - 1));
16315 let r = 0;
16316 let g = 0;
16317 let b = 0;
16318 if (hp < 1) {
16319 r = c;
16320 g = x;
16321 } else if (hp < 2) {
16322 r = x;
16323 g = c;
16324 } else if (hp < 3) {
16325 g = c;
16326 b = x;
16327 } else if (hp < 4) {
16328 g = x;
16329 b = c;
16330 } else if (hp < 5) {
16331 r = x;
16332 b = c;
16333 } else {
16334 r = c;
16335 b = x;
16336 }
16337 const m = ln - c / 2;
16338 const ri = Math.round((r + m) * 255);
16339 const gi = Math.round((g + m) * 255);
16340 const bi = Math.round((b + m) * 255);
16341 return ri * 65536 + gi * 256 + bi;
16342 }
16343 function stripTags(html2) {
16344 const tmp = document.createElement("div");
16345 tmp.innerHTML = html2;
16346 return tmp.textContent || tmp.innerText || "";
16347 }
16348 function showToast(title, err) {
16349 const reason = err instanceof Error ? err.message : String(err);
16350 const api = window.wp?.desktop;
16351 if (api && typeof api.showToast === "function") {
16352 api.showToast({
16353 message: `${title} ${reason}`.trim(),
16354 duration: 6e3
16355 });
16356 return;
16357 }
16358 console.error(title, err);
16359 }
16360 async function fetchShellJson(client, url) {
16361 const cfg = client.getConfig();
16362 const init = {
16363 method: "GET",
16364 credentials: "same-origin",
16365 headers: {
16366 "X-WP-Nonce": cfg.restNonce,
16367 Accept: "application/json"
16368 }
16369 };
16370 const response = await trackedFetch(url, init, {
16371 windowId: "desktop-mode-posts"
16372 });
16373 if (!response.ok) {
16374 throw new Error(`${response.status} ${response.statusText}`);
16375 }
16376 const json = await response.json();
16377 return { json, headers: response.headers };
16378 }
16379 function computePositionsKey() {
16380 try {
16381 const host = window.location.host || "unknown";
16382 const path = window.location.pathname.replace(/\/?wp-admin\/?.*$/, "");
16383 return `wpd-tagcloud-positions:${host}${path}`;
16384 } catch {
16385 return "wpd-tagcloud-positions:fallback";
16386 }
16387 }
16388 function readPersistedPositions(key) {
16389 try {
16390 const raw = window.localStorage.getItem(key);
16391 if (!raw) {
16392 return /* @__PURE__ */ new Map();
16393 }
16394 const parsed = JSON.parse(raw);
16395 if (!parsed || typeof parsed !== "object") {
16396 return /* @__PURE__ */ new Map();
16397 }
16398 const out = /* @__PURE__ */ new Map();
16399 for (const [k, v] of Object.entries(
16400 parsed
16401 )) {
16402 const id = parseInt(k, 10);
16403 if (!Number.isFinite(id)) {
16404 continue;
16405 }
16406 const pos = v;
16407 if (typeof pos?.x === "number" && typeof pos?.y === "number") {
16408 out.set(id, { x: pos.x, y: pos.y });
16409 }
16410 }
16411 return out;
16412 } catch {
16413 return /* @__PURE__ */ new Map();
16414 }
16415 }
16416 function writePersistedPositions(key, positions) {
16417 try {
16418 const obj = {};
16419 for (const [id, pos] of positions) {
16420 obj[String(id)] = pos;
16421 }
16422 window.localStorage.setItem(key, JSON.stringify(obj));
16423 } catch {
16424 }
16425 }
16426 function createTagChip(pixi, chipLayer, term, fontSize, hue) {
16427 const container = new pixi.Container();
16428 container.eventMode = "static";
16429 container.cursor = "pointer";
16430 const shadow = new pixi.Graphics();
16431 container.addChild(shadow);
16432 const bg = new pixi.Graphics();
16433 container.addChild(bg);
16434 const hashText = new pixi.Text({
16435 text: "#",
16436 style: {
16437 fill: hslToInt(hue, 65, 42),
16438 fontSize,
16439 fontFamily: FONT_FAMILY,
16440 fontWeight: "700"
16441 },
16442 resolution: CHIP_TEXT_RES
16443 });
16444 container.addChild(hashText);
16445 const nameText = new pixi.Text({
16446 text: truncateChipName(term.name),
16447 style: {
16448 fill: 1909543,
16449 fontSize,
16450 fontFamily: FONT_FAMILY,
16451 fontWeight: "600"
16452 },
16453 resolution: CHIP_TEXT_RES
16454 });
16455 container.addChild(nameText);
16456 const countText = new pixi.Text({
16457 text: String(term.count),
16458 style: {
16459 fill: 16777215,
16460 fontSize: Math.max(10, Math.round(fontSize * 0.55)),
16461 fontFamily: FONT_FAMILY,
16462 fontWeight: "700"
16463 },
16464 resolution: CHIP_TEXT_RES
16465 });
16466 container.addChild(countText);
16467 chipLayer.addChild(container);
16468 return {
16469 container,
16470 shadow,
16471 bg,
16472 hashText,
16473 nameText,
16474 countText,
16475 width: 0,
16476 height: 0,
16477 cachedName: "",
16478 cachedCount: -1,
16479 cachedFocused: false,
16480 cachedHover: false,
16481 cachedHue: -1
16482 };
16483 }
16484 const tagsCloud = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
16485 __proto__: null,
16486 mountTagsCloud
16487 }, Symbol.toStringTag, { value: "Module" }));
16488 async function showUsersIntroDialog() {
16489 return new Promise((resolve) => {
16490 const backdrop = document.createElement("div");
16491 backdrop.className = "desktop-mode-users-intro__backdrop";
16492 backdrop.setAttribute("role", "presentation");
16493 Object.assign(backdrop.style, {
16494 position: "fixed",
16495 inset: "0",
16496 background: "color-mix(in srgb, var(--wp-admin-theme-color, #1d2327) 60%, transparent)",
16497 backdropFilter: "blur(2px)",
16498 zIndex: "100000",
16499 display: "flex",
16500 alignItems: "center",
16501 justifyContent: "center",
16502 padding: "24px"
16503 });
16504 const dialog = document.createElement("div");
16505 dialog.setAttribute("role", "dialog");
16506 dialog.setAttribute("aria-modal", "true");
16507 dialog.setAttribute(
16508 "aria-labelledby",
16509 "desktop-mode-users-intro-title"
16510 );
16511 dialog.className = "desktop-mode-users-intro";
16512 Object.assign(dialog.style, {
16513 background: "var(--wp-admin-theme-bg, #fff)",
16514 color: "var(--wp-admin-theme-fg, #1d2327)",
16515 borderRadius: "14px",
16516 boxShadow: "0 24px 60px rgba(0,0,0,.28)",
16517 maxWidth: "520px",
16518 width: "100%",
16519 maxHeight: "90vh",
16520 overflow: "auto",
16521 padding: "28px 32px 24px",
16522 fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
16523 });
16524 dialog.innerHTML = renderDialogMarkup();
16525 backdrop.appendChild(dialog);
16526 document.body.appendChild(backdrop);
16527 const primaryBtn = dialog.querySelector(
16528 '[data-action="confirm"]'
16529 );
16530 const settingsBtn = dialog.querySelector(
16531 '[data-action="settings"]'
16532 );
16533 primaryBtn?.focus();
16534 let resolved = false;
16535 const cleanup = (result) => {
16536 if (resolved) {
16537 return;
16538 }
16539 resolved = true;
16540 document.removeEventListener("keydown", onKey, true);
16541 backdrop.remove();
16542 resolve(result);
16543 };
16544 const onKey = (e) => {
16545 if (e.key === "Escape") {
16546 e.preventDefault();
16547 cleanup("cancel");
16548 }
16549 };
16550 document.addEventListener("keydown", onKey, true);
16551 backdrop.addEventListener("click", (e) => {
16552 if (e.target === backdrop) {
16553 cleanup("cancel");
16554 }
16555 });
16556 primaryBtn?.addEventListener("click", () => cleanup("confirm"));
16557 settingsBtn?.addEventListener("click", () => cleanup("settings"));
16558 });
16559 }
16560 function renderDialogMarkup() {
16561 const title = __("Welcome to the new Users window");
16562 const lede = __(
16563 "Same data you already manage, with the polish the Users list has been waiting for."
16564 );
16565 const highlights = [
16566 __("Live online indicator on every row — see who is around right now."),
16567 __("Last-login column so you finally know who is actually using the site."),
16568 __("Bulk role change with strict role-permission enforcement — never accidentally promote anyone above your own level."),
16569 __("One-click password reset and resend-welcome buttons, with sensible rate-limiting."),
16570 __("Click-to-copy email and a long-overdue search that matches name, username, AND email."),
16571 __("Per-user content stats: posts, pages, comments at a glance.")
16572 ];
16573 const li = (arr) => arr.map(
16574 (s) => `<li><span class="dot" aria-hidden="true"></span>${escapeHtml(s)}</li>`
16575 ).join("");
16576 return `
16577 <style>
16578 .desktop-mode-users-intro h2 {
16579 margin: 0 0 8px;
16580 font-size: 22px;
16581 font-weight: 600;
16582 letter-spacing: -0.01em;
16583 }
16584 .desktop-mode-users-intro p.lede {
16585 margin: 0 0 20px;
16586 color: var(--wp-admin-theme-fg-muted, #50575e);
16587 font-size: 14px;
16588 line-height: 1.5;
16589 }
16590 .desktop-mode-users-intro__list {
16591 list-style: none;
16592 margin: 0 0 22px;
16593 padding: 0;
16594 font-size: 14px;
16595 line-height: 1.5;
16596 }
16597 .desktop-mode-users-intro__list li {
16598 display: flex;
16599 align-items: flex-start;
16600 gap: 10px;
16601 padding: 6px 0;
16602 }
16603 .desktop-mode-users-intro__list .dot {
16604 flex: 0 0 auto;
16605 width: 6px;
16606 height: 6px;
16607 margin-top: 9px;
16608 border-radius: 50%;
16609 background: var(--wp-admin-theme-color, #2271b1);
16610 }
16611 .desktop-mode-users-intro__footer {
16612 display: flex;
16613 justify-content: flex-end;
16614 gap: 8px;
16615 margin-top: 8px;
16616 }
16617 .desktop-mode-users-intro__footer button {
16618 appearance: none;
16619 border: 1px solid var(--wp-admin-theme-border, #dcdcde);
16620 background: var(--wp-admin-theme-bg, #fff);
16621 color: inherit;
16622 padding: 8px 14px;
16623 border-radius: 6px;
16624 font-size: 13px;
16625 cursor: pointer;
16626 }
16627 .desktop-mode-users-intro__footer button.primary {
16628 border-color: var(--wp-admin-theme-color, #2271b1);
16629 background: var(--wp-admin-theme-color, #2271b1);
16630 color: #fff;
16631 font-weight: 500;
16632 }
16633 .desktop-mode-users-intro__footer button:hover { filter: brightness(1.05); }
16634 .desktop-mode-users-intro__footer button:focus-visible {
16635 outline: 2px solid var(--wp-admin-theme-color, #2271b1);
16636 outline-offset: 2px;
16637 }
16638 </style>
16639 <h2 id="desktop-mode-users-intro-title">${escapeHtml(title)}</h2>
16640 <p class="lede">${escapeHtml(lede)}</p>
16641 <ul class="desktop-mode-users-intro__list">${li(highlights)}</ul>
16642 <div class="desktop-mode-users-intro__footer">
16643 <button type="button" data-action="settings">${escapeHtml(
16644 __("Take me to settings")
16645 )}</button>
16646 <button type="button" class="primary" data-action="confirm">${escapeHtml(
16647 __("Got it")
16648 )}</button>
16649 </div>
16650 `;
16651 }
16652 function escapeHtml(s) {
16653 const t = document.createElement("div");
16654 t.textContent = s;
16655 return t.innerHTML;
16656 }
16657 const _initial = {
16658 userId: null,
16659 requestedAt: 0,
16660 tabRequested: false
16661 };
16662 let _store = null;
16663 function getStore() {
16664 if (_store) {
16665 return _store;
16666 }
16667 const w = window;
16668 const factory = w.wp?.desktop?.createSharedStore;
16669 if (typeof factory !== "function") {
16670 return null;
16671 }
16672 _store = factory(
16673 "desktop-mode/user-edit/target",
16674 () => ({ ..._initial })
16675 );
16676 return _store;
16677 }
16678 function setUserEditTarget(userId) {
16679 const store = getStore();
16680 if (store) {
16681 store.state.userId = userId;
16682 store.state.requestedAt = Date.now();
16683 store.state.tabRequested = true;
16684 store.notify();
16685 return;
16686 }
16687 const w = window;
16688 w._wpdUserEditTarget = {
16689 userId,
16690 requestedAt: Date.now(),
16691 tabRequested: true
16692 };
16693 }
16694 function readUserEditTarget() {
16695 const store = getStore();
16696 if (store) {
16697 return { ...store.state };
16698 }
16699 const w = window;
16700 return w._wpdUserEditTarget ?? { ..._initial };
16701 }
16702 function clearUserEditTarget() {
16703 const store = getStore();
16704 if (store) {
16705 store.state.userId = null;
16706 store.state.requestedAt = 0;
16707 store.state.tabRequested = false;
16708 store.notify();
16709 }
16710 const w = window;
16711 if (w._wpdUserEditTarget) {
16712 w._wpdUserEditTarget = {
16713 userId: null,
16714 requestedAt: 0,
16715 tabRequested: false
16716 };
16717 }
16718 }
16719 function setUserEditTabRequested(requested) {
16720 const store = getStore();
16721 if (store) {
16722 store.state.tabRequested = requested;
16723 store.notify();
16724 return;
16725 }
16726 const w = window;
16727 const prev = w._wpdUserEditTarget ?? { ..._initial };
16728 w._wpdUserEditTarget = { ...prev, tabRequested: requested };
16729 }
16730 function subscribeUserEditTarget(cb) {
16731 const store = getStore();
16732 if (!store) {
16733 return () => {
16734 };
16735 }
16736 return store.subscribe((state) => cb({ ...state }));
16737 }
16738 const userEditTarget = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
16739 __proto__: null,
16740 clearUserEditTarget,
16741 readUserEditTarget,
16742 setUserEditTabRequested,
16743 setUserEditTarget,
16744 subscribeUserEditTarget
16745 }, Symbol.toStringTag, { value: "Module" }));
16746 function wpdConfirmGlobal(options) {
16747 const w = window;
16748 const fn = w.wp?.desktop?.confirm;
16749 if (typeof fn !== "function") {
16750 return Promise.resolve(window.confirm(options.message));
16751 }
16752 return fn(options);
16753 }
16754 function notifyToast(body, opts = {}) {
16755 const w = window;
16756 const api = w.wp?.desktop;
16757 if (api?.notify) {
16758 api.notify({ body, kind: opts.kind });
16759 return;
16760 }
16761 console.info("[users-window]", body);
16762 }
16763 function openUserEditWindow(userId) {
16764 if (!Number.isFinite(userId) || userId <= 0) {
16765 return;
16766 }
16767 setUserEditTarget(userId);
16768 console.info(
16769 "[users-window] opening user-edit window for user",
16770 userId
16771 );
16772 const w = window;
16773 const fn = w.wp?.desktop?.openWindow;
16774 if (typeof fn !== "function") {
16775 console.error(
16776 "[users-window] wp.desktop.openWindow is missing — desktop shell may not be ready."
16777 );
16778 notifyToast(
16779 __("Could not open profile window — desktop shell unavailable."),
16780 { kind: "error" }
16781 );
16782 return;
16783 }
16784 const opened = fn("desktop-mode-user-edit", {
16785 source: "users-window/row-click"
16786 });
16787 if (!opened) {
16788 console.error(
16789 '[users-window] openWindow("desktop-mode-user-edit") returned false — window not registered server-side. Check includes/user-edit-window/window.php.'
16790 );
16791 notifyToast(
16792 __("Profile window not registered — see console."),
16793 { kind: "error" }
16794 );
16795 }
16796 }
16797 const ROOT = "[data-desktop-mode-posts-root]";
16798 const STATUS = "[data-desktop-mode-posts-status]";
16799 const SEARCH = "[data-desktop-mode-posts-search]";
16800 const REFRESH = "[data-desktop-mode-posts-refresh]";
16801 const NEW_BTN = "[data-desktop-mode-posts-new]";
16802 const TABLE = "[data-desktop-mode-posts-table]";
16803 const BULK = "[data-desktop-mode-posts-bulk]";
16804 const COUNT = "[data-desktop-mode-posts-count]";
16805 const PAGE_INDICATOR = "[data-desktop-mode-posts-page-indicator]";
16806 const PREV = "[data-desktop-mode-posts-prev]";
16807 const NEXT = "[data-desktop-mode-posts-next]";
16808 const PER_PAGE = "[data-desktop-mode-posts-per-page]";
16809 const BULK_ACTIONS_HOST = "[data-desktop-mode-posts-bulk-actions]";
16810 const SEARCH_DEBOUNCE_MS = 250;
16811 function userCellKey(id, key) {
16812 return `${id}::${key}`;
16813 }
16814 function memoUserCell(cache, id, key, build) {
16815 const k = userCellKey(id, key);
16816 const cached = cache.get(k);
16817 if (cached) {
16818 return cached;
16819 }
16820 const node = build();
16821 cache.set(k, node);
16822 return node;
16823 }
16824 const _usersIntroShown = { v: false };
16825 function maybeShowUsersIntro(client) {
16826 if (_usersIntroShown.v) {
16827 return;
16828 }
16829 let cfg;
16830 try {
16831 cfg = client.getConfig();
16832 } catch {
16833 return;
16834 }
16835 if (cfg.introSeen) {
16836 return;
16837 }
16838 _usersIntroShown.v = true;
16839 void showUsersIntroDialog().then((result) => {
16840 if (result === "cancel") {
16841 _usersIntroShown.v = false;
16842 return;
16843 }
16844 void markUsersIntroSeen(client, cfg);
16845 if (result === "settings") {
16846 const w = window;
16847 w.wp?.desktop?.openOsSettings?.();
16848 }
16849 }).catch(() => {
16850 _usersIntroShown.v = false;
16851 });
16852 }
16853 async function markUsersIntroSeen(client, cfg) {
16854 if (!cfg.introUrl) {
16855 return;
16856 }
16857 try {
16858 await trackedFetch(
16859 cfg.introUrl,
16860 {
16861 method: "POST",
16862 credentials: "same-origin",
16863 headers: {
16864 "Content-Type": "application/json",
16865 "X-WP-Nonce": cfg.restNonce
16866 },
16867 body: JSON.stringify({ slug: "users" })
16868 },
16869 {
16870 windowId: client.windowId,
16871 source: "users-window/intro"
16872 }
16873 );
16874 cfg.introSeen = true;
16875 } catch {
16876 }
16877 }
16878 function buildIdentityCell(row, cfg) {
16879 const cell = document.createElement("span");
16880 cell.style.cssText = "display:flex;align-items:center;gap:10px;min-width:0;";
16881 const avatar = document.createElement("wpd-avatar");
16882 avatar.setAttribute("size", "32");
16883 if (row.name) {
16884 avatar.setAttribute("name", row.name);
16885 }
16886 const presence = row.desktop_mode_presence ?? "offline";
16887 avatar.setAttribute("presence", presence);
16888 const avatars = row.avatar_urls ?? {};
16889 const rawAvatar = avatars["48"] ?? avatars["96"] ?? avatars["24"] ?? "";
16890 if (rawAvatar) {
16891 applyAvatarSrc(avatar, rawAvatar);
16892 }
16893 cell.appendChild(avatar);
16894 const text = document.createElement("span");
16895 text.style.cssText = "display:flex;flex-direction:column;min-width:0;line-height:1.25;";
16896 const nameRow = document.createElement("span");
16897 const name = document.createElement("a");
16898 name.href = `${cfg.editPostUrlBase}?user_id=${row.id}`;
16899 name.textContent = row.name || `#${row.id}`;
16900 name.title = name.textContent;
16901 name.setAttribute("data-noclick", "");
16902 name.style.cssText = "font-weight:600;color:inherit;text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:240px;";
16903 name.addEventListener("mouseenter", () => {
16904 name.style.textDecoration = "underline";
16905 });
16906 name.addEventListener("mouseleave", () => {
16907 name.style.textDecoration = "none";
16908 });
16909 name.addEventListener("click", (e) => {
16910 e.preventDefault();
16911 e.stopPropagation();
16912 void openUserEditWindow(row.id);
16913 });
16914 nameRow.appendChild(name);
16915 text.appendChild(nameRow);
16916 if (row.slug) {
16917 const sub = document.createElement("span");
16918 sub.textContent = `@${row.slug}`;
16919 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;";
16920 text.appendChild(sub);
16921 }
16922 cell.appendChild(text);
16923 return cell;
16924 }
16925 function buildEmailCell(row) {
16926 const cell = document.createElement("button");
16927 cell.type = "button";
16928 const email = typeof row.email === "string" ? row.email : "";
16929 cell.textContent = email || "—";
16930 cell.disabled = email === "";
16931 cell.title = email ? __("Click to copy email") : "";
16932 Object.assign(cell.style, {
16933 appearance: "none",
16934 background: "transparent",
16935 border: "none",
16936 padding: "2px 6px",
16937 font: "inherit",
16938 color: "inherit",
16939 cursor: email ? "copy" : "default",
16940 textAlign: "left",
16941 fontSize: "13px",
16942 borderRadius: "4px",
16943 maxWidth: "100%",
16944 overflow: "hidden",
16945 textOverflow: "ellipsis",
16946 whiteSpace: "nowrap"
16947 });
16948 cell.addEventListener("click", (e) => {
16949 e.stopPropagation();
16950 if (!email) {
16951 return;
16952 }
16953 void navigator.clipboard?.writeText(email).then(() => {
16954 const orig = cell.textContent;
16955 cell.textContent = __("Copied!");
16956 cell.style.color = "var(--wp-admin-theme-color, #2271b1)";
16957 setTimeout(() => {
16958 cell.textContent = orig;
16959 cell.style.color = "";
16960 }, 1200);
16961 }).catch(() => {
16962 });
16963 });
16964 return cell;
16965 }
16966 function buildRoleCell(row, cfg) {
16967 const cell = document.createElement("span");
16968 cell.style.cssText = "display:inline-flex;flex-wrap:wrap;gap:4px;min-width:0;";
16969 const roles = Array.isArray(row.roles) ? row.roles : [];
16970 const labels = cfg.allRoles ?? {};
16971 if (roles.length === 0) {
16972 const none = document.createElement("span");
16973 none.textContent = __("No role");
16974 none.style.cssText = "color:var(--wp-admin-theme-fg-muted, #8c8f94);font-style:italic;";
16975 cell.appendChild(none);
16976 return cell;
16977 }
16978 for (const slug of roles) {
16979 const chip = document.createElement("span");
16980 chip.textContent = labels[slug] ?? slug;
16981 chip.style.cssText = [
16982 "display:inline-flex",
16983 "align-items:center",
16984 "padding:2px 8px",
16985 "border-radius:10px",
16986 "font-size:11px",
16987 "font-weight:600",
16988 "background:rgba(34,113,177,0.10)",
16989 "color:#0a4b78",
16990 "white-space:nowrap"
16991 ].join(";");
16992 cell.appendChild(chip);
16993 }
16994 return cell;
16995 }
16996 function buildStatsCell(row) {
16997 const stats = row.desktop_mode_user_stats ?? {
16998 posts: 0,
16999 pages: 0,
17000 comments: 0
17001 };
17002 const cell = document.createElement("span");
17003 cell.style.cssText = "display:inline-flex;align-items:center;gap:10px;font-size:12px;font-variant-numeric:tabular-nums;";
17004 const mk = (dashicon, count, label) => {
17005 const span = document.createElement("span");
17006 span.style.cssText = "display:inline-flex;align-items:center;gap:3px;";
17007 span.title = label;
17008 const ic = document.createElement("wpd-icon");
17009 ic.setAttribute("name", dashicon);
17010 ic.setAttribute("size", "14");
17011 ic.style.color = "var(--wp-admin-theme-fg-muted, #8c8f94)";
17012 span.appendChild(ic);
17013 const txt = document.createElement("span");
17014 txt.textContent = String(count);
17015 if (count === 0) {
17016 txt.style.color = "var(--wp-admin-theme-fg-muted, #8c8f94)";
17017 }
17018 span.appendChild(txt);
17019 return span;
17020 };
17021 cell.appendChild(mk("admin-post", stats.posts, __("Posts")));
17022 cell.appendChild(mk("admin-page", stats.pages, __("Pages")));
17023 cell.appendChild(
17024 mk("admin-comments", stats.comments, __("Comments"))
17025 );
17026 return cell;
17027 }
17028 function relativeTime(ts) {
17029 const now = Math.floor(Date.now() / 1e3);
17030 const delta = now - ts;
17031 if (delta < 60) {
17032 return __("just now");
17033 }
17034 if (delta < 3600) {
17035 const m = Math.floor(delta / 60);
17036 return sprintf(__("%d min ago"), m);
17037 }
17038 if (delta < 86400) {
17039 const h = Math.floor(delta / 3600);
17040 return sprintf(__("%d h ago"), h);
17041 }
17042 if (delta < 86400 * 30) {
17043 const d = Math.floor(delta / 86400);
17044 return sprintf(__("%d d ago"), d);
17045 }
17046 if (delta < 86400 * 365) {
17047 const mo = Math.floor(delta / (86400 * 30));
17048 return sprintf(__("%d mo ago"), mo);
17049 }
17050 const y = Math.floor(delta / (86400 * 365));
17051 return sprintf(__("%d y ago"), y);
17052 }
17053 function buildLastLoginCell(row) {
17054 const cell = document.createElement("span");
17055 cell.style.cssText = "font-size:13px;font-variant-numeric:tabular-nums;";
17056 const ts = row.desktop_mode_last_login;
17057 if (!ts || typeof ts !== "number") {
17058 cell.textContent = __("Never");
17059 cell.style.color = "var(--wp-admin-theme-fg-muted, #8c8f94)";
17060 return cell;
17061 }
17062 cell.textContent = relativeTime(ts);
17063 const dt = new Date(ts * 1e3);
17064 cell.title = dt.toLocaleString();
17065 return cell;
17066 }
17067 function buildRegisteredCell(row) {
17068 const cell = document.createElement("span");
17069 cell.style.cssText = "font-size:13px;font-variant-numeric:tabular-nums;";
17070 const raw = typeof row.registered_date === "string" ? row.registered_date : "";
17071 if (!raw) {
17072 cell.textContent = "—";
17073 cell.style.color = "var(--wp-admin-theme-fg-muted, #8c8f94)";
17074 return cell;
17075 }
17076 const hasTz = /[Zz]|[+-]\d{2}:?\d{2}$/.test(raw);
17077 const ts = Math.floor(Date.parse(hasTz ? raw : raw + "Z") / 1e3);
17078 if (!Number.isFinite(ts)) {
17079 cell.textContent = raw;
17080 return cell;
17081 }
17082 cell.textContent = relativeTime(ts);
17083 cell.title = new Date(ts * 1e3).toLocaleString();
17084 return cell;
17085 }
17086 function buildActionsCell(row, cfg, client) {
17087 const cell = document.createElement("span");
17088 cell.style.cssText = "display:inline-flex;gap:4px;align-items:center;";
17089 const canEditViewer = cfg.canEdit === true;
17090 const canEditRow = row.desktop_mode_can_edit === true;
17091 if (!canEditViewer || !canEditRow) {
17092 cell.textContent = "—";
17093 cell.style.color = "var(--wp-admin-theme-fg-muted, #8c8f94)";
17094 return cell;
17095 }
17096 const mk = (label, dashicon, fn) => {
17097 const btn = document.createElement("button");
17098 btn.type = "button";
17099 btn.title = label;
17100 btn.setAttribute("aria-label", label);
17101 Object.assign(btn.style, {
17102 appearance: "none",
17103 border: "1px solid var(--wp-admin-theme-border, #dcdcde)",
17104 background: "var(--wp-admin-theme-bg, #fff)",
17105 color: "inherit",
17106 padding: "4px 6px",
17107 borderRadius: "4px",
17108 cursor: "pointer",
17109 lineHeight: "1"
17110 });
17111 const ic = document.createElement("wpd-icon");
17112 ic.setAttribute("name", dashicon);
17113 ic.setAttribute("size", "14");
17114 btn.appendChild(ic);
17115 btn.addEventListener("click", (e) => {
17116 e.stopPropagation();
17117 fn();
17118 });
17119 return btn;
17120 };
17121 cell.appendChild(
17122 mk(
17123 __("Send password reset"),
17124 "email-alt",
17125 async () => {
17126 const ok = await wpdConfirmGlobal({
17127 title: __("Send password reset email?"),
17128 message: sprintf(
17129 // translators: %s is a user name.
17130 __("WordPress will email %s a password-reset link."),
17131 row.name
17132 ),
17133 confirmLabel: __("Send reset email")
17134 });
17135 if (!ok) {
17136 return;
17137 }
17138 const result = await client.sendPasswordReset(row.id);
17139 if (result.ok) {
17140 notifyToast(
17141 sprintf(
17142 // translators: %s is the user's email address.
17143 __("Reset email sent to %s."),
17144 result.email ?? row.email ?? ""
17145 ),
17146 { kind: "success" }
17147 );
17148 } else {
17149 notifyToast(
17150 sprintf(
17151 // translators: %s is an error code.
17152 __("Could not send reset email (%s)."),
17153 result.error ?? "unknown"
17154 ),
17155 { kind: "error" }
17156 );
17157 }
17158 }
17159 )
17160 );
17161 cell.appendChild(
17162 mk(
17163 __("Resend welcome email"),
17164 "megaphone",
17165 async () => {
17166 const ok = await wpdConfirmGlobal({
17167 title: __("Resend welcome email?"),
17168 message: sprintf(
17169 // translators: %s is a user name.
17170 __(
17171 "WordPress will resend the original welcome email to %s."
17172 ),
17173 row.name
17174 ),
17175 confirmLabel: __("Resend")
17176 });
17177 if (!ok) {
17178 return;
17179 }
17180 const result = await client.resendWelcome(row.id);
17181 if (result.ok) {
17182 notifyToast(
17183 sprintf(
17184 // translators: %s is the user's email address.
17185 __("Welcome email resent to %s."),
17186 result.email ?? row.email ?? ""
17187 ),
17188 { kind: "success" }
17189 );
17190 } else {
17191 notifyToast(
17192 sprintf(
17193 // translators: %s is an error code.
17194 __("Could not resend welcome (%s)."),
17195 result.error ?? "unknown"
17196 ),
17197 { kind: "error" }
17198 );
17199 }
17200 }
17201 )
17202 );
17203 return cell;
17204 }
17205 function buildColumns(cache, cfg, client) {
17206 const cols = [
17207 {
17208 key: "identity",
17209 label: __("Name"),
17210 sortable: false,
17211 sticky: true,
17212 minWidth: "260px",
17213 render: (_v, row) => memoUserCell(
17214 cache,
17215 row.id,
17216 "identity",
17217 () => buildIdentityCell(row, cfg)
17218 )
17219 },
17220 {
17221 key: "email",
17222 label: __("Email"),
17223 minWidth: "220px",
17224 render: (_v, row) => memoUserCell(cache, row.id, "email", () => buildEmailCell(row))
17225 },
17226 {
17227 key: "role",
17228 label: __("Role"),
17229 width: "180px",
17230 render: (_v, row) => memoUserCell(
17231 cache,
17232 row.id,
17233 "role",
17234 () => buildRoleCell(row, cfg)
17235 )
17236 },
17237 {
17238 key: "stats",
17239 label: __("Content"),
17240 width: "160px",
17241 sortValue: (row) => {
17242 const s = row.desktop_mode_user_stats;
17243 return s ? s.posts + s.pages + s.comments : 0;
17244 },
17245 render: (_v, row) => memoUserCell(cache, row.id, "stats", () => buildStatsCell(row))
17246 },
17247 {
17248 key: "last_login",
17249 label: __("Last login"),
17250 width: "140px",
17251 sortable: false,
17252 sortValue: (row) => typeof row.desktop_mode_last_login === "number" ? row.desktop_mode_last_login : 0,
17253 render: (_v, row) => memoUserCell(
17254 cache,
17255 row.id,
17256 "last_login",
17257 () => buildLastLoginCell(row)
17258 )
17259 },
17260 {
17261 key: "registered",
17262 label: __("Registered"),
17263 width: "140px",
17264 sortable: true,
17265 render: (_v, row) => memoUserCell(
17266 cache,
17267 row.id,
17268 "registered",
17269 () => buildRegisteredCell(row)
17270 )
17271 }
17272 ];
17273 if (cfg.canEdit === true) {
17274 cols.push({
17275 key: "actions",
17276 label: __("Actions"),
17277 width: "110px",
17278 sortable: false,
17279 render: (_v, row) => (
17280 // Actions cell is intentionally NOT memoized — its closure
17281 // captures `row` and the row payload changes between
17282 // fetches. Cheap to rebuild, fewer surprises.
17283 buildActionsCell(row, cfg, client)
17284 )
17285 });
17286 }
17287 return cols;
17288 }
17289 function defaultStatusSegments() {
17290 return [
17291 { value: "", label: __("All") },
17292 { value: "online", label: __("Online") },
17293 { value: "recent", label: __("Active 30d") },
17294 { value: "never", label: __("Never logged in") }
17295 ];
17296 }
17297 function applyClientStatusFilter(rows, status) {
17298 if (!status) {
17299 return rows;
17300 }
17301 if (status === "online") {
17302 return rows.filter((r) => r.desktop_mode_presence === "online");
17303 }
17304 if (status === "recent") {
17305 const now = Math.floor(Date.now() / 1e3);
17306 return rows.filter((r) => {
17307 const ts = r.desktop_mode_last_login;
17308 return typeof ts === "number" && ts > 0 && now - ts < 86400 * 30;
17309 });
17310 }
17311 if (status === "never") {
17312 return rows.filter(
17313 (r) => !r.desktop_mode_last_login || typeof r.desktop_mode_last_login !== "number"
17314 );
17315 }
17316 return rows;
17317 }
17318 async function renderUsersWindow(body, client) {
17319 const root = body.querySelector(ROOT);
17320 const table = body.querySelector(TABLE);
17321 if (!root || !table) {
17322 return;
17323 }
17324 table.addEventListener("wpd-table-row-click", (e) => {
17325 const detail = e.detail;
17326 const id = detail?.row?.id;
17327 if (typeof id !== "number" || id <= 0) {
17328 return;
17329 }
17330 void openUserEditWindow(id);
17331 });
17332 maybeShowUsersIntro(client);
17333 const cfg = client.getConfig();
17334 const view = {
17335 page: 1,
17336 perPage: Math.max(1, cfg.defaultPerPage || 20),
17337 search: "",
17338 status: "",
17339 orderby: "name",
17340 order: "asc",
17341 roles: [],
17342 searchDebounce: null
17343 };
17344 const cellCache = /* @__PURE__ */ new Map();
17345 table.columns = buildColumns(cellCache, cfg, client);
17346 table.getRowId = (row) => row.id;
17347 table.sort = { key: "name", direction: "asc" };
17348 if (!cfg.canEdit && !cfg.canPromote && !cfg.canDelete) {
17349 table.removeAttribute("selectable");
17350 }
17351 let totalPages = 0;
17352 let totalRows = 0;
17353 let refreshSeq = 0;
17354 const perPageEl = root.querySelector(PER_PAGE);
17355 if (perPageEl) {
17356 perPageEl.value = String(view.perPage);
17357 }
17358 const indicator = root.querySelector(PAGE_INDICATOR);
17359 const prevBtn = root.querySelector(PREV);
17360 const nextBtn = root.querySelector(NEXT);
17361 const bulkBar = root.querySelector(BULK);
17362 const countEl = root.querySelector(COUNT);
17363 const bulkActionsHost = root.querySelector(BULK_ACTIONS_HOST);
17364 const statusHost = root.querySelector(STATUS);
17365 if (statusHost) {
17366 statusHost.replaceChildren();
17367 for (const seg of defaultStatusSegments()) {
17368 const el = document.createElement("wpd-segment");
17369 el.setAttribute("value", seg.value);
17370 el.textContent = seg.label;
17371 statusHost.appendChild(el);
17372 }
17373 statusHost.addEventListener("wpd-pick", (e) => {
17374 const detail = e.detail;
17375 view.status = detail?.value ?? "";
17376 view.page = 1;
17377 void refresh();
17378 });
17379 }
17380 const searchEl = root.querySelector(SEARCH);
17381 if (searchEl) {
17382 searchEl.addEventListener("input", () => {
17383 if (view.searchDebounce !== null) {
17384 clearTimeout(view.searchDebounce);
17385 }
17386 view.searchDebounce = window.setTimeout(() => {
17387 view.search = searchEl.value.trim();
17388 view.page = 1;
17389 void refresh();
17390 }, SEARCH_DEBOUNCE_MS);
17391 });
17392 }
17393 const refreshBtn = root.querySelector(REFRESH);
17394 refreshBtn?.addEventListener("click", () => {
17395 void refresh();
17396 });
17397 const newBtn = root.querySelector(NEW_BTN);
17398 if (newBtn) {
17399 if (!cfg.canCreate) {
17400 newBtn.style.display = "none";
17401 } else {
17402 newBtn.addEventListener("click", (e) => {
17403 e.preventDefault();
17404 const tabs = body.querySelector(
17405 "[data-desktop-mode-users-tabs]"
17406 );
17407 if (!tabs) {
17408 return;
17409 }
17410 tabs.value = "add-new";
17411 tabs.setAttribute("value", "add-new");
17412 });
17413 }
17414 }
17415 perPageEl?.addEventListener("change", () => {
17416 const n = parseInt(perPageEl.value, 10);
17417 if (Number.isFinite(n) && n > 0) {
17418 view.perPage = n;
17419 view.page = 1;
17420 void refresh();
17421 }
17422 });
17423 const renderBulkBar = () => {
17424 if (!bulkBar || !bulkActionsHost) {
17425 return;
17426 }
17427 const sel = table.selection;
17428 const ids = sel ? Array.from(sel) : [];
17429 if (ids.length === 0) {
17430 bulkBar.hidden = true;
17431 return;
17432 }
17433 bulkBar.hidden = false;
17434 if (countEl) {
17435 countEl.textContent = sprintf(
17436 // translators: %d is a count of selected users.
17437 __("%d selected"),
17438 ids.length
17439 );
17440 }
17441 bulkActionsHost.replaceChildren();
17442 const assignable = cfg.assignableRoles ?? {};
17443 const assignableKeys = Object.keys(assignable);
17444 if (cfg.canPromote && assignableKeys.length > 0) {
17445 const wrap = document.createElement("span");
17446 wrap.style.cssText = "display:inline-flex;align-items:center;gap:6px;";
17447 const roleDropdown = document.createElement("select");
17448 Object.assign(roleDropdown.style, {
17449 padding: "4px 8px",
17450 borderRadius: "4px",
17451 border: "1px solid var(--wp-admin-theme-border, #dcdcde)",
17452 background: "var(--wp-admin-theme-bg, #fff)",
17453 color: "inherit",
17454 font: "inherit",
17455 fontSize: "13px"
17456 });
17457 const placeholder = document.createElement("option");
17458 placeholder.value = "";
17459 placeholder.textContent = __("Set role to…");
17460 roleDropdown.appendChild(placeholder);
17461 for (const slug of assignableKeys) {
17462 const opt = document.createElement("option");
17463 opt.value = slug;
17464 opt.textContent = assignable[slug];
17465 roleDropdown.appendChild(opt);
17466 }
17467 const apply = document.createElement("wpd-button");
17468 apply.setAttribute("variant", "primary");
17469 apply.textContent = __("Apply");
17470 apply.addEventListener("click", async (e) => {
17471 e.preventDefault();
17472 const role = roleDropdown.value;
17473 if (!role) {
17474 return;
17475 }
17476 const ok = await wpdConfirmGlobal({
17477 title: __("Change role for selected users?"),
17478 message: sprintf(
17479 // translators: %1$d is a user count, %2$s is a role label.
17480 __("Set %1$d user(s)' role to %2$s?"),
17481 ids.length,
17482 assignable[role]
17483 ),
17484 confirmLabel: __("Set role")
17485 });
17486 if (!ok) {
17487 return;
17488 }
17489 const out = await client.bulkSetRole(ids, role).catch((err) => {
17490 notifyToast(
17491 String(err.message ?? err),
17492 { kind: "error" }
17493 );
17494 return null;
17495 });
17496 if (!out) {
17497 return;
17498 }
17499 const successes = Object.values(out.results).filter(
17500 (r) => r.ok
17501 ).length;
17502 const failures = ids.length - successes;
17503 if (successes > 0) {
17504 notifyToast(
17505 sprintf(
17506 // translators: %1$d users updated, %2$d failed.
17507 __("Role updated for %1$d user(s) (%2$d skipped)."),
17508 successes,
17509 failures
17510 ),
17511 { kind: failures > 0 ? "info" : "success" }
17512 );
17513 } else {
17514 notifyToast(__("No users updated."), { kind: "error" });
17515 }
17516 void refresh();
17517 });
17518 wrap.appendChild(roleDropdown);
17519 wrap.appendChild(apply);
17520 bulkActionsHost.appendChild(wrap);
17521 }
17522 };
17523 table.addEventListener("wpd-table-selection-change", renderBulkBar);
17524 prevBtn?.addEventListener("click", () => {
17525 if (view.page > 1) {
17526 view.page -= 1;
17527 void refresh();
17528 }
17529 });
17530 nextBtn?.addEventListener("click", () => {
17531 if (view.page < totalPages) {
17532 view.page += 1;
17533 void refresh();
17534 }
17535 });
17536 const updatePager = () => {
17537 if (indicator) {
17538 indicator.textContent = sprintf(
17539 // translators: %1$d current page, %2$d total pages, %3$d total rows.
17540 __("Page %1$d of %2$d · %3$d users"),
17541 view.page,
17542 Math.max(1, totalPages),
17543 totalRows
17544 );
17545 }
17546 if (prevBtn) {
17547 prevBtn.disabled = view.page <= 1;
17548 }
17549 if (nextBtn) {
17550 nextBtn.disabled = view.page >= totalPages;
17551 }
17552 };
17553 const buildParams = () => {
17554 return {
17555 page: view.page,
17556 perPage: view.perPage,
17557 search: view.search || void 0,
17558 roles: view.roles.length > 0 ? view.roles : void 0,
17559 orderby: view.orderby,
17560 order: view.order
17561 };
17562 };
17563 const refresh = async () => {
17564 const mySeq = ++refreshSeq;
17565 table.toggleAttribute("loading", true);
17566 try {
17567 const result = await client.fetchUsers(buildParams());
17568 if (mySeq !== refreshSeq) {
17569 return;
17570 }
17571 if (result.items.length === 0 && view.page > 1 && result.totalPages > 0 && view.page > result.totalPages) {
17572 view.page = 1;
17573 await refresh();
17574 return;
17575 }
17576 cellCache.clear();
17577 const filtered = applyClientStatusFilter(result.items, view.status);
17578 table.data = filtered;
17579 totalRows = result.total;
17580 totalPages = result.totalPages;
17581 updatePager();
17582 renderBulkBar();
17583 } catch (err) {
17584 console.error("[users-window] fetch failed:", err);
17585 notifyToast(
17586 __("Could not load users. Try Refresh."),
17587 { kind: "error" }
17588 );
17589 } finally {
17590 table.toggleAttribute("loading", false);
17591 }
17592 };
17593 mountAddUserForm(body, client, cfg, {
17594 afterCreate: () => {
17595 const tabs = body.querySelector(
17596 "[data-desktop-mode-users-tabs]"
17597 );
17598 if (tabs) {
17599 tabs.value = "all";
17600 tabs.setAttribute("value", "all");
17601 }
17602 view.page = 1;
17603 void refresh();
17604 }
17605 });
17606 wireProfileSubTab(body, cfg);
17607 const patchUserRow = async (id) => {
17608 try {
17609 const updated = await client.fetchOneUser(id);
17610 const list = table.data;
17611 const idx = list.findIndex((r) => r.id === id);
17612 if (idx < 0) {
17613 return;
17614 }
17615 if (!updated) {
17616 const next2 = list.slice();
17617 next2.splice(idx, 1);
17618 table.data = next2;
17619 return;
17620 }
17621 for (const k of Array.from(cellCache.keys())) {
17622 if (k.startsWith(`${id}::`)) {
17623 cellCache.delete(k);
17624 }
17625 }
17626 const next = list.slice();
17627 next[idx] = updated;
17628 table.data = applyClientStatusFilter(next, view.status);
17629 } catch (err) {
17630 console.warn("[users-window] row patch failed, falling back to refresh", err);
17631 void refresh();
17632 }
17633 };
17634 const subscribeApi = window.wp?.desktop;
17635 const unsubscribe = subscribeApi?.subscribe?.(
17636 "desktop-mode.user.changed",
17637 (payload) => {
17638 const ids = payload?.ids;
17639 if (!Array.isArray(ids)) {
17640 return;
17641 }
17642 for (const raw of ids) {
17643 const id = typeof raw === "number" ? raw : Number(raw);
17644 if (Number.isFinite(id) && id > 0) {
17645 void patchUserRow(id);
17646 }
17647 }
17648 }
17649 );
17650 if (unsubscribe) {
17651 document.addEventListener(
17652 "desktop-mode-window-closed",
17653 (e) => {
17654 const detail = e.detail;
17655 if (detail?.windowId === "desktop-mode-users") {
17656 unsubscribe();
17657 }
17658 },
17659 { once: false }
17660 );
17661 }
17662 void refresh();
17663 }
17664 function wireProfileSubTab(body, cfg) {
17665 const profile = body.querySelector(
17666 "wpd-user-profile[data-wpd-user-profile-self]"
17667 );
17668 if (!profile) {
17669 return;
17670 }
17671 const viewerId = cfg.currentUserId;
17672 if (typeof viewerId === "number" && viewerId > 0) {
17673 profile.setAttribute("user-id", String(viewerId));
17674 }
17675 }
17676 function mountAddUserForm(body, client, cfg, opts) {
17677 const formNullable = body.querySelector(
17678 "[data-desktop-mode-users-add-form]"
17679 );
17680 if (!formNullable) {
17681 return;
17682 }
17683 const form = formNullable;
17684 const defaultRole = cfg.defaultRole ?? "subscriber";
17685 const assignableRoles = cfg.assignableRoles && Object.keys(cfg.assignableRoles).length > 0 ? cfg.assignableRoles : { [defaultRole]: defaultRole };
17686 mountSelect(form, "role", __("Role"), assignableRoles, defaultRole);
17687 mountSelect(
17688 form,
17689 "locale",
17690 __("Language"),
17691 cfg.locales ?? { "": __("Site default") },
17692 ""
17693 );
17694 const generateBtn = form.querySelector(
17695 '[data-action="generate-password"]'
17696 );
17697 generateBtn?.addEventListener("click", (e) => {
17698 e.preventDefault();
17699 e.stopPropagation();
17700 const pwd = generateStrongPassword(18);
17701 const pwdField = form.querySelector(
17702 'wpd-text-field[name="password"]'
17703 );
17704 if (pwdField) {
17705 pwdField.value = pwd;
17706 pwdField.setAttribute("value", pwd);
17707 }
17708 void navigator.clipboard?.writeText(pwd).catch(() => {
17709 });
17710 notifyToast(__("Generated password copied to clipboard."), {
17711 kind: "success"
17712 });
17713 });
17714 let pending = false;
17715 form.addEventListener("wpd-form-submit", (e) => {
17716 const detail = e.detail;
17717 void onSubmit(detail.values);
17718 });
17719 async function onSubmit(values) {
17720 if (pending) {
17721 return;
17722 }
17723 pending = true;
17724 form.setBusy(true);
17725 form.clearErrors();
17726 const payload = {
17727 username: String(values.username ?? "").trim(),
17728 email: String(values.email ?? "").trim(),
17729 first_name: optionalString(values.first_name),
17730 last_name: optionalString(values.last_name),
17731 url: optionalString(values.url),
17732 locale: String(values.locale ?? ""),
17733 password: optionalString(values.password),
17734 role: optionalString(values.role),
17735 send_notification: Boolean(values.send_notification)
17736 };
17737 const result = await client.createUser(payload);
17738 pending = false;
17739 form.setBusy(false);
17740 if (!result.ok) {
17741 handleCreateError(form, result.error, result.message, payload);
17742 return;
17743 }
17744 notifyToast(
17745 sprintf(
17746 // translators: %s is the user's email address.
17747 __("User created — welcome email sent to %s."),
17748 result.email ?? payload.email
17749 ),
17750 { kind: "success" }
17751 );
17752 opts.afterCreate();
17753 }
17754 }
17755 function mountSelect(form, name, _label, optionsMap, initialValue) {
17756 const select = form.querySelector(
17757 `wpd-select[name="${name}"]`
17758 );
17759 if (!select) {
17760 return;
17761 }
17762 const items = Object.entries(optionsMap).map(([value, label]) => ({
17763 value,
17764 label
17765 }));
17766 select.items = items;
17767 if (initialValue && optionsMap[initialValue] !== void 0) {
17768 select.value = initialValue;
17769 select.setAttribute("value", initialValue);
17770 }
17771 }
17772 function handleCreateError(form, code, message, payload) {
17773 let summary = message;
17774 if (!summary) {
17775 switch (code) {
17776 case "desktop_mode_users_username_exists":
17777 case "existing_user_login":
17778 summary = __("That username is already in use.");
17779 break;
17780 case "desktop_mode_users_email_exists":
17781 case "existing_user_email":
17782 summary = __("That email is already in use.");
17783 break;
17784 case "desktop_mode_users_username_invalid":
17785 summary = __("Username is not valid.");
17786 break;
17787 case "desktop_mode_users_email_invalid":
17788 summary = __("A valid email address is required.");
17789 break;
17790 case "desktop_mode_users_role_forbidden":
17791 summary = __("You are not allowed to assign that role.");
17792 break;
17793 default:
17794 summary = __("Could not create the user.");
17795 }
17796 }
17797 form.setError(summary);
17798 if (code === "desktop_mode_users_username_exists" || code === "existing_user_login" || code === "desktop_mode_users_username_invalid") {
17799 form.setFieldInvalid("username");
17800 }
17801 if (code === "desktop_mode_users_email_exists" || code === "existing_user_email" || code === "desktop_mode_users_email_invalid") {
17802 form.setFieldInvalid("email");
17803 }
17804 if (code === "desktop_mode_users_role_forbidden") {
17805 form.setFieldInvalid("role");
17806 }
17807 notifyToast(summary, { kind: "error" });
17808 console.warn("[users-window] create failed", { code, payload });
17809 }
17810 function optionalString(value) {
17811 if (typeof value !== "string") {
17812 return void 0;
17813 }
17814 const trimmed = value.trim();
17815 return trimmed === "" ? void 0 : trimmed;
17816 }
17817 function generateStrongPassword(length) {
17818 const upper = "ABCDEFGHJKLMNPQRSTUVWXYZ";
17819 const lower = "abcdefghjkmnpqrstuvwxyz";
17820 const digits = "23456789";
17821 const symbols = "!@#$%^&*-_=+";
17822 const all = upper + lower + digits + symbols;
17823 const buf = new Uint32Array(length);
17824 crypto.getRandomValues(buf);
17825 let out = "";
17826 for (let i = 0; i < length; i += 1) {
17827 out += all[buf[i] % all.length];
17828 }
17829 return out;
17830 }
17831 const usersRender = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
17832 __proto__: null,
17833 renderUsersWindow
17834 }, Symbol.toStringTag, { value: "Module" }));
17835 exports.renderPostsWindow = renderPostsWindow;
17836 Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
17837 return exports;
17838 }({});
17839